neo_mobile_kit 0.2.4
neo_mobile_kit: ^0.2.4 copied to clipboard
Neo-Brutalist Flutter UI components, theme tokens, layout helpers, backgrounds, navigation, and motion primitives.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:neo_mobile_kit/neo_mobile_kit.dart';
void main() {
runApp(const NeoMobileKitExample());
}
class NeoMobileKitExample extends StatefulWidget {
const NeoMobileKitExample({super.key});
@override
State<NeoMobileKitExample> createState() => _NeoMobileKitExampleState();
}
class _NeoMobileKitExampleState extends State<NeoMobileKitExample> {
ThemeMode _themeMode = ThemeMode.light;
NeoThemePreset _preset = NeoThemePreset.worldSkills;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: NeoTheme.buildTheme(Brightness.light, preset: _preset),
darkTheme: NeoTheme.buildTheme(Brightness.dark, preset: _preset),
themeMode: _themeMode,
home: CatalogPage(
themeMode: _themeMode,
preset: _preset,
onThemeModeChanged: (value) => setState(() => _themeMode = value),
onPresetChanged: (value) => setState(() => _preset = value),
onReset: () => setState(() {
_themeMode = ThemeMode.light;
_preset = NeoThemePreset.worldSkills;
}),
),
);
}
}
class CatalogPage extends StatefulWidget {
const CatalogPage({
super.key,
required this.themeMode,
required this.preset,
required this.onThemeModeChanged,
required this.onPresetChanged,
required this.onReset,
});
final ThemeMode themeMode;
final NeoThemePreset preset;
final ValueChanged<ThemeMode> onThemeModeChanged;
final ValueChanged<NeoThemePreset> onPresetChanged;
final VoidCallback onReset;
@override
State<CatalogPage> createState() => _CatalogPageState();
}
class _CatalogPageState extends State<CatalogPage> {
final TextEditingController _searchController = TextEditingController();
final GlobalKey _revealKey = GlobalKey();
Set<String> _filters = {'widgets'};
int _bottomIndex = 0;
int _tabIndex = 0;
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final palette = NeoPalette.of(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
return NeoPageShell(
background: NeoShellBackground.index(index: _bottomIndex),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NeoAppBar(
title: 'Neo Brutalist UI',
subtitle: 'Component catalog',
isDark: isDark,
searchController: _searchController,
onSearchChanged: (_) {},
onThemeToggle: (value) {
widget.onThemeModeChanged(value ? ThemeMode.dark : ThemeMode.light);
},
onMenuPressed: () => _openMenu(context),
onSettingsPressed: () => _openSettings(context),
),
const SizedBox(height: NeoSpacing.lg),
NeoSectionHeader(
eyebrow: 'CATALOG',
title: 'Neo-Brutalist building blocks',
subtitle:
'Composable widgets, layouts, motion primitives, and visual layers for Flutter interfaces.',
action: NeoIconButton(
key: _revealKey,
icon: Icons.bolt_rounded,
onPressed: _runReveal,
),
),
const SizedBox(height: NeoSpacing.lg),
NeoTabs(
currentIndex: _tabIndex,
onChanged: (value) => setState(() => _tabIndex = value),
items: const [
NeoTabItem(label: 'Widgets', icon: Icons.widgets_rounded),
NeoTabItem(label: 'Motion', icon: Icons.auto_awesome_rounded),
NeoTabItem(label: 'Data', icon: Icons.table_chart_rounded),
],
),
const SizedBox(height: NeoSpacing.lg),
NeoEntranceGroup(
sort: NeoStaggerSort.inOrder,
preset: NeoEntrancePreset.fadeSlideUp,
children: [
_ThemeAndLayoutSection(palette: palette),
_CoreWidgetsSection(
filters: _filters,
onFiltersChanged: (value) => setState(() => _filters = value),
),
_DataWidgetsSection(tabIndex: _tabIndex),
_MotionAndBackgroundSection(palette: palette),
_PresentationSection(
onOpenOnboarding: _openOnboarding,
onOpenSplash: _openSplash,
),
NeoBottomNav(
currentIndex: _bottomIndex,
onChanged: (value) => setState(() => _bottomIndex = value),
items: const [
NeoBottomNavItem(label: 'Home', icon: Icons.home_rounded),
NeoBottomNavItem(label: 'Profile', icon: Icons.person_rounded),
],
),
],
),
],
),
);
}
void _openMenu(BuildContext context) {
showNeoSideMenu(
context: context,
title: 'SECTIONS',
items: [
NeoSideMenuItem(
label: 'Widgets',
icon: Icons.widgets_rounded,
isSelected: _tabIndex == 0,
onPressed: () => setState(() => _tabIndex = 0),
),
NeoSideMenuItem(
label: 'Motion',
icon: Icons.auto_awesome_rounded,
isSelected: _tabIndex == 1,
onPressed: () => setState(() => _tabIndex = 1),
),
],
);
}
void _openSettings(BuildContext context) {
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
builder: (_) => SafeArea(
child: NeoAppSettingsSheet(
themeMode: widget.themeMode,
preset: widget.preset,
onThemeModeChanged: widget.onThemeModeChanged,
onPresetChanged: widget.onPresetChanged,
onReset: widget.onReset,
),
),
);
}
void _runReveal() {
showNeoOrbRevealTransition(
context: context,
originKey: _revealKey,
color: NeoPalette.of(context).primary,
onCovered: () {},
);
}
void _openOnboarding() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => NeoOnboardingPage(
appName: 'Component Study',
steps: const [
NeoOnboardingStep(
title: 'Compose surfaces',
description: 'Use panels, inputs, navigation, and motion as independent pieces.',
icon: Icons.dashboard_customize_rounded,
),
NeoOnboardingStep(
title: 'Control behavior',
description: 'Keep state, routing, validation, and data in your own application code.',
icon: Icons.tune_rounded,
),
],
onFinish: () => Navigator.of(context).pop(),
),
),
);
}
void _openSplash() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => NeoSplashPage(
replace: false,
logoText: 'NEO',
nextPageBuilder: (_) => const _SplashLandingPage(),
),
),
);
}
}
class _ThemeAndLayoutSection extends StatelessWidget {
const _ThemeAndLayoutSection({required this.palette});
final NeoPalette palette;
@override
Widget build(BuildContext context) {
return NeoWindowPanel(
title: 'theme_layout.dart',
trailing: NeoPill(label: palette.isDark ? 'DARK' : 'LIGHT'),
child: NeoSplitLayout(
primary: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NeoSummaryCard(
title: 'Palette',
value: palette.primaryStrong.toString(),
helper: 'NeoPalette, NeoColors, and NeoTokens drive custom composition.',
badge: 'TOKENS',
icon: Icons.palette_rounded,
),
const SizedBox(height: NeoSpacing.md),
NeoStepper(
currentStep: 1,
steps: const [
NeoStepItem(label: 'Theme', helper: 'Build ThemeData'),
NeoStepItem(label: 'Layout', helper: 'Compose sections'),
NeoStepItem(label: 'Motion', helper: 'Add animation'),
],
),
],
),
secondary: const NeoPanel(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NeoPill(label: 'SPACING'),
SizedBox(height: NeoSpacing.sm),
Text('NeoBreakpoints and NeoSpacing keep custom layouts aligned.'),
],
),
),
),
);
}
}
class _CoreWidgetsSection extends StatelessWidget {
const _CoreWidgetsSection({
required this.filters,
required this.onFiltersChanged,
});
final Set<String> filters;
final ValueChanged<Set<String>> onFiltersChanged;
@override
Widget build(BuildContext context) {
return NeoWindowPanel(
title: 'core_widgets.dart',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NeoInfoBanner(
variant: NeoInfoBannerVariant.info,
title: 'Composable components',
message: 'Each widget renders presentation only; behavior comes from callbacks and external state.',
trailing: const NeoPill(label: 'API'),
),
const SizedBox(height: NeoSpacing.md),
NeoFilterChips<String>(
selectedValues: filters,
onChanged: onFiltersChanged,
items: const [
NeoFilterChipItem(value: 'widgets', label: 'Widgets', icon: Icons.widgets_rounded),
NeoFilterChipItem(value: 'forms', label: 'Forms', icon: Icons.edit_rounded),
NeoFilterChipItem(value: 'feedback', label: 'Feedback', icon: Icons.info_rounded),
],
),
const SizedBox(height: NeoSpacing.md),
NeoAccordion(
title: 'Form controls',
subtitle: 'Inputs, buttons, search, and press feedback.',
initiallyExpanded: true,
child: Column(
children: [
const NeoInputField(
label: 'Label',
hint: 'Type something',
icon: Icons.edit_rounded,
),
const SizedBox(height: NeoSpacing.md),
NeoSearchBox(controller: TextEditingController(), hintText: 'Search locally'),
const SizedBox(height: NeoSpacing.md),
NeoPressMotion(
onTap: () {},
child: NeoButton(
label: 'Primary action',
icon: Icons.arrow_forward_rounded,
onPressed: () {},
),
),
const SizedBox(height: NeoSpacing.md),
NeoButton(
label: 'Secondary action',
variant: NeoButtonVariant.secondary,
onPressed: () {},
),
],
),
),
const SizedBox(height: NeoSpacing.md),
NeoActionTile(
title: 'Open details',
subtitle: 'Action tile with custom trailing content.',
trailing: const NeoPill(label: 'GO'),
onTap: () {},
),
const SizedBox(height: NeoSpacing.md),
NeoCompactListItem(
title: 'Compact row',
subtitle: 'Dense content with badges and metadata.',
meta: '12m',
badges: const [NeoPill(label: 'NEW')],
selected: true,
),
const SizedBox(height: NeoSpacing.md),
NeoListItem(
title: 'Readable row',
subtitle: 'A larger list item with icon and pill label.',
icon: Icons.article_rounded,
pillLabel: 'DOC',
),
const SizedBox(height: NeoSpacing.md),
const NeoDetailRow(
label: 'Status',
value: 'Ready',
helper: 'Displayed with NeoDetailRow.',
emphasis: true,
),
const SizedBox(height: NeoSpacing.md),
NeoBottomSheetCard(
title: 'Inline sheet shell',
subtitle: 'The same surface can be placed in a modal or inline.',
primaryActionLabel: 'Accept',
secondaryActionLabel: 'Cancel',
onPrimaryPressed: () {},
onSecondaryPressed: () {},
child: const Text('Custom content belongs here.'),
),
const SizedBox(height: NeoSpacing.md),
NeoEmptyState(
icon: Icons.inbox_rounded,
title: 'Nothing selected',
message: 'Use this surface when a collection or filter has no visible data.',
buttonLabel: 'Create item',
onPressed: () {},
),
const SizedBox(height: NeoSpacing.md),
NeoOrbSubmitButton<String>(
label: 'Submit with orb',
onSubmit: () async => 'ok',
onSuccess: (_) {},
),
const SizedBox(height: NeoSpacing.md),
const Center(
child: NeoOrbSubmitSpinner(color: Colors.black, size: 28),
),
],
),
);
}
}
class _DataWidgetsSection extends StatelessWidget {
const _DataWidgetsSection({required this.tabIndex});
final int tabIndex;
@override
Widget build(BuildContext context) {
return NeoWindowPanel(
title: 'data_feedback.dart',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NeoMetricStrip(
items: const [
NeoMetricItem(label: 'Widgets', value: '42', helper: 'Documented'),
NeoMetricItem(label: 'Groups', value: '8', helper: 'Catalog sections'),
NeoMetricItem(label: 'Tab', value: '0', helper: 'Local state'),
],
),
const SizedBox(height: NeoSpacing.md),
NeoDataTableLite(
columns: const [
NeoDataTableColumn(label: 'Component', flex: 2),
NeoDataTableColumn(label: 'Layer'),
NeoDataTableColumn(label: 'State'),
],
rows: [
const NeoDataTableRow(cells: ['NeoPanel', 'Surface', 'Stable'], highlight: true),
NeoDataTableRow(cells: ['NeoTabs', 'Navigation', 'Index $tabIndex']),
],
),
const SizedBox(height: NeoSpacing.md),
const NeoProgressBadge(
label: 'Documentation',
progress: .86,
value: '86%',
),
const SizedBox(height: NeoSpacing.md),
NeoStatusCluster(
items: const [
NeoStatusItem(label: 'Stable', tone: NeoStatusTone.success),
NeoStatusItem(label: 'Review', tone: NeoStatusTone.warning),
NeoStatusItem(label: 'Neutral', tone: NeoStatusTone.neutral),
],
),
const SizedBox(height: NeoSpacing.md),
const NeoShapeLoader(label: 'Loading state'),
const SizedBox(height: NeoSpacing.md),
NeoTimeline(
highlightedIndex: 1,
entries: const [
NeoTimelineEntry(title: 'Install', subtitle: 'Add the package.'),
NeoTimelineEntry(title: 'Compose', subtitle: 'Place widgets in your layout.'),
NeoTimelineEntry(title: 'Customize', subtitle: 'Tune colors and motion.'),
],
),
const SizedBox(height: NeoSpacing.md),
const NeoStatCard(
label: 'Coverage',
value: 'All',
helper: 'Current public widgets are represented.',
icon: Icons.verified_rounded,
),
],
),
);
}
}
class _MotionAndBackgroundSection extends StatelessWidget {
const _MotionAndBackgroundSection({required this.palette});
final NeoPalette palette;
@override
Widget build(BuildContext context) {
return NeoWindowPanel(
title: 'motion_backgrounds.dart',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 180,
child: Stack(
children: [
Positioned.fill(
child: NeoOrbBackground(specs: buildAuthOrbs(palette)),
),
const Center(child: NeoPill(label: 'NeoOrbBackground')),
],
),
),
const SizedBox(height: NeoSpacing.md),
SizedBox(
height: 150,
child: Stack(
children: [
Positioned.fill(
child: NeoMoonBackground(
specs: [
NeoMoonSpec(
leftFactor: .15,
topFactor: .2,
size: 90,
color: NeoColors.moon,
shadowColor: palette.shadowSoft,
pageColor: palette.page,
borderColor: palette.outline,
xTravel: 8,
yTravel: 10,
),
],
),
),
const Center(child: NeoPill(label: 'NeoMoonBackground')),
],
),
),
const SizedBox(height: NeoSpacing.md),
const SizedBox(height: 160, child: NeoProfileMotionBanner()),
const SizedBox(height: NeoSpacing.md),
NeoFadeSlide(child: NeoPanel(child: Text('NeoFadeSlide'))),
const SizedBox(height: NeoSpacing.md),
NeoPop(child: NeoPanel(child: Text('NeoPop'))),
const SizedBox(height: NeoSpacing.md),
NeoPanelDrop(child: NeoPanel(child: Text('NeoPanelDrop'))),
const SizedBox(height: NeoSpacing.md),
NeoScrollReveal(
preset: NeoEntrancePreset.neoPop,
child: NeoPanel(child: Text('NeoScrollReveal')),
),
const SizedBox(height: NeoSpacing.md),
Text(
'Motion token duration: ${NeoMotionTokens.normal.inMilliseconds}ms',
style: Theme.of(context).textTheme.labelLarge,
),
],
),
);
}
}
class _PresentationSection extends StatelessWidget {
const _PresentationSection({
required this.onOpenOnboarding,
required this.onOpenSplash,
});
final VoidCallback onOpenOnboarding;
final VoidCallback onOpenSplash;
@override
Widget build(BuildContext context) {
return NeoWindowPanel(
title: 'presentation.dart',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NeoOnboardingWizard(
steps: const [
NeoOnboardingWizardStep(
id: 'one',
title: 'Choose primitives',
description: 'Use only the components that fit your interface.',
icon: Icons.widgets_rounded,
content: Text('Arbitrary widget content can live inside each step.'),
),
NeoOnboardingWizardStep(
id: 'two',
title: 'Connect behavior',
description: 'Callbacks expose navigation without owning it.',
icon: Icons.route_rounded,
content: Text('The host application decides what happens next.'),
),
],
onComplete: () {},
),
const SizedBox(height: NeoSpacing.md),
NeoButton(
label: 'Open NeoOnboardingPage',
icon: Icons.play_arrow_rounded,
onPressed: onOpenOnboarding,
),
const SizedBox(height: NeoSpacing.md),
NeoButton(
label: 'Open NeoSplashPage',
icon: Icons.auto_awesome_rounded,
variant: NeoButtonVariant.secondary,
onPressed: onOpenSplash,
),
],
),
);
}
}
class _SplashLandingPage extends StatelessWidget {
const _SplashLandingPage();
@override
Widget build(BuildContext context) {
return NeoPageShell(
alignment: Alignment.center,
child: NeoPanel(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const NeoPill(label: 'SPLASH COMPLETE'),
const SizedBox(height: NeoSpacing.md),
NeoButton(
label: 'Close',
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
);
}
}