nice2dev_flutter_ui 0.2.0
nice2dev_flutter_ui: ^0.2.0 copied to clipboard
NiceToDev Flutter UI — A comprehensive set of enterprise-grade widgets for building ERP applications. Counterpart of @nice2dev/ui for Flutter.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:nice2dev_flutter_ui/nice2dev_flutter_ui.dart';
void main() {
runApp(const NiceExampleApp());
}
class NiceExampleApp extends StatelessWidget {
const NiceExampleApp({super.key});
@override
Widget build(BuildContext context) {
return NiceTheme(
data: NiceThemeData.light(),
child: MaterialApp(
title: 'NiceToDev Flutter UI',
theme: NiceThemeData.light().toThemeData(),
darkTheme: NiceThemeData.dark().toThemeData(),
home: const ExampleHomePage(),
),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
int _selectedIndex = 0;
final List<({String title, IconData icon, Widget page})> _sections = [
(title: 'Buttons', icon: Icons.smart_button, page: const ButtonsSection()),
(title: 'Inputs', icon: Icons.text_fields, page: const InputsSection()),
(title: 'Data', icon: Icons.table_chart, page: const DataSection()),
(title: 'Forms', icon: Icons.dynamic_form, page: const FormsSection()),
(title: 'Feedback', icon: Icons.notifications, page: const FeedbackSection()),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('NiceToDev Flutter UI'),
),
drawer: Drawer(
child: ListView.builder(
itemCount: _sections.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text(
'Widget Examples',
style: TextStyle(color: Colors.white, fontSize: 24),
),
);
}
final section = _sections[index - 1];
return ListTile(
leading: Icon(section.icon),
title: Text(section.title),
selected: _selectedIndex == index - 1,
onTap: () {
setState(() => _selectedIndex = index - 1);
Navigator.pop(context);
},
);
},
),
),
body: _sections[_selectedIndex].page,
);
}
}
// === BUTTONS SECTION ===
class ButtonsSection extends StatelessWidget {
const ButtonsSection({super.key});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle('NiceButton'),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
NiceButton(
label: 'Primary',
onPressed: () {},
),
NiceButton(
label: 'Outlined',
variant: NiceButtonVariant.outlined,
onPressed: () {},
),
NiceButton(
label: 'Text',
variant: NiceButtonVariant.text,
onPressed: () {},
),
NiceButton(
label: 'Danger',
variant: NiceButtonVariant.danger,
onPressed: () {},
),
const NiceButton(
label: 'Disabled',
disabled: true,
),
const NiceButton(
label: 'Loading',
loading: true,
),
],
),
const SizedBox(height: 24),
_SectionTitle('NiceButton Sizes'),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
NiceButton(
label: 'Small',
size: NiceButtonSize.small,
onPressed: () {},
),
NiceButton(
label: 'Medium',
size: NiceButtonSize.medium,
onPressed: () {},
),
NiceButton(
label: 'Large',
size: NiceButtonSize.large,
onPressed: () {},
),
],
),
const SizedBox(height: 24),
_SectionTitle('NiceIconButton'),
Wrap(
spacing: 8,
children: [
NiceIconButton(
icon: Icons.add,
onPressed: () {},
tooltip: 'Add item',
),
NiceIconButton(
icon: Icons.edit,
onPressed: () {},
tooltip: 'Edit',
),
NiceIconButton(
icon: Icons.delete,
onPressed: () {},
tooltip: 'Delete',
),
],
),
const SizedBox(height: 24),
_SectionTitle('NiceButtonGroup'),
const NiceButtonGroup(
children: [
NiceButton(label: 'Left'),
NiceButton(label: 'Center'),
NiceButton(label: 'Right'),
],
),
],
);
}
}
// === INPUTS SECTION ===
class InputsSection extends StatefulWidget {
const InputsSection({super.key});
@override
State<InputsSection> createState() => _InputsSectionState();
}
class _InputsSectionState extends State<InputsSection> {
String _textValue = '';
double _numberValue = 5;
String? _selectedOption;
bool _checkboxValue = false;
bool _toggleValue = false;
String _radioValue = 'option1';
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle('NiceTextInput'),
NiceTextInput(
label: 'Username',
hint: 'Enter your username',
onChanged: (value) => setState(() => _textValue = value),
),
const SizedBox(height: 8),
Text('Value: $_textValue'),
const SizedBox(height: 24),
_SectionTitle('NiceNumberInput'),
NiceNumberInput(
label: 'Quantity',
value: _numberValue,
min: 0,
max: 10,
onChanged: (value) => setState(() => _numberValue = value.toDouble()),
),
Text('Value: $_numberValue'),
const SizedBox(height: 24),
_SectionTitle('NiceSelect'),
NiceSelect<String>(
label: 'Select Option',
value: _selectedOption,
items: const [
NiceSelectItem(value: 'opt1', label: 'Option 1'),
NiceSelectItem(value: 'opt2', label: 'Option 2'),
NiceSelectItem(value: 'opt3', label: 'Option 3'),
],
onChanged: (value) => setState(() => _selectedOption = value),
),
const SizedBox(height: 24),
_SectionTitle('NiceCheckbox'),
NiceCheckbox(
label: 'Accept terms and conditions',
value: _checkboxValue,
onChanged: (value) => setState(() => _checkboxValue = value ?? false),
),
const SizedBox(height: 24),
_SectionTitle('NiceToggle'),
NiceToggle(
label: 'Enable notifications',
value: _toggleValue,
onChanged: (value) => setState(() => _toggleValue = value),
),
const SizedBox(height: 24),
_SectionTitle('NiceRadioGroup'),
NiceRadioGroup<String>(
label: 'Choose option',
value: _radioValue,
items: const [
NiceRadioItem(value: 'option1', label: 'Option 1'),
NiceRadioItem(value: 'option2', label: 'Option 2'),
NiceRadioItem(value: 'option3', label: 'Option 3'),
],
onChanged: (value) => setState(() => _radioValue = value),
),
const SizedBox(height: 24),
_SectionTitle('NiceTextArea'),
const NiceTextArea(
label: 'Description',
hint: 'Enter a description...',
minLines: 3,
maxLines: 5,
),
],
);
}
}
// === DATA SECTION ===
class DataSection extends StatelessWidget {
const DataSection({super.key});
@override
Widget build(BuildContext context) {
final columns = [
NiceGridColumn<Map<String, dynamic>>(
id: 'name',
header: 'Name',
cellBuilder: (row, index) => Text(row['name'] as String),
),
NiceGridColumn<Map<String, dynamic>>(
id: 'email',
header: 'Email',
cellBuilder: (row, index) => Text(row['email'] as String),
),
NiceGridColumn<Map<String, dynamic>>(
id: 'role',
header: 'Role',
cellBuilder: (row, index) => Text(row['role'] as String),
),
];
final rows = [
{'name': 'John Doe', 'email': 'john@example.com', 'role': 'Admin'},
{'name': 'Jane Smith', 'email': 'jane@example.com', 'role': 'User'},
{'name': 'Bob Wilson', 'email': 'bob@example.com', 'role': 'Editor'},
{'name': 'Alice Brown', 'email': 'alice@example.com', 'role': 'User'},
];
return ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle('NiceDataGrid'),
SizedBox(
height: 300,
child: NiceDataGrid<Map<String, dynamic>>(
columns: columns,
rows: rows,
onRowTap: (row, index) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped: ${row['name']}')),
);
},
),
),
const SizedBox(height: 24),
_SectionTitle('NiceList'),
SizedBox(
height: 200,
child: NiceList<String>(
items: List.generate(10, (i) => 'List Item ${i + 1}'),
itemBuilder: (item, index) => ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(item),
subtitle: Text('Subtitle for $item'),
),
),
),
const SizedBox(height: 24),
_SectionTitle('NiceTreeView'),
SizedBox(
height: 200,
child: NiceTreeView<String>(
nodes: [
NiceTreeNode(
key: 'root1',
data: 'Documents',
children: [
NiceTreeNode(key: 'doc1', data: 'Report.pdf'),
NiceTreeNode(key: 'doc2', data: 'Presentation.pptx'),
],
),
NiceTreeNode(
key: 'root2',
data: 'Images',
children: [
NiceTreeNode(key: 'img1', data: 'Photo.jpg'),
NiceTreeNode(key: 'img2', data: 'Logo.png'),
],
),
],
labelBuilder: (node) => node.data,
),
),
],
);
}
}
// === FEEDBACK SECTION ===
class FeedbackSection extends StatelessWidget {
const FeedbackSection({super.key});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle('Toast'),
NiceButton(
label: 'Show Toast',
onPressed: () {
showNiceToast(
context,
message: 'This is a toast notification!',
severity: NiceToastSeverity.success,
);
},
),
const SizedBox(height: 24),
_SectionTitle('Dialog'),
NiceButton(
label: 'Show Dialog',
onPressed: () async {
final result = await showNiceDialog(
context,
title: 'Confirm Action',
message: 'Are you sure you want to continue?',
confirmLabel: 'Yes',
cancelLabel: 'No',
);
if (context.mounted) {
showNiceToast(
context,
message: result ? 'Confirmed!' : 'Cancelled',
severity: result ? NiceToastSeverity.success : NiceToastSeverity.info,
);
}
},
),
const SizedBox(height: 24),
_SectionTitle('Loading States'),
Wrap(
spacing: 16,
runSpacing: 8,
children: [
const SizedBox(
width: 100,
child: Column(
children: [
NiceSpinner(),
SizedBox(height: 8),
Text('Spinner'),
],
),
),
const SizedBox(
width: 100,
child: Column(
children: [
NiceSkeleton(
width: 80,
height: 80,
borderRadius: 8,
),
SizedBox(height: 8),
Text('Skeleton'),
],
),
),
],
),
const SizedBox(height: 24),
_SectionTitle('Alerts'),
const NiceAlert(
severity: NiceAlertSeverity.info,
message: 'This is an info alert.',
),
const SizedBox(height: 8),
const NiceAlert(
severity: NiceAlertSeverity.success,
message: 'This is a success alert.',
),
const SizedBox(height: 8),
const NiceAlert(
severity: NiceAlertSeverity.warning,
message: 'This is a warning alert.',
),
const SizedBox(height: 8),
const NiceAlert(
severity: NiceAlertSeverity.error,
message: 'This is an error alert.',
),
const SizedBox(height: 24),
_SectionTitle('Empty State'),
const NiceEmpty(
icon: Icons.inbox_outlined,
title: 'No items',
description: 'There are no items to display.',
),
],
);
}
}
// === FORMS SECTION ===
class FormsSection extends StatefulWidget {
const FormsSection({super.key});
@override
State<FormsSection> createState() => _FormsSectionState();
}
class _FormsSectionState extends State<FormsSection> {
String _submittedJson = '';
// JSON Schema example - contact form
static const Map<String, dynamic> _contactSchema = {
'type': 'object',
'properties': {
'firstName': {
'type': 'string',
'title': 'First Name',
'minLength': 2,
'maxLength': 50,
},
'lastName': {
'type': 'string',
'title': 'Last Name',
'minLength': 2,
'maxLength': 50,
},
'email': {
'type': 'string',
'title': 'Email',
'format': 'email',
},
'age': {
'type': 'integer',
'title': 'Age',
'minimum': 18,
'maximum': 120,
},
'country': {
'title': 'Country',
'oneOf': [
{'const': 'PL', 'title': 'Poland'},
{'const': 'DE', 'title': 'Germany'},
{'const': 'US', 'title': 'United States'},
{'const': 'UK', 'title': 'United Kingdom'},
{'const': 'FR', 'title': 'France'},
],
},
'newsletter': {
'type': 'boolean',
'title': 'Subscribe to newsletter',
'default': false,
},
'bio': {
'type': 'string',
'title': 'Bio',
'maxLength': 500,
},
},
'required': ['firstName', 'lastName', 'email'],
};
// UI Schema for custom rendering
static const Map<String, dynamic> _contactUiSchema = {
'firstName': {'ui:order': 1},
'lastName': {'ui:order': 2},
'email': {'ui:order': 3},
'age': {'ui:order': 4},
'country': {'ui:order': 5},
'newsletter': {'ui:order': 6},
'bio': {'ui:order': 7, 'ui:widget': 'textarea'},
};
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle('JSON Schema Form'),
const Text(
'Form automatically generated from JSON Schema (draft-07).\n'
'Supports validation, custom widgets, and nested objects.',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: NiceJsonSchemaForm(
schema: _contactSchema,
uiSchema: _contactUiSchema,
columnsPerRow: 2,
onSubmit: (values) {
setState(() {
_submittedJson = _formatJson(values);
});
showNiceToast(
context,
message: 'Form submitted successfully!',
severity: NiceToastSeverity.success,
);
},
submitLabel: 'Submit Form',
),
),
),
if (_submittedJson.isNotEmpty) ...[
const SizedBox(height: 24),
_SectionTitle('Submitted Data'),
Card(
color: Colors.grey.shade100,
child: Padding(
padding: const EdgeInsets.all(16),
child: SelectableText(
_submittedJson,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
],
const SizedBox(height: 32),
_SectionTitle('NiceFormBuilder'),
const Text(
'Low-code form builder using field definitions.',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: NiceFormBuilder(
rows: [
NiceFormRow(fields: [
const NiceFormFieldDef(
key: 'companyName',
type: NiceFormFieldType.text,
label: 'Company Name',
required: true,
),
const NiceFormFieldDef(
key: 'taxId',
type: NiceFormFieldType.text,
label: 'Tax ID (NIP)',
hint: '10 digits',
),
]),
NiceFormRow(fields: [
const NiceFormFieldDef(
key: 'employees',
type: NiceFormFieldType.number,
label: 'Number of Employees',
),
const NiceFormFieldDef(
key: 'founded',
type: NiceFormFieldType.date,
label: 'Founded Date',
),
]),
NiceFormRow(fields: [
const NiceFormFieldDef(
key: 'industry',
type: NiceFormFieldType.dropdown,
label: 'Industry',
options: ['Technology', 'Finance', 'Healthcare', 'Manufacturing', 'Retail'],
),
]),
NiceFormRow(fields: [
const NiceFormFieldDef(
key: 'description',
type: NiceFormFieldType.textarea,
label: 'Company Description',
),
]),
NiceFormRow(fields: [
const NiceFormFieldDef(
key: 'active',
type: NiceFormFieldType.toggle,
label: 'Active',
defaultValue: true,
),
const NiceFormFieldDef(
key: 'verified',
type: NiceFormFieldType.checkbox,
label: 'Verified Company',
),
]),
],
onSubmit: (values) {
showNiceToast(
context,
message: 'Company form submitted!',
severity: NiceToastSeverity.success,
);
},
submitLabel: 'Save Company',
),
),
),
],
);
}
String _formatJson(Map<String, dynamic> data) {
final buffer = StringBuffer();
buffer.writeln('{');
final entries = data.entries.toList();
for (var i = 0; i < entries.length; i++) {
final e = entries[i];
final comma = i < entries.length - 1 ? ',' : '';
if (e.value is String) {
buffer.writeln(' "${e.key}": "${e.value}"$comma');
} else {
buffer.writeln(' "${e.key}": ${e.value}$comma');
}
}
buffer.write('}');
return buffer.toString();
}
}
// Helper widget for section titles
class _SectionTitle extends StatelessWidget {
final String title;
const _SectionTitle(this.title);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12, top: 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
);
}
}