Skip to main content

prism_kit 1.1.0 copy "prism_kit: ^1.1.0" to clipboard
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,
          ),
        ],
      ),
    );
  }
}
0
likes
0
points
1.19k
downloads

Publisher

unverified uploader

Weekly Downloads

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, settings, forms, pagination, markdown, a code viewer and an AI chat client — responsive on phone, desktop and web.

Repository (GitHub)
View/report issues

Topics

#widget #components #ui #adaptive #responsive

License

unknown (license)

Dependencies

flutter, http, markdown, material_color_utilities, meta, shared_preferences, url_launcher

More

Packages that depend on prism_kit