prism_kit 1.1.0
prism_kit: ^1.1.0 copied to clipboard
A unified Flutter component library with a shared design system: adaptive toasts, banners, sheets, data tables, steppers, skeletons, avatars, OTP and chip inputs, timelines, a command palette, search, [...]
example/lib/main.dart
// Prism gallery — interactive demos for every component.
//
// Run: `flutter run` inside this folder, then tap a component from the
// grid. Every demo is a working, copy-pasteable snippet using the real API.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:prism_kit/prism.dart';
void main() => runApp(const PrismGalleryApp());
class PrismGalleryApp extends StatelessWidget {
const PrismGalleryApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Prism gallery',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: const Color(0xFF39A9DB)),
darkTheme: ThemeData(
colorSchemeSeed: const Color(0xFF39A9DB),
brightness: Brightness.dark,
),
home: const GalleryHome(),
);
}
}
class GalleryHome extends StatelessWidget {
const GalleryHome({super.key});
static final demos = <(String, IconData, WidgetBuilder)>[
('Adaptive scaffold', Icons.smartphone, _build(ScaffoldDemo())),
('Toast', Icons.notifications_none, _build(ToastDemo())),
('Banner', Icons.campaign_outlined, _build(BannerDemo())),
('Search', Icons.search, _build(SearchDemo())),
('Settings', Icons.settings_outlined, _build(SettingsDemo())),
('Markdown', Icons.article_outlined, _build(MarkdownDemo())),
('Code view', Icons.code, _build(CodeDemo())),
('Form', Icons.edit_note, _build(FormDemo())),
('Pagination', Icons.menu, _build(PaginationDemo())),
('Master-detail', Icons.view_agenda_outlined, _build(MasterDetailDemo())),
('Adaptive dialog', Icons.open_in_full, _build(DialogDemo())),
('State views', Icons.extension_outlined, _build(StateViewsDemo())),
('Palette', Icons.palette_outlined, _build(PaletteDemo())),
('Changelog', Icons.history, _build(ChangelogDemo())),
('AI chat', Icons.smart_toy_outlined, _build(AiDemo())),
('Skeleton', Icons.blur_on, _build(SkeletonDemo())),
('Avatar', Icons.account_circle_outlined, _build(AvatarDemo())),
('Chip input', Icons.label_outline, _build(ChipInputDemo())),
('OTP field', Icons.password, _build(OtpDemo())),
('Timeline', Icons.timeline, _build(TimelineDemo())),
('Stepper', Icons.linear_scale, _build(StepperDemo())),
('Data table', Icons.table_chart_outlined, _build(DataTableDemo())),
('Command palette', Icons.keyboard_command_key, _build(CommandPaletteDemo())),
('Adaptive sheet', Icons.vertical_align_bottom, _build(AdaptiveSheetDemo())),
('Connection banner', Icons.cloud_off_outlined, _build(ConnectionDemo())),
('Design tokens', Icons.palette, _build(TokensDemo())),
];
static WidgetBuilder _build(Widget widget) => (_) => widget;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Prism gallery')),
body: GridView.count(
crossAxisCount: MediaQuery.sizeOf(context).width > 600 ? 3 : 2,
padding: const EdgeInsets.all(16),
children: [
for (final (name, icon, builder) in demos)
Card(
child: InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute<void>(builder: builder),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 32),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(name, textAlign: TextAlign.center),
),
],
),
),
),
),
],
),
);
}
}
// ─── Adaptive scaffold ──────────────────────────────────────────────────────
// Resize the window to see bottom nav → rail → expanded rail.
class ScaffoldDemo extends StatefulWidget {
const ScaffoldDemo({super.key});
@override
State<ScaffoldDemo> createState() => _ScaffoldDemoState();
}
class _ScaffoldDemoState extends State<ScaffoldDemo> {
int _index = 0;
@override
Widget build(BuildContext context) {
return PrismAdaptiveScaffold(
currentIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
AdaptiveDestination(icon: Icons.home_outlined, selectedIcon: Icons.home, label: 'Home'),
AdaptiveDestination(icon: Icons.search_outlined, selectedIcon: Icons.search, label: 'Search'),
AdaptiveDestination(icon: Icons.settings_outlined, selectedIcon: Icons.settings, label: 'Settings'),
],
bodyBuilder: (i) => Center(
child: Text(
'Page $i — resize the window!',
style: Theme.of(context).textTheme.titleMedium,
),
),
);
}
}
// ─── Toast ──────────────────────────────────────────────────────────────────
class ToastDemo extends StatelessWidget {
const ToastDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismToast')),
body: Center(
child: Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
FilledButton(
onPressed: () => PrismToast.success(context, 'Saved successfully'),
child: const Text('Success'),
),
FilledButton(
onPressed: () => PrismToast.error(context, 'Something went wrong'),
child: const Text('Error'),
),
FilledButton(
onPressed: () => PrismToast.info(context, 'Heads up!'),
child: const Text('Info'),
),
FilledButton(
onPressed: () => PrismToast.warning(context, 'Low battery'),
child: const Text('Warning'),
),
FilledButton(
onPressed: () => PrismToast.show(
context,
PrismToastMessage(
message: 'Report synced to your workspace',
title: 'Done',
duration: const Duration(seconds: 5),
actionLabel: 'Undo',
onAction: () {},
),
),
child: const Text('With action'),
),
],
),
),
);
}
}
// ─── Banner ─────────────────────────────────────────────────────────────────
class BannerDemo extends StatelessWidget {
const BannerDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismBanner')),
body: ListView(
children: [
PrismBanner(
announcements: const [
PrismAnnouncement(
id: 'gallery-demo-1',
version: 1,
title: 'Announcement',
message: 'Dismiss me — I stay dismissed across restarts.',
severity: PrismSeverity.info,
),
PrismAnnouncement(
id: 'gallery-demo-2',
version: 1,
title: 'Critical notice',
message: 'Highest severity wins with PrismBannerPriority.severity.',
severity: PrismSeverity.error,
),
],
priority: PrismBannerPriority.severity,
),
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'The banner shows the highest-severity undismissed announcement. '
'Dismissals persist via shared_preferences — restart and they stay gone.',
),
),
],
),
);
}
}
// ─── Search ─────────────────────────────────────────────────────────────────
class SearchDemo extends StatelessWidget {
const SearchDemo({super.key});
static const fruits = [
'Apple', 'Apricot', 'Banana', 'Blueberry', 'Cherry', 'Cranberry',
'Dragonfruit', 'Durian', 'Elderberry', 'Fig', 'Grape', 'Guava',
'Kiwi', 'Lemon', 'Lime', 'Mango', 'Melon', 'Nectarine', 'Orange',
'Papaya', 'Peach', 'Pear', 'Pineapple', 'Plum', 'Raspberry',
'Strawberry', 'Tangerine', 'Watermelon',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismSearch')),
body: Padding(
padding: const EdgeInsets.all(16),
child: PrismSearch<String>(
items: fruits,
itemLabel: (item) => item,
hintText: 'Search fruits…',
onSelected: (item) => PrismToast.success(context, 'Picked $item'),
),
),
);
}
}
// ─── Settings ───────────────────────────────────────────────────────────────
class SettingsDemo extends StatefulWidget {
const SettingsDemo({super.key});
@override
State<SettingsDemo> createState() => _SettingsDemoState();
}
class _SettingsDemoState extends State<SettingsDemo> {
late final PrismSettingsController _controller;
@override
void initState() {
super.initState();
_controller = PrismSettingsController();
_controller.setAll({'notifications': true, 'dark-mode': false});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismSettingsPage')),
body: PrismSettingsPage(
controller: _controller,
searchable: true,
sections: const [
PrismSettingsSection(
title: 'General',
tiles: [
PrismSwitchTile(
common: PrismTileCommon(
key: 'notifications',
title: 'Notifications',
subtitle: 'Receive break reminders',
leading: Icons.notifications_outlined,
),
),
PrismSwitchTile(
common: PrismTileCommon(
key: 'dark-mode',
title: 'Dark mode',
subtitle: 'Follow the system theme',
leading: Icons.dark_mode,
),
),
PrismActionTile(
common: PrismTileCommon(
key: 'language',
title: 'Language',
subtitle: 'English',
leading: Icons.language,
),
),
],
),
PrismSettingsSection(
title: 'About',
tiles: [
PrismActionTile(
common: PrismTileCommon(
key: 'version',
title: 'Version',
subtitle: '1.0.0',
leading: Icons.info_outline,
),
),
],
),
],
),
);
}
}
// ─── Markdown ───────────────────────────────────────────────────────────────
class MarkdownDemo extends StatelessWidget {
const MarkdownDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismMarkdown')),
body: const SingleChildScrollView(
padding: EdgeInsets.all(16),
child: PrismMarkdown(
'# Hello Prism\n\n'
'A **fast**, plugin-free markdown renderer with full CommonMark\n'
'and GFM support.\n\n'
'## Features\n\n'
'- Rich **bold**, *italic* and ~~strikethrough~~\n'
'- [Links](https://flutter.dev) that are tappable\n'
'- `inline code` and fenced blocks\n\n'
'```dart\n'
"void main() => print('rendered');\n"
'```\n',
),
),
);
}
}
// ─── Code view ──────────────────────────────────────────────────────────────
class CodeDemo extends StatelessWidget {
const CodeDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismCodeView')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
PrismCodeView(code: "void main() {\n print('Hello, Prism!');\n}"),
const SizedBox(height: 16),
const Text('Built-in Dart & JSON tokenizers — bring your own with LanguageTokenizer.'),
],
),
);
}
}
// ─── Form ───────────────────────────────────────────────────────────────────
class FormDemo extends StatefulWidget {
const FormDemo({super.key});
@override
State<FormDemo> createState() => _FormDemoState();
}
class _FormDemoState extends State<FormDemo> {
final _controller = PrismFormController();
String _result = '';
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismForm')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
PrismForm(
controller: _controller,
child: Column(
children: [
PrismTextField.email(name: 'email', label: 'Email'),
const SizedBox(height: 12),
PrismPasswordField(
common: PrismFieldCommon(
name: 'password',
label: 'Password',
validators: [Validators.required('Enter a password')],
),
),
const SizedBox(height: 12),
PrismDropdownField<String>(
common: PrismFieldCommon(
name: 'plan',
label: 'Plan',
validators: [Validators.required('Pick a plan')],
),
items: const ['Free', 'Pro', 'Team'],
itemLabel: (plan) => plan,
),
const SizedBox(height: 12),
PrismCheckboxField(
common: PrismFieldCommon(
name: 'terms',
label: 'I accept the terms of service',
validators: [Validators.required('Please accept')],
),
),
const SizedBox(height: 20),
FilledButton(
onPressed: () async {
final ok = await _controller.submit(
(values) async => _mockRegister(values),
);
setState(() {
_result = ok ? '✅ Account created!' : '❌ Check the fields';
});
},
child: const Text('Create account'),
),
if (_result.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(_result),
),
],
),
),
],
),
);
}
Future<void> _mockRegister(Map<String, Object?> values) async {
await Future<void>.delayed(const Duration(milliseconds: 600));
// ignore: avoid_print
print('registering: $values');
}
}
// ─── Pagination ─────────────────────────────────────────────────────────────
class PaginationDemo extends StatelessWidget {
const PaginationDemo({super.key});
Future<List<String>> _loadPage(int page) async {
await Future<void>.delayed(const Duration(milliseconds: 700));
if (page >= 3) return const []; // end of data
return List.generate(20, (i) => 'Item #${page * 20 + i + 1}');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismPaginatedList')),
body: PrismPaginatedList<String>(
pageLoader: _loadPage,
itemBuilder: (context, item, index) => ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(item),
),
),
);
}
}
// ─── Master-detail ──────────────────────────────────────────────────────────
class MasterDetailDemo extends StatelessWidget {
const MasterDetailDemo({super.key});
@override
Widget build(BuildContext context) {
final items = List.generate(8, (i) => 'Conversation ${i + 1}');
return Scaffold(
appBar: AppBar(title: const Text('PrismMasterDetail')),
body: PrismMasterDetail<String>(
items: items,
masterBuilder: (context, item, selected) => ListTile(
selected: selected,
leading: const Icon(Icons.chat_bubble_outline),
title: Text(item),
subtitle: const Text('Last message preview…'),
),
detailBuilder: (context, item) => Center(child: Text('Detail of $item')),
empty: (context) => const Center(child: Text('Pick a conversation')),
),
);
}
}
// ─── Adaptive dialog ────────────────────────────────────────────────────────
class DialogDemo extends StatelessWidget {
const DialogDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismAdaptiveDialog')),
body: Center(
child: FilledButton(
onPressed: () async {
final ok = await PrismAdaptiveDialog.show<bool>(
context,
builder: (context) => AlertDialog(
title: const Text('Delete conversation?'),
content: const Text('This action cannot be undone.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
);
if (!context.mounted) return;
if (ok ?? false) {
PrismToast.success(context, 'Deleted');
}
},
child: const Text('Show adaptive dialog'),
),
),
);
}
}
// ─── State views ────────────────────────────────────────────────────────────
class StateViewsDemo extends StatefulWidget {
const StateViewsDemo({super.key});
@override
State<StateViewsDemo> createState() => _StateViewsDemoState();
}
class _StateViewsDemoState extends State<StateViewsDemo> {
late final PrismViewStateNotifier<List<String>> _notifier;
int _step = 0;
@override
void initState() {
super.initState();
_notifier = PrismViewStateNotifier<List<String>>.fromFuture(_mockLoad());
_cycle();
}
static Future<List<String>> _mockLoad() async {
await Future<void>.delayed(const Duration(seconds: 2));
return const ['Note A', 'Note B', 'Note C'];
}
Future<void> _cycle() async {
while (mounted) {
await Future<void>.delayed(const Duration(seconds: 2));
if (!mounted) return;
_step = (_step + 1) % 4;
switch (_step) {
case 0:
_notifier.value = const PrismViewLoading();
case 1:
_notifier.value = const PrismViewSuccess(['Note A', 'Note B', 'Note C']);
case 2:
_notifier.value = const PrismViewEmpty();
case 3:
_notifier.value = PrismViewError(StateError('Network unavailable'));
}
}
}
@override
void dispose() {
_notifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismStateView (auto-cycling)')),
body: ValueListenableBuilder(
valueListenable: _notifier,
builder: (context, state, _) => PrismStateView<List<String>>(
state: state,
successBuilder: (notes) => ListView(
children: [for (final n in notes) ListTile(title: Text(n))],
),
),
),
);
}
}
// ─── Palette ────────────────────────────────────────────────────────────────
class PaletteDemo extends StatelessWidget {
const PaletteDemo({super.key});
@override
Widget build(BuildContext context) {
final colors = <Color>[
const Color(0xFF39A9DB), const Color(0xFFF39237),
const Color(0xFF2E7D5B), const Color(0xFFB04141),
];
return Scaffold(
appBar: AppBar(title: const Text('PrismPalette')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'PrismPalette.fromImage(image) extracts:\n'
'vibrant, muted, dominant colors + text contrast pairs.\n\n'
'Sample extracted swatches:',
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Wrap(
spacing: 8,
children: [
for (final c in colors)
Container(
width: 64,
height: 64,
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(12)),
),
],
),
],
),
),
);
}
}
// ─── Changelog ──────────────────────────────────────────────────────────────
class ChangelogDemo extends StatelessWidget {
const ChangelogDemo({super.key});
static const _changelog = '''
# Changelog
## 2.1.0 - 2026-10-30
### Added
- Dark mode support
- Faster sync engine
### Fixed
- Crash when opening locked notes
## 2.0.0 - 2026-09-12
### Added
- End-to-end encryption
''';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ChangelogView')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
ChangelogView(
changelog: _changelog,
highlightVersion: '2.1.0',
bulletBuilder: (bullet) => PrismMarkdown(bullet),
),
],
),
);
}
}
// ─── AI chat ────────────────────────────────────────────────────────────────
class AiDemo extends StatelessWidget {
const AiDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismAi')),
body: const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.smart_toy_outlined, size: 48),
SizedBox(height: 16),
Text(
'Provider-agnostic streaming AI chat.',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
SizedBox(height: 12),
Text(
'PrismAiChatView gives you a complete streaming chat UI.\n\n'
'Plug in any provider:\n\n'
'OpenAiStreamClient(baseUrl: "https://api.openai.com/v1", apiKey: "sk-…")\n'
'OpenAiStreamClient(baseUrl: "http://localhost:11434/v1") // Ollama\n'
'GeminiStreamClient(apiKey: "…")\n'
'AnthropicStreamClient(apiKey: "…")\n\n'
'…or implement AiStreamClient for anything else.\n'
'Includes SSE parsing, typed events (AiDelta, AiToolCall, AiDone),\n'
'and an automatic tool-calling loop (AiAgent).',
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
// ─── Skeleton ───────────────────────────────────────────────────────────────
class SkeletonDemo extends StatelessWidget {
const SkeletonDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismSkeleton')),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
Text('Card skeleton — keeps the page shape while loading'),
SizedBox(height: 12),
PrismSkeletonCard(mediaHeight: 140, lines: 3),
SizedBox(height: 24),
Text('Tile list skeleton'),
SizedBox(height: 12),
PrismSkeletonList.tiles(count: 3),
SizedBox(height: 24),
Text('Text + circle primitives'),
SizedBox(height: 12),
Row(
children: [
PrismSkeleton.circle(diameter: 44),
SizedBox(width: 12),
Expanded(child: PrismSkeletonText(lines: 2, lastLineFraction: 0.4)),
],
),
],
),
);
}
}
// ─── Avatar ─────────────────────────────────────────────────────────────────
class AvatarDemo extends StatelessWidget {
const AvatarDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismAvatar')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('Initials are derived automatically and the colour is '
'stable for a given name.'),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: const [
PrismAvatar(name: 'Ayesha Khan'),
PrismAvatar(name: 'Bilal Ahmed', size: 48),
PrismAvatar(name: 'Chen Wei', size: 56),
PrismAvatar(name: 'Dana Malik', size: 64),
],
),
const SizedBox(height: 24),
const Text('Presence indicators'),
const SizedBox(height: 12),
Wrap(
spacing: 12,
children: const [
PrismAvatar(name: 'Online User', presence: PrismPresence.online),
PrismAvatar(name: 'Away User', presence: PrismPresence.away),
PrismAvatar(name: 'Busy User', presence: PrismPresence.busy),
PrismAvatar(name: 'Offline User', presence: PrismPresence.offline),
],
),
const SizedBox(height: 24),
const Text('Rounded shape for teams and badges'),
const SizedBox(height: 12),
Row(
children: [
const PrismAvatar(
name: 'Acme Inc',
size: 48,
shape: PrismAvatarShape.rounded,
),
const SizedBox(width: 12),
const PrismAvatar(
name: 'Ayesha Khan',
size: 48,
borderWidth: 2,
badge: Icon(Icons.verified, size: 16),
),
],
),
],
),
);
}
}
// ─── Chip input ─────────────────────────────────────────────────────────────
class ChipInputDemo extends StatefulWidget {
const ChipInputDemo({super.key});
@override
State<ChipInputDemo> createState() => _ChipInputDemoState();
}
class _ChipInputDemoState extends State<ChipInputDemo> {
List<String> _tags = ['flutter', 'open-source'];
List<String> _limited = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismChipInput')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
PrismChipInput(
label: 'Tags',
hint: 'Type and press Enter…',
value: _tags,
suggestions: const ['dart', 'flutter', 'ui', 'mobile', 'web', 'api'],
helperText: 'Paste a comma-separated list to add several at once.',
onChanged: (next) => setState(() => _tags = next),
),
const SizedBox(height: 32),
PrismChipInput(
label: 'Up to 3 (no duplicates)',
value: _limited,
maxChips: 3,
onChanged: (next) => setState(() => _limited = next),
),
],
),
);
}
}
// ─── OTP field ──────────────────────────────────────────────────────────────
class OtpDemo extends StatefulWidget {
const OtpDemo({super.key});
@override
State<OtpDemo> createState() => _OtpDemoState();
}
class _OtpDemoState extends State<OtpDemo> {
String? _error;
String _code = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismOtpField')),
body: ListView(
padding: const EdgeInsets.all(24),
children: [
const Text('Six-digit code — paste works, and it auto-submits.'),
const SizedBox(height: 20),
PrismOtpField(
length: 6,
groupSeparatorAfter: 3,
errorText: _error,
helperText: 'Hint: 123456 is accepted',
onChanged: (v) => setState(() {
_code = v;
_error = null;
}),
onCompleted: (code) {
setState(() {
_error = code == '123456' ? null : 'That code is not valid';
});
if (code == '123456') {
PrismToast.success(context, 'Code accepted');
}
},
),
const SizedBox(height: 32),
const Text('Obscured PIN variant'),
const SizedBox(height: 12),
const PrismOtpField(length: 4, obscure: true, autofocus: false),
const SizedBox(height: 20),
Text('Current value: ${_code.isEmpty ? '—' : _code}'),
],
),
);
}
}
// ─── Timeline ───────────────────────────────────────────────────────────────
class TimelineDemo extends StatelessWidget {
const TimelineDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismTimeline')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
PrismTimeline(
entries: [
const PrismTimelineEntry(
title: 'Order placed',
subtitle: 'Payment confirmed',
timestamp: 'Today, 09:12',
semantic: PrismSemantic.success,
),
const PrismTimelineEntry(
title: 'Packed',
timestamp: 'Today, 10:04',
icon: Icons.inventory_2_outlined,
),
PrismTimelineEntry(
title: 'Out for delivery',
subtitle: 'Driver Sam · 2 stops away',
timestamp: 'Today, 11:30',
isActive: true,
child: Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: const Icon(Icons.local_shipping_outlined),
title: const Text('Track live'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
),
),
const PrismTimelineEntry(
title: 'Delivery attempt failed',
subtitle: 'No one home — will retry tomorrow',
timestamp: 'Today, 14:15',
semantic: PrismSemantic.error,
),
],
),
],
),
);
}
}
// ─── Stepper ────────────────────────────────────────────────────────────────
class StepperDemo extends StatefulWidget {
const StepperDemo({super.key});
@override
State<StepperDemo> createState() => _StepperDemoState();
}
class _StepperDemoState extends State<StepperDemo> {
int _index = 1;
final _errored = <int>{};
static const _steps = [
PrismStep(title: 'Account', subtitle: 'Email & password'),
PrismStep(title: 'Profile', subtitle: 'Name & photo'),
PrismStep(title: 'Preferences', subtitle: 'Notifications', isOptional: true),
PrismStep(title: 'Done'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismStepper')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PrismStepper(
steps: _steps,
currentIndex: _index,
erroredIndices: _errored,
onStepTapped: (i) => setState(() => _index = i),
),
const SizedBox(height: 40),
const Text('Resize the window: the stepper collapses to a compact '
'"Step X of Y" header with a progress bar.'),
const Spacer(),
Row(
children: [
OutlinedButton(
onPressed: _index == 0
? null
: () => setState(() => _index--),
child: const Text('Back'),
),
const SizedBox(width: 12),
FilledButton(
onPressed: () {
setState(() {
if (_index == 1) {
_errored.add(1);
} else {
_errored.clear();
if (_index < _steps.length - 1) _index++;
}
});
},
child: const Text('Next'),
),
],
),
],
),
),
);
}
}
// ─── Data table ─────────────────────────────────────────────────────────────
class DataTableDemo extends StatelessWidget {
const DataTableDemo({super.key});
static final _invoices = [
_Invoice('INV-1042', 'Zenith Ltd', 1240, 'Paid'),
_Invoice('INV-1043', 'Aurora Media', 380, 'Overdue'),
_Invoice('INV-1044', 'Meridian Co', 915, 'Paid'),
_Invoice('INV-1045', 'Northwind', 2100, 'Draft'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismDataTable')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
PrismDataTable<_Invoice>(
rows: _invoices,
onRowTap: (row, i) => PrismToast.info(context, 'Opened ${row.id}'),
columns: [
PrismColumn<_Invoice>(
label: 'Invoice',
alwaysVisible: true,
cell: (c, r, i) => Text(r.id),
sortComparator: (a, b) => a.id.compareTo(b.id),
),
PrismColumn<_Invoice>(
label: 'Client',
cell: (c, r, i) => Text(r.client),
sortComparator: (a, b) => a.client.compareTo(b.client),
),
PrismColumn<_Invoice>(
label: 'Status',
cell: (c, r, i) => Chip(
label: Text(r.status),
visualDensity: VisualDensity.compact,
),
),
PrismColumn<_Invoice>(
label: 'Total',
numeric: true,
cell: (c, r, i) => Text('\$${r.total}'),
sortComparator: (a, b) => a.total.compareTo(b.total),
),
],
),
const SizedBox(height: 16),
const Text('Tap a header to sort. On a narrow window the same data '
'becomes labelled cards.'),
],
),
);
}
}
class _Invoice {
const _Invoice(this.id, this.client, this.total, this.status);
final String id;
final String client;
final int total;
final String status;
}
// ─── Command palette ────────────────────────────────────────────────────────
class CommandPaletteDemo extends StatelessWidget {
const CommandPaletteDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismCommandPalette')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard_command_key, size: 48),
const SizedBox(height: 16),
const Text('A ⌘K-style launcher with fuzzy search,\ngrouping and '
'full keyboard navigation.', textAlign: TextAlign.center),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () => _open(context),
icon: const Icon(Icons.search),
label: const Text('Open palette'),
),
],
),
),
);
}
void _open(BuildContext context) {
PrismCommandPalette.show(
context: context,
commands: [
PrismCommand(
label: 'New note',
subtitle: 'Create a blank note',
icon: Icons.note_add_outlined,
group: 'File',
shortcut: '⌘N',
keywords: const ['create', 'add'],
onSelect: () => PrismToast.success(context, 'New note'),
),
PrismCommand(
label: 'Search notes',
icon: Icons.search,
group: 'File',
shortcut: '⌘F',
onSelect: () => PrismToast.info(context, 'Search'),
),
PrismCommand(
label: 'Toggle dark mode',
icon: Icons.dark_mode_outlined,
group: 'View',
keywords: const ['theme', 'appearance'],
onSelect: () => PrismToast.info(context, 'Theme toggled'),
),
PrismCommand(
label: 'Export as markdown',
icon: Icons.download_outlined,
group: 'Share',
keywords: const ['save', 'download'],
onSelect: () => PrismToast.info(context, 'Exported'),
),
PrismCommand(
label: 'Sign out',
icon: Icons.logout,
group: 'Account',
onSelect: () => PrismToast.warning(context, 'Signed out'),
),
],
);
}
}
// ─── Adaptive sheet ─────────────────────────────────────────────────────────
class AdaptiveSheetDemo extends StatelessWidget {
const AdaptiveSheetDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismAdaptiveSheet')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'On a phone this slides up from the bottom.\n'
'On a wide window it becomes a centred dialog.\n\n'
'Resize the window and try again.',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
FilledButton(
onPressed: () => _open(context),
child: const Text('Show sheet'),
),
],
),
),
),
);
}
Future<void> _open(BuildContext context) async {
final confirmed = await PrismAdaptiveSheet.show<bool>(
context: context,
title: 'Delete this note?',
subtitle: 'The note will move to Trash for 30 days.',
child: const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'You can restore it any time from the Trash, or delete it '
'permanently right away.',
),
),
actions: [
const PrismSheetAction(label: 'Cancel', value: false),
PrismSheetAction(
label: 'Delete',
value: true,
isDestructive: true,
icon: Icons.delete_outline,
),
],
);
if (!context.mounted) return;
if (confirmed ?? false) {
PrismToast.success(context, 'Note deleted');
}
}
}
// ─── Connection banner ──────────────────────────────────────────────────────
class ConnectionDemo extends StatefulWidget {
const ConnectionDemo({super.key});
@override
State<ConnectionDemo> createState() => _ConnectionDemoState();
}
class _ConnectionDemoState extends State<ConnectionDemo> {
final _controller = StreamController<bool>.broadcast();
bool _online = true;
@override
void dispose() {
_controller.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PrismConnectionBanner')),
body: Column(
children: [
PrismConnectionBanner(
source: () => _controller.stream,
onlineMessage: 'Back online — syncing…',
retryLabel: 'Retry now',
onRetry: () => PrismToast.info(context, 'Retrying…'),
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_online ? 'Simulated: online' : 'Simulated: offline'),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
_online = !_online;
_controller.add(_online);
setState(() {});
},
child: Text(_online ? 'Go offline' : 'Go online'),
),
],
),
),
),
],
),
);
}
}
// ─── Design tokens ──────────────────────────────────────────────────────────
class TokensDemo extends StatelessWidget {
const TokensDemo({super.key});
@override
Widget build(BuildContext context) {
final tokens = context.prismTokens;
final colors = context.prismColors;
Widget swatch(String label, Color color, Color onColor) => Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(tokens.radiusMd),
),
child: Text(label, style: TextStyle(color: onColor, fontSize: 12)),
);
return Scaffold(
appBar: AppBar(title: const Text('Prism design tokens')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('Semantic colors', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final role in PrismSemantic.values)
swatch(
role.name,
colors.forRole(role).background,
colors.forRole(role).foreground,
),
],
),
const SizedBox(height: 24),
Text('Spacing scale', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
for (final (name, value) in [
('spaceXs', tokens.spaceXs),
('spaceSm', tokens.spaceSm),
('spaceMd', tokens.spaceMd),
('spaceLg', tokens.spaceLg),
('spaceXl', tokens.spaceXl),
('spaceXxl', tokens.spaceXxl),
])
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
SizedBox(width: 90, child: Text(name)),
Container(
height: 14,
width: value * 4,
color: colors.info,
),
const SizedBox(width: 8),
Text(value.toStringAsFixed(0)),
],
),
),
const SizedBox(height: 24),
Text('Corner radii', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
Wrap(
spacing: 12,
children: [
for (final (name, value) in [
('sm', tokens.radiusSm),
('md', tokens.radiusMd),
('lg', tokens.radiusLg),
('xl', tokens.radiusXl),
])
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: colors.surfaceSunken,
borderRadius: BorderRadius.circular(value),
border: Border.all(color: colors.outline),
),
alignment: Alignment.center,
child: Text('$name\n${value.toStringAsFixed(0)}',
textAlign: TextAlign.center),
),
],
),
const SizedBox(height: 24),
Text('Motion', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(
'fast ${tokens.durationFast.inMilliseconds} ms · '
'medium ${tokens.durationMedium.inMilliseconds} ms · '
'slow ${tokens.durationSlow.inMilliseconds} ms',
),
const SizedBox(height: 24),
Text(
'Override any of these in ThemeData.extensions: const '
'[PrismTokens(spaceMd: 20, radiusMd: 16)].',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}