v360_ui 1.0.2
v360_ui: ^1.0.2 copied to clipboard
A comprehensive Flutter UI component library built on top of ForUI with styled buttons, dialogs, tiles, cards, and input controls.
example/lib/main.dart
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:v360_ui/v360_ui.dart';
void main() => runApp(const ExampleApp());
/// Root widget wiring up the ForUI theme that every `v360_ui` component needs.
///
/// `v360_ui` re-exports `package:forui/forui.dart`, so [FTheme], [FThemes],
/// [FScaffold] and [FToaster] are all available from the single
/// `package:v360_ui/v360_ui.dart` import.
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
final isDesktop = switch (defaultTargetPlatform) {
TargetPlatform.macOS ||
TargetPlatform.linux ||
TargetPlatform.windows => true,
_ => false,
};
final theme = isDesktop
? FThemes.zinc.light.desktop
: FThemes.zinc.light.touch;
return MaterialApp(
title: 'v360_ui example',
debugShowCheckedModeBanner: false,
localizationsDelegates: FLocalizations.localizationsDelegates,
supportedLocales: FLocalizations.supportedLocales,
theme: theme.toApproximateMaterialTheme(),
builder: (context, child) => FTheme(
data: theme,
child: FToaster(child: FTooltipGroup(child: child!)),
),
home: const HomePage(),
);
}
}
/// A single scrolling gallery of the most common `v360_ui` components.
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return FScaffold(
header: const FHeader(title: Text('v360_ui')),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
const _Section(
title: 'Typography',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
VTitle('Large title', size: VTitleSize.large, bold: true),
VTitle('Medium secondary', variant: VTitleVariant.secondary),
VDetail('A detail line used for supporting body copy.'),
],
),
),
const _Section(title: 'Buttons', child: _ButtonsSection()),
const _Section(
title: 'Badges',
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
VBadge(child: Text('Info')),
VBadge(variant: VBadgeVariant.success, child: Text('Success')),
VBadge(variant: VBadgeVariant.warning, child: Text('Warning')),
VBadge(variant: VBadgeVariant.danger, child: Text('Danger')),
],
),
),
const _Section(
title: 'Alerts',
child: Column(
spacing: 12,
children: [
VAlert(
icon: Icons.info_outline,
title: 'Sync complete',
subtitle: 'All devices are up to date.',
),
VAlert(
icon: Icons.warning_amber_outlined,
title: 'Payment failed',
subtitle: 'Your transaction could not be processed.',
variant: VAlertVariant.destructive,
),
],
),
),
const _Section(title: 'Form controls', child: _FormSection()),
const _Section(title: 'Card', child: _CardSection()),
const _Section(
title: 'Tiles',
child: VTileGroup(
label: Text('Connected devices'),
children: [
VTile(title: Text('Router'), prefix: Icon(Icons.router)),
VTile(title: Text('Printer'), prefix: Icon(Icons.print)),
],
),
),
_Section(
title: 'Table',
child: VTable(
height: 180,
columnFlex: [1, 2, 1],
rightColumns: [0],
centerColumns: [2],
headers: ['ID', 'Customer', 'Status'],
rows: [
['#1001', 'Alice Johnson', 'Active'],
['#1002', 'Bob Smith', 'Inactive'],
['#1003', 'Carol White', 'Active'],
],
),
),
const _Section(
title: 'Progress & avatar',
child: Row(
spacing: 16,
children: [
Expanded(child: VProgress(value: 0.65)),
VProgress(mode: VProgressMode.circularIndeterminate),
VAvatar(text: 'AB'),
],
),
),
const _Section(title: 'Dialogs', child: _DialogsSection()),
],
),
);
}
}
/// A labelled block used to separate each part of the gallery.
class _Section extends StatelessWidget {
const _Section({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
VTitle(title, size: VTitleSize.small, variant: VTitleVariant.secondary),
const SizedBox(height: 8),
child,
],
),
);
}
}
class _ButtonsSection extends StatelessWidget {
const _ButtonsSection();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
VButton(onPress: () {}, child: const Text('Primary')),
VButton(
onPress: () {},
variant: VButtonVariant.secondary,
child: const Text('Secondary'),
),
VButton(
onPress: () {},
variant: VButtonVariant.destructive,
child: const Text('Destructive'),
),
VButton(
onPress: () {},
variant: VButtonVariant.outline,
child: const Text('Outline'),
),
VButton(
onPress: () {},
variant: VButtonVariant.ghost,
child: const Text('Ghost'),
),
VButton(
onPress: () {},
tooltip: 'Delete',
prefix: const Icon(Icons.delete_outline),
),
],
),
VButton(
onPress: () {},
fullwidth: true,
prefix: const Icon(Icons.save_outlined),
child: const Text('Full-width'),
),
],
);
}
}
class _FormSection extends StatefulWidget {
const _FormSection();
@override
State<_FormSection> createState() => _FormSectionState();
}
class _FormSectionState extends State<_FormSection> {
final _name = TextEditingController();
String? _role = 'viewer';
bool _agreed = false;
@override
void dispose() {
_name.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
spacing: 12,
children: [
VTextField(label: 'Name', controller: _name, hint: 'Jane Doe'),
VSelect(
label: 'Role',
hint: 'Pick a role',
value: _role,
items: const {
'admin': 'Administrator',
'editor': 'Editor',
'viewer': 'Viewer',
},
onChange: (value) => setState(() => _role = value),
),
VCheckbox(
label: 'I agree to the terms',
value: _agreed,
onChange: (value) => setState(() => _agreed = value),
),
],
);
}
}
class _CardSection extends StatelessWidget {
const _CardSection();
@override
Widget build(BuildContext context) {
return VCard(
title: const Text('Gratitude'),
subtitle: const Text('Readiness to show appreciation and return kindness.'),
content: const VInfoItem(
label: 'Status',
value: 'Processing',
icon: Icons.hourglass_empty,
),
footer: Row(
spacing: 8,
children: [
Expanded(
child: VButton(
onPress: () {},
variant: VButtonVariant.secondary,
child: const Text('Cancel'),
),
),
Expanded(
child: VButton(onPress: () {}, child: const Text('Submit')),
),
],
),
);
}
}
class _DialogsSection extends StatefulWidget {
const _DialogsSection();
@override
State<_DialogsSection> createState() => _DialogsSectionState();
}
class _DialogsSectionState extends State<_DialogsSection> {
String _lastResult = 'No dialog opened yet.';
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
VButton(
onPress: () async {
final confirmed = await VDailog.confirmDialog(
context: context,
title: const Text('Delete item?'),
body: const Text('This action cannot be undone.'),
onConfirm: () {},
);
setState(() => _lastResult = 'Confirm dialog: $confirmed');
},
variant: VButtonVariant.destructive,
child: const Text('Confirm dialog'),
),
VButton(
onPress: () => TextEditDialog.show(
context,
title: 'Rename',
initialValue: 'Untitled',
onSave: (value) async {
setState(() => _lastResult = 'Text dialog saved: $value');
},
),
child: const Text('Text edit dialog'),
),
VButton(
onPress: () => NumericEditDialog.show(
context,
title: 'Set threshold',
initialValue: 42,
suffix: '%',
onSave: (value) async {
setState(() => _lastResult = 'Numeric dialog saved: $value');
},
),
child: const Text('Numeric edit dialog'),
),
],
),
VDetail(_lastResult),
],
);
}
}