vroxaldesign 1.0.1
vroxaldesign: ^1.0.1 copied to clipboard
Vroxal Design System for Flutter — design tokens (color, typography, spacing), a 1,598-glyph custom icon library (VdIcons), and 21 production-ready UI components with automatic light/dark mode support.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:vroxaldesign/vroxaldesign.dart';
void main() => runApp(const ShowcaseApp());
class ShowcaseApp extends StatelessWidget {
const ShowcaseApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Vroxal Design Showcase',
debugShowCheckedModeBanner: false,
theme: ThemeData(brightness: Brightness.light),
darkTheme: ThemeData(brightness: Brightness.dark),
themeMode: ThemeMode.system,
home: const _RootNav(),
);
}
}
// ── Root navigation ───────────────────────────────────────────
class _RootNav extends StatefulWidget {
const _RootNav();
@override
State<_RootNav> createState() => _RootNavState();
}
class _RootNavState extends State<_RootNav> {
int _index = 0;
static const _tabs = [
(label: 'Actions', icon: Icons.touch_app),
(label: 'Displays', icon: Icons.label),
(label: 'Feedback', icon: Icons.notifications),
(label: 'Forms', icon: Icons.edit_note),
(label: 'Phase 3', icon: Icons.new_releases),
];
static const _pages = <Widget>[
_Phase1Screen(),
_Phase2DisplaysScreen(),
_Phase2FeedbacksScreen(),
_Phase2FormsScreen(),
_Phase3Screen(),
];
@override
Widget build(BuildContext context) {
final scheme = VdColorScheme.of(context);
return Scaffold(
backgroundColor: scheme.backgroundDefaultBase,
body: _pages[_index],
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: [
for (final t in _tabs)
NavigationDestination(icon: Icon(t.icon), label: t.label),
],
),
);
}
}
// ── Shared helpers ────────────────────────────────────────────
Widget _section(BuildContext context, String title) {
return Padding(
padding: const EdgeInsets.only(top: VdSpacing.s800, bottom: VdSpacing.s400),
child: Text(
title,
style: VdFont.labelSmall.textStyle.copyWith(
color: VdColorScheme.of(context).contentDefaultTertiary,
),
),
);
}
Widget _row(List<Widget> children) {
return Wrap(
spacing: VdSpacing.s300,
runSpacing: VdSpacing.s300,
crossAxisAlignment: WrapCrossAlignment.center,
children: children,
);
}
AppBar _appBar(BuildContext context, String title) {
final scheme = VdColorScheme.of(context);
return AppBar(
backgroundColor: scheme.backgroundDefaultBase,
elevation: 0,
title: Text(
title,
style: VdFont.titleMedium.textStyle.copyWith(color: scheme.contentDefaultBase),
),
);
}
// ─────────────────────────────────────────────────────────────
// Phase 1 — Actions
// ─────────────────────────────────────────────────────────────
class _Phase1Screen extends StatefulWidget {
const _Phase1Screen();
@override
State<_Phase1Screen> createState() => _Phase1ScreenState();
}
class _Phase1ScreenState extends State<_Phase1Screen> {
bool _chipSelected = false;
bool _loading = false;
@override
Widget build(BuildContext context) {
final scheme = VdColorScheme.of(context);
return Scaffold(
backgroundColor: scheme.backgroundDefaultBase,
appBar: _appBar(context, 'Phase 1 — Actions'),
body: ListView(
padding: const EdgeInsets.all(VdSpacing.s600),
children: [
_section(context, 'VdButton — Styles'),
_row([
VdButton('Solid', onPressed: () {}),
VdButton('Subtle', style: VdButtonStyle.subtle, onPressed: () {}),
VdButton('Outlined', style: VdButtonStyle.outlined, onPressed: () {}),
VdButton('Ghost', style: VdButtonStyle.transparent, onPressed: () {}),
]),
_section(context, 'VdButton — Colors'),
_row([
VdButton('Primary', color: VdButtonColor.primary, onPressed: () {}),
VdButton('Neutral', color: VdButtonColor.neutral, onPressed: () {}),
VdButton('Error', color: VdButtonColor.error, onPressed: () {}),
VdButton('Success', color: VdButtonColor.success, onPressed: () {}),
VdButton('Warning', color: VdButtonColor.warning, onPressed: () {}),
VdButton('Info', color: VdButtonColor.info, onPressed: () {}),
]),
_section(context, 'VdButton — Sizes'),
_row([
VdButton('Small', size: VdButtonSize.small, onPressed: () {}),
VdButton('Medium', onPressed: () {}),
VdButton('Large', size: VdButtonSize.large, onPressed: () {}),
]),
_section(context, 'VdButton — Modifiers'),
_row([
VdButton('Rounded', rounded: true, onPressed: () {}),
VdButton('With Icon', leftIcon: const Icon(Icons.add), onPressed: () {}),
VdButton(
_loading ? 'Loading' : 'Toggle Load',
isLoading: _loading,
onPressed: () => setState(() => _loading = !_loading),
),
VdButton('Disabled', isDisabled: true, onPressed: () {}),
]),
_section(context, 'VdButton — Full width'),
VdButton('Full Width', fullWidth: true, onPressed: () {}),
const SizedBox(height: VdSpacing.s200),
VdButton(
'Full Width Outlined',
fullWidth: true,
style: VdButtonStyle.outlined,
onPressed: () {},
),
_section(context, 'VdIconButton — Styles'),
_row([
VdIconButton(icon: const Icon(Icons.star), onPressed: () {}),
VdIconButton(icon: const Icon(Icons.star), style: VdIconButtonStyle.subtle, onPressed: () {}),
VdIconButton(icon: const Icon(Icons.star), style: VdIconButtonStyle.outlined, onPressed: () {}),
VdIconButton(icon: const Icon(Icons.star), style: VdIconButtonStyle.transparent, onPressed: () {}),
]),
_section(context, 'VdIconButton — Sizes & Rounded'),
_row([
VdIconButton(icon: const Icon(Icons.add), size: VdIconButtonSize.small, onPressed: () {}),
VdIconButton(icon: const Icon(Icons.add), onPressed: () {}),
VdIconButton(icon: const Icon(Icons.add), size: VdIconButtonSize.large, onPressed: () {}),
VdIconButton(icon: const Icon(Icons.add), rounded: true, onPressed: () {}),
]),
_section(context, 'VdChip'),
_row([
VdChip('Default', onTap: () {}),
VdChip(
_chipSelected ? 'Selected' : 'Tap to select',
isSelected: _chipSelected,
onTap: () => setState(() => _chipSelected = !_chipSelected),
),
VdChip('Closable', closable: true, onTap: () {}, onClose: () {}),
VdChip('With Icon', icon: const Icon(Icons.label), onTap: () {}),
VdChip('Disabled', isDisabled: true, onTap: () {}),
]),
const SizedBox(height: VdSpacing.s1600),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────
// Phase 2 — Displays
// ─────────────────────────────────────────────────────────────
class _Phase2DisplaysScreen extends StatelessWidget {
const _Phase2DisplaysScreen();
@override
Widget build(BuildContext context) {
final scheme = VdColorScheme.of(context);
return Scaffold(
backgroundColor: scheme.backgroundDefaultBase,
appBar: _appBar(context, 'Phase 2 — Displays'),
body: ListView(
padding: const EdgeInsets.all(VdSpacing.s600),
children: [
_section(context, 'VdBadge — Colors (solid)'),
_row([
VdBadge('Primary', color: VdBadgeColor.primary),
VdBadge('Neutral', color: VdBadgeColor.neutral),
VdBadge('Error', color: VdBadgeColor.error),
VdBadge('Success', color: VdBadgeColor.success),
VdBadge('Warning', color: VdBadgeColor.warning),
VdBadge('Info', color: VdBadgeColor.info),
]),
_section(context, 'VdBadge — Styles'),
_row([
VdBadge('Solid', style: VdBadgeStyle.solid),
VdBadge('Subtle', style: VdBadgeStyle.subtle),
]),
_section(context, 'VdBadge — Sizes & Rounded'),
_row([
VdBadge('Small', size: VdBadgeSize.small),
VdBadge('Medium'),
VdBadge('Rounded SM', size: VdBadgeSize.small, rounded: true),
VdBadge('Rounded MD', rounded: true),
]),
_section(context, 'VdDivider — Horizontal'),
const VdDivider(),
const SizedBox(height: VdSpacing.s200),
const VdDivider(label: 'Center'),
const SizedBox(height: VdSpacing.s200),
const VdDivider(label: 'Leading', labelAlignment: VdDividerLabelAlignment.leading),
const SizedBox(height: VdSpacing.s200),
const VdDivider(label: 'Trailing', labelAlignment: VdDividerLabelAlignment.trailing),
_section(context, 'VdDivider — Colors'),
_row([
const SizedBox(width: 200, child: VdDivider(color: VdDividerColor.defaultColor)),
const SizedBox(width: 200, child: VdDivider(color: VdDividerColor.primary)),
const SizedBox(width: 200, child: VdDivider(color: VdDividerColor.neutral)),
]),
const SizedBox(height: VdSpacing.s1600),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────
// Phase 2 — Feedbacks
// ─────────────────────────────────────────────────────────────
class _Phase2FeedbacksScreen extends StatefulWidget {
const _Phase2FeedbacksScreen();
@override
State<_Phase2FeedbacksScreen> createState() => _Phase2FeedbacksScreenState();
}
class _Phase2FeedbacksScreenState extends State<_Phase2FeedbacksScreen> {
bool _showOverlay = false;
@override
Widget build(BuildContext context) {
final scheme = VdColorScheme.of(context);
return Scaffold(
backgroundColor: scheme.backgroundDefaultBase,
appBar: _appBar(context, 'Phase 2 — Feedbacks'),
body: Stack(
children: [
ListView(
padding: const EdgeInsets.all(VdSpacing.s600),
children: [
_section(context, 'VdAlert — Colors'),
VdAlert(description: 'Primary alert', color: VdAlertColor.primary),
const SizedBox(height: VdSpacing.s200),
VdAlert(description: 'Success alert', color: VdAlertColor.success),
const SizedBox(height: VdSpacing.s200),
VdAlert(description: 'Error alert', color: VdAlertColor.error),
const SizedBox(height: VdSpacing.s200),
VdAlert(description: 'Warning alert', color: VdAlertColor.warning),
const SizedBox(height: VdSpacing.s200),
VdAlert(description: 'Neutral alert', color: VdAlertColor.neutral),
_section(context, 'VdAlert — With content'),
VdAlert(
title: 'Update available',
description: 'A new version of the app is ready to install.',
color: VdAlertColor.info,
action: 'Update now',
closable: true,
onAction: () {},
onClose: () {},
),
_section(context, 'VdEmptyState'),
VdEmptyState(
icon: const Icon(Icons.inbox),
title: 'No messages yet',
description: 'Your inbox is empty. Start a conversation to see messages here.',
primaryActionTitle: 'New message',
onPrimaryAction: () {},
),
_section(context, 'VdLoadingState — Inline'),
const VdLoadingState(title: 'Loading data…', description: 'Please wait a moment.'),
_section(context, 'VdLoadingState — Overlay (tap button)'),
VdButton(
_showOverlay ? 'Hide overlay' : 'Show overlay',
onPressed: () => setState(() => _showOverlay = !_showOverlay),
),
_section(context, 'VdSkeleton shimmer'),
Container(height: 20, width: 200, color: Colors.transparent)
.vdSkeleton(),
const SizedBox(height: VdSpacing.s200),
const Text('Name').vdSkeleton(),
const SizedBox(height: VdSpacing.s1600),
],
),
if (_showOverlay)
VdLoadingState(
style: VdLoadingStyle.overlay,
title: 'Saving…',
description: 'Do not close the app.',
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────
// Phase 2 — Forms
// ─────────────────────────────────────────────────────────────
class _Phase2FormsScreen extends StatefulWidget {
const _Phase2FormsScreen();
@override
State<_Phase2FormsScreen> createState() => _Phase2FormsScreenState();
}
class _Phase2FormsScreenState extends State<_Phase2FormsScreen> {
final _textCtrl = TextEditingController();
final _textErrorCtrl = TextEditingController(text: 'Bad input');
final _textSuccessCtrl = TextEditingController(text: 'All good');
final _textDisabledCtrl = TextEditingController(text: 'Disabled');
final _textIconCtrl = TextEditingController();
final _areaCtrl = TextEditingController();
final _areaErrorCtrl = TextEditingController();
bool _checked = false;
bool _indeterminate = false;
String _radioPick = 'a';
String _cardPick = 'x';
bool _cardChecked = false;
DateTime? _dateTime;
@override
void dispose() {
_textCtrl.dispose();
_textErrorCtrl.dispose();
_textSuccessCtrl.dispose();
_textDisabledCtrl.dispose();
_textIconCtrl.dispose();
_areaCtrl.dispose();
_areaErrorCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = VdColorScheme.of(context);
return Scaffold(
backgroundColor: scheme.backgroundDefaultBase,
appBar: _appBar(context, 'Phase 2 — Forms'),
body: ListView(
padding: const EdgeInsets.all(VdSpacing.s600),
children: [
_section(context, 'VdTextField — States'),
VdTextField('Normal', controller: _textCtrl, placeholder: 'Enter text'),
const SizedBox(height: VdSpacing.s400),
VdTextField('Error', controller: _textErrorCtrl,
state: VdInputState.error, helperText: 'Something went wrong'),
const SizedBox(height: VdSpacing.s400),
VdTextField('Success', controller: _textSuccessCtrl,
state: VdInputState.success, helperText: 'Looks great'),
const SizedBox(height: VdSpacing.s400),
VdTextField('Disabled', controller: _textDisabledCtrl,
state: VdInputState.disabled),
const SizedBox(height: VdSpacing.s400),
VdTextField('With icon',
controller: _textIconCtrl,
leadingIcon: const Icon(Icons.search),
placeholder: 'Search…'),
const SizedBox(height: VdSpacing.s400),
VdTextField('With limit',
controller: _textCtrl,
characterLimit: 100,
helperText: 'Optional field',
isOptional: true),
_section(context, 'VdTextArea'),
VdTextArea(
controller: _areaCtrl,
label: 'Notes',
placeholder: 'Enter your notes here…',
helperText: 'Up to 240pt of text',
characterLimit: 300,
),
const SizedBox(height: VdSpacing.s400),
VdTextArea(
controller: _areaErrorCtrl,
label: 'Error',
state: VdInputState.error,
helperText: 'Please fill in this field',
),
_section(context, 'VdCheckbox'),
VdCheckbox(
value: _checked,
label: 'Accept terms',
description: 'You agree to our terms of service.',
onChanged: (v) => setState(() => _checked = v),
),
const SizedBox(height: VdSpacing.s300),
VdCheckbox(
value: true,
isIndeterminate: _indeterminate,
label: _indeterminate ? 'Indeterminate' : 'Checked',
onChanged: (_) => setState(() => _indeterminate = !_indeterminate),
),
const SizedBox(height: VdSpacing.s300),
const VdCheckbox(value: false, label: 'Disabled unchecked', isDisabled: true),
const SizedBox(height: VdSpacing.s300),
const VdCheckbox(value: true, label: 'Disabled checked', isDisabled: true),
_section(context, 'VdRadioButton — Standalone'),
VdRadioButton(
isSelected: _radioPick == 'a',
label: 'Option A',
onChanged: (_) => setState(() => _radioPick = 'a'),
),
const SizedBox(height: VdSpacing.s300),
VdRadioButton(
isSelected: _radioPick == 'b',
label: 'Option B',
description: 'Some extra detail',
onChanged: (_) => setState(() => _radioPick = 'b'),
),
const SizedBox(height: VdSpacing.s300),
const VdRadioButton(isSelected: false, label: 'Disabled', isDisabled: true),
_section(context, 'VdRadioGroup'),
VdRadioGroup<String>(
value: _radioPick,
onChanged: (v) => setState(() => _radioPick = v),
child: Column(
children: const [
VdRadioOption(value: 'a', label: 'Option A'),
SizedBox(height: VdSpacing.s300),
VdRadioOption(value: 'b', label: 'Option B'),
SizedBox(height: VdSpacing.s300),
VdRadioOption(value: 'c', label: 'Option C (disabled)', isDisabled: true),
],
),
),
_section(context, 'VdSelectionCard — Checkbox'),
VdSelectionCard(
isSelected: _cardChecked,
title: 'Enable notifications',
description: 'Receive alerts for important updates.',
icon: const Icon(Icons.notifications),
onChanged: (v) => setState(() => _cardChecked = v),
),
const SizedBox(height: VdSpacing.s300),
const VdSelectionCard(
isSelected: true,
title: 'Disabled selected',
isDisabled: true,
),
_section(context, 'VdSelectionCardGroup (radio)'),
VdSelectionCardGroup<String>(
value: _cardPick,
onChanged: (v) => setState(() => _cardPick = v),
child: Column(
children: const [
VdSelectionCardOption(
value: 'x',
title: 'Starter plan',
description: 'Best for individuals.',
icon: Icon(Icons.person),
),
SizedBox(height: VdSpacing.s300),
VdSelectionCardOption(
value: 'y',
title: 'Pro plan',
description: 'Great for small teams.',
icon: Icon(Icons.groups),
),
SizedBox(height: VdSpacing.s300),
VdSelectionCardOption(
value: 'z',
title: 'Enterprise (disabled)',
isDisabled: true,
),
],
),
),
_section(context, 'VdDateTimeField'),
VdDateTimeField(
'Date & Time',
selection: _dateTime,
placeholder: 'Select date & time',
leadingIcon: const Icon(Icons.calendar_today),
helperText: 'Tap to open picker',
onChanged: (d) => setState(() => _dateTime = d),
),
const SizedBox(height: VdSpacing.s400),
VdDateTimeField(
'Date only',
selection: _dateTime,
placeholder: 'Select date',
mode: VdDateTimeFieldMode.date,
leadingIcon: const Icon(Icons.calendar_month),
onChanged: (d) => setState(() => _dateTime = d),
),
const SizedBox(height: VdSpacing.s400),
VdDateTimeField(
'Disabled',
selection: _dateTime,
state: VdInputState.disabled,
helperText: 'Not available right now',
onChanged: null,
),
const SizedBox(height: VdSpacing.s400),
VdDateTimeField(
'Error state',
selection: null,
state: VdInputState.error,
helperText: 'Please select a valid date',
onChanged: (d) => setState(() => _dateTime = d),
),
const SizedBox(height: VdSpacing.s1600),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────
// Phase 3 — VdSnackbar, VdSearchField, VdSelectField, VdCodeInput
// ─────────────────────────────────────────────────────────────
class _Phase3Screen extends StatefulWidget {
const _Phase3Screen();
@override
State<_Phase3Screen> createState() => _Phase3ScreenState();
}
class _Phase3ScreenState extends State<_Phase3Screen> {
final _searchCtrl = TextEditingController();
String? _selectedFruit;
String? _selectedState;
String _code = '';
static const _fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig'];
static const _states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California'];
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = VdColorScheme.of(context);
return Scaffold(
backgroundColor: scheme.backgroundDefaultBase,
appBar: _appBar(context, 'Phase 3'),
body: ListView(
padding: const EdgeInsets.all(VdSpacing.s600),
children: [
// ── VdSnackbar pill ──────────────────────────────
_section(context, 'VdSnackbar — Pill'),
VdSnackbar(message: 'Changes saved successfully'),
const SizedBox(height: VdSpacing.s200),
VdSnackbar(
message: 'Message deleted',
action: 'Undo',
onAction: () {},
),
const SizedBox(height: VdSpacing.s200),
VdSnackbar(
message: 'Your session will expire in 5 minutes',
leadingIcon: const Icon(Icons.warning),
closable: true,
onClose: () {},
),
// ── showVdSnackbar ───────────────────────────────
_section(context, 'showVdSnackbar — Global presenter'),
Wrap(spacing: VdSpacing.s300, runSpacing: VdSpacing.s300, children: [
VdButton(
'Basic',
style: VdButtonStyle.outlined,
onPressed: () => showVdSnackbar(
context,
message: 'Changes saved',
closable: true,
),
),
VdButton(
'With action',
onPressed: () => showVdSnackbar(
context,
message: 'Message deleted',
action: 'Undo',
onAction: () {},
closable: true,
),
),
VdButton(
'With icon',
style: VdButtonStyle.subtle,
onPressed: () => showVdSnackbar(
context,
message: 'File uploaded successfully',
leadingIcon: const Icon(Icons.check_circle),
closable: true,
),
),
]),
// ── VdSearchField ────────────────────────────────
_section(context, 'VdSearchField'),
VdSearchField(controller: _searchCtrl, placeholder: 'Search items…'),
const SizedBox(height: VdSpacing.s400),
VdSearchField(
controller: TextEditingController(),
placeholder: 'Disabled',
isDisabled: true,
),
// ── VdSelectField ────────────────────────────────
_section(context, 'VdSelectField'),
VdSelectField<String>(
selection: _selectedFruit,
options: _fruits,
label: 'Fruit',
placeholder: 'Select a fruit',
helperText: 'Pick your favourite',
onChanged: (v) => setState(() => _selectedFruit = v),
),
const SizedBox(height: VdSpacing.s400),
VdSelectField<String>(
selection: _selectedState,
options: _states,
label: 'State (with icon)',
leadingIcon: const Icon(Icons.location_on),
isOptional: true,
onChanged: (v) => setState(() => _selectedState = v),
),
const SizedBox(height: VdSpacing.s400),
VdSelectField<String>(
selection: null,
options: _fruits,
label: 'Error',
state: VdInputState.error,
helperText: 'Please select an option',
onChanged: null,
),
const SizedBox(height: VdSpacing.s400),
VdSelectField<String>(
selection: _selectedFruit,
options: _fruits,
label: 'Disabled',
state: VdInputState.disabled,
onChanged: null,
),
// ── VdCodeInput ──────────────────────────────────
_section(context, 'VdCodeInput — Normal (6 digits)'),
VdCodeInput(
code: _code,
onChanged: (v) => setState(() => _code = v),
onComplete: () => showVdSnackbar(
context,
message: 'Code complete: $_code',
closable: true,
),
),
if (_code.isNotEmpty) ...[
const SizedBox(height: VdSpacing.s200),
Text(
'Entered: $_code',
style: VdFont.bodySmall.textStyle.copyWith(
color: VdColorScheme.of(context).contentDefaultTertiary,
),
),
],
_section(context, 'VdCodeInput — 4 digits'),
VdCodeInput(
code: '',
length: 4,
onChanged: (_) {},
),
_section(context, 'VdCodeInput — Error'),
VdCodeInput(
code: '123',
state: VdCodeInputState.error,
onChanged: (_) {},
),
_section(context, 'VdCodeInput — Disabled'),
VdCodeInput(
code: '456789',
state: VdCodeInputState.disabled,
onChanged: null,
),
const SizedBox(height: VdSpacing.s1600),
],
),
);
}
}