mervey_ui_kit 0.1.0
mervey_ui_kit: ^0.1.0 copied to clipboard
Adaptive Material/Cupertino design system: theming tokens, semantic color palette and reusable widgets that pick the right backend per platform.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:mervey_ui_kit/mervey_ui_kit.dart';
void main() {
runApp(const ExampleApp());
}
/// Top-level app that exposes a light/dark switch and renders the
/// component showcase.
class ExampleApp extends StatefulWidget {
/// Creates the example app.
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
ThemeMode _mode = ThemeMode.light;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'mervey_ui_kit example',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: _mode,
builder: AppTheme.wrapWithCupertino,
home: _Showcase(
mode: _mode,
onModeChanged: (m) => setState(() => _mode = m),
),
);
}
}
class _Showcase extends StatefulWidget {
const _Showcase({required this.mode, required this.onModeChanged});
final ThemeMode mode;
final ValueChanged<ThemeMode> onModeChanged;
@override
State<_Showcase> createState() => _ShowcaseState();
}
class _ShowcaseState extends State<_Showcase> {
final _textController = TextEditingController();
final _searchController = TextEditingController();
int _tabIndex = 0;
int _navIndex = 0;
bool _switchValue = false;
@override
void dispose() {
_textController.dispose();
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = context.colors;
final tokens = context.typography;
final isDark = widget.mode == ThemeMode.dark;
return AppScaffold(
title: 'mervey_ui_kit',
actions: [
IconButton(
tooltip: isDark ? 'Switch to light' : 'Switch to dark',
icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode),
onPressed: () =>
widget.onModeChanged(isDark ? ThemeMode.light : ThemeMode.dark),
),
],
bottomNavigationBar: AppNavbar(
currentIndex: _navIndex,
onTap: (i) => setState(() => _navIndex = i),
items: const [
AppNavBarItem(icon: Icon(Icons.home), label: 'Home'),
AppNavBarItem(icon: Icon(Icons.search), label: 'Browse'),
AppNavBarItem(icon: Icon(Icons.settings), label: 'Settings'),
],
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.md),
children: [
_Section(
title: 'Typography',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Headline', style: AppTextStyles.headline(tokens)),
const SizedBox(height: AppSpacing.xs),
Text('Title', style: AppTextStyles.title(tokens)),
const SizedBox(height: AppSpacing.xs),
Text('Body', style: AppTextStyles.body(tokens)),
const SizedBox(height: AppSpacing.xs),
Text('Caption', style: AppTextStyles.caption(tokens)),
const SizedBox(height: AppSpacing.xs),
Text('Label', style: AppTextStyles.label(tokens)),
],
),
),
_Section(
title: 'Pills',
child: AppPill(label: 'Active', color: colors.success),
),
_Section(
title: 'Buttons',
child: Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: [
AppButton(label: 'Primary', onPressed: () {}),
AppButton(
label: 'Secondary',
variant: AppButtonVariant.secondary,
onPressed: () {},
),
const AppButton(label: 'Disabled'),
AppButton(label: 'Loading', isLoading: true, onPressed: () {}),
AppGlassButton(onTap: () {}, child: const Icon(Icons.star)),
],
),
),
_Section(
title: 'Inputs',
child: Column(
children: [
AppTextField(
label: 'Username',
controller: _textController,
helperText: 'Lowercase, no spaces',
),
const SizedBox(height: AppSpacing.sm),
AppSearchField(
controller: _searchController,
hintText: 'Search items',
),
],
),
),
_Section(
title: 'Cards',
child: Column(
children: [
const AppCard(child: Text('Plain AppCard content')),
const SizedBox(height: AppSpacing.sm),
AppTappableCard(
padding: const EdgeInsets.all(AppSpacing.md),
onTap: () {},
child: const Text('Tappable card'),
),
],
),
),
_Section(
title: 'Pill tabs',
child: AppPillTabs(
tabs: const ['All', 'Tokens', 'Components', 'States'],
selectedIndex: _tabIndex,
onTabSelected: (i) => setState(() => _tabIndex = i),
),
),
_Section(
title: 'Avatar & badged icon',
child: Row(
children: [
const AvatarInitials(name: 'Mervey UI Kit'),
const SizedBox(width: AppSpacing.md),
BadgedIcon(
icon: Icon(Icons.notifications, color: colors.textPrimary),
count: 7,
),
],
),
),
_Section(
title: 'Switch',
child: Row(
children: [
AppSwitch(
value: _switchValue,
onChanged: (v) => setState(() => _switchValue = v),
),
const SizedBox(width: AppSpacing.sm),
const Text('Toggle me'),
],
),
),
_Section(
title: 'Overlays',
child: Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: [
AppButton(
label: 'Show dialog',
variant: AppButtonVariant.secondary,
onPressed: () => showAppDialog<void>(
context: context,
title: 'Hello',
content: const Text('A platform-native dialog.'),
actions: const [
AppDialogAction(label: 'Cancel'),
AppDialogAction(label: 'OK', isDefault: true),
],
),
),
AppButton(
label: 'Show sheet',
variant: AppButtonVariant.secondary,
onPressed: () => showAppBottomSheet<void>(
context,
builder: (_) => const AppBottomSheet(
header: AppBottomSheetHeader(title: 'Sheet'),
child: Padding(
padding: EdgeInsets.symmetric(vertical: AppSpacing.md),
child: Text('Bottom sheet content'),
),
),
),
),
AppButton(
label: 'Show snack',
variant: AppButtonVariant.secondary,
onPressed: () => context.showSnack(
'Saved',
variant: AppSnackVariant.success,
),
),
],
),
),
_Section(
title: 'States',
child: Column(
children: [
const SizedBox(
height: 80,
child: LoadingState(label: 'Loading'),
),
SizedBox(
height: 80,
child: ErrorState(
label: 'Something went wrong',
onRetry: () {},
),
),
const SizedBox(
height: 60,
child: EmptyState(label: 'Nothing here'),
),
],
),
),
],
),
);
}
}
class _Section extends StatelessWidget {
const _Section({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
final tokens = context.typography;
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.title(
tokens,
).copyWith(color: context.colors.textSecondary),
),
const SizedBox(height: AppSpacing.sm),
child,
],
),
);
}
}