Skip to main content

prism_kit 1.5.0 copy "prism_kit: ^1.5.0" to clipboard
prism_kit: ^1.5.0 copied to clipboard

One dependency, 51 adaptive Flutter components on a shared design system: data tables, calendar, tabs, toasts, forms, controls, loading and empty states, markdown, an AI chat client, and more.

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());

/// The gallery shell.
///
/// It exercises the parts of Prism that are easiest to get wrong when you are
/// the one building the library: theming, a live token override, and the
/// adaptive shell doing real work on a real screen.
class PrismGalleryApp extends StatefulWidget {
  const PrismGalleryApp({super.key});

  @override
  State<PrismGalleryApp> createState() => _PrismGalleryAppState();
}

class _PrismGalleryAppState extends State<PrismGalleryApp> {
  ThemeMode _mode = ThemeMode.system;

  /// When true, the app is themed with deliberately unusual tokens so you can
  /// see every component adopt them at once. This is the design system doing
  /// its job, not a mock-up.
  bool _compactSpacing = false;

  void _toggleMode() {
    setState(() {
      _mode = switch (_mode) {
        ThemeMode.system => ThemeMode.light,
        ThemeMode.light => ThemeMode.dark,
        ThemeMode.dark => ThemeMode.system,
      };
    });
  }

  void _toggleTokens() => setState(() => _compactSpacing = !_compactSpacing);

  @override
  Widget build(BuildContext context) {
    const seed = Color(0xFF39A9DB);

    // One token set, installed in both themes. Flip the switch in the app bar
    // and watch every component change rhythm at once.
    final tokens = _compactSpacing
        ? const PrismTokens(
            spaceMd: 10,
            spaceLg: 14,
            radiusSm: 2,
            radiusMd: 3,
            radiusLg: 4,
            radiusXl: 6,
          )
        : PrismTokens.fallback;

    return MaterialApp(
      title: 'Prism gallery',
      debugShowCheckedModeBanner: false,
      themeMode: _mode,
      theme: ThemeData(colorSchemeSeed: seed, extensions: [tokens]),
      darkTheme: ThemeData(
        colorSchemeSeed: seed,
        brightness: Brightness.dark,
        extensions: [tokens],
      ),
      home: GalleryHome(
        themeMode: _mode,
        onToggleTheme: _toggleMode,
        compactTokens: _compactSpacing,
        onToggleTokens: _toggleTokens,
      ),
    );
  }
}

/// A searchable grid of every component demo.
class GalleryHome extends StatefulWidget {
  const GalleryHome({
    super.key,
    required this.themeMode,
    required this.onToggleTheme,
    required this.compactTokens,
    required this.onToggleTokens,
  });

  /// Current theme mode, shown on the toggle button.
  final ThemeMode themeMode;

  /// Cycles system → light → dark.
  final VoidCallback onToggleTheme;

  /// Whether the unusual token set is active.
  final bool compactTokens;

  /// Installs or removes the unusual token set.
  final VoidCallback onToggleTokens;

  @override
  State<GalleryHome> createState() => _GalleryHomeState();
}

class _GalleryHomeState extends State<GalleryHome> {
  final _controller = TextEditingController();

  /// Every demo, in the order they appear in the grid.
  ///
  /// `keywords` are extra search terms, so "table" finds the data table and
  /// "pin" finds the OTP field.
  static const _demos = <_Demo>[
    _Demo('Adaptive scaffold', Icons.smartphone, [
      'nav',
      'rail',
      'responsive',
    ], _scaffold),
    _Demo('Toast', Icons.notifications_none, [
      'snackbar',
      'notification',
    ], _toast),
    _Demo('Banner', Icons.campaign_outlined, [
      'announcement',
      'dismiss',
    ], _banner),
    _Demo('Search', Icons.search, ['filter', 'query', 'fuzzy'], _search),
    _Demo('Settings', Icons.settings_outlined, [
      'preferences',
      'tiles',
    ], _settings),
    _Demo('Markdown', Icons.article_outlined, [
      'gfm',
      'commonmark',
      'text',
    ], _markdown),
    _Demo('Code view', Icons.code, ['syntax', 'highlight', 'copy'], _code),
    _Demo('Form', Icons.edit_note, ['validation', 'fields', 'submit'], _form),
    _Demo('Pagination', Icons.menu, [
      'infinite',
      'scroll',
      'list',
    ], _pagination),
    _Demo('Master-detail', Icons.view_agenda_outlined, [
      'split',
      'panes',
    ], _masterDetail),
    _Demo('Adaptive dialog', Icons.open_in_full, ['modal', 'confirm'], _dialog),
    _Demo('State views', Icons.extension_outlined, [
      'loading',
      'empty',
      'error',
    ], _stateViews),
    _Demo('Palette', Icons.palette_outlined, [
      'colour',
      'extract',
      'theme',
    ], _palette),
    _Demo('Changelog', Icons.history, [
      'releases',
      'version',
      'whats new',
    ], _changelog),
    _Demo('AI chat', Icons.smart_toy_outlined, [
      'llm',
      'openai',
      'ollama',
      'streaming',
    ], _ai),
    _Demo('Skeleton', Icons.blur_on, [
      'loading',
      'shimmer',
      'placeholder',
    ], _skeleton),
    _Demo('Avatar', Icons.account_circle_outlined, [
      'initials',
      'profile',
      'presence',
    ], _avatar),
    _Demo('Chip input', Icons.label_outline, [
      'tags',
      'tokens',
      'labels',
    ], _chipInput),
    _Demo('OTP field', Icons.password, ['pin', 'code', 'verification'], _otp),
    _Demo('Timeline', Icons.timeline, [
      'activity',
      'steps',
      'history',
    ], _timeline),
    _Demo('Stepper', Icons.linear_scale, [
      'wizard',
      'progress',
      'steps',
    ], _stepper),
    _Demo('Data table', Icons.table_chart_outlined, [
      'grid',
      'sort',
      'rows',
      'columns',
    ], _dataTable),
    _Demo('Command palette', Icons.keyboard_command_key, [
      'launcher',
      'shortcut',
      'cmd k',
    ], _commandPalette),
    _Demo('Adaptive sheet', Icons.vertical_align_bottom, [
      'bottom sheet',
      'modal',
    ], _adaptiveSheet),
    _Demo('Connection banner', Icons.cloud_off_outlined, [
      'offline',
      'network',
    ], _connection),
    _Demo('Design tokens', Icons.palette, [
      'spacing',
      'radius',
      'motion',
    ], _tokens),
  ];

  static Widget _scaffold() => const ScaffoldDemo();
  static Widget _toast() => const ToastDemo();
  static Widget _banner() => const BannerDemo();
  static Widget _search() => const SearchDemo();
  static Widget _settings() => const SettingsDemo();
  static Widget _markdown() => const MarkdownDemo();
  static Widget _code() => const CodeDemo();
  static Widget _form() => const FormDemo();
  static Widget _pagination() => const PaginationDemo();
  static Widget _masterDetail() => const MasterDetailDemo();
  static Widget _dialog() => const DialogDemo();
  static Widget _stateViews() => const StateViewsDemo();
  static Widget _palette() => const PaletteDemo();
  static Widget _changelog() => const ChangelogDemo();
  static Widget _ai() => const AiDemo();
  static Widget _skeleton() => const SkeletonDemo();
  static Widget _avatar() => const AvatarDemo();
  static Widget _chipInput() => const ChipInputDemo();
  static Widget _otp() => const OtpDemo();
  static Widget _timeline() => const TimelineDemo();
  static Widget _stepper() => const StepperDemo();
  static Widget _dataTable() => const DataTableDemo();
  static Widget _commandPalette() => const CommandPaletteDemo();
  static Widget _adaptiveSheet() => const AdaptiveSheetDemo();
  static Widget _connection() => const ConnectionDemo();
  static Widget _tokens() => const TokensDemo();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  List<_Demo> get _visible {
    final query = _controller.text.trim().toLowerCase();
    if (query.isEmpty) return _demos;
    return _demos.where((d) => d.matches(query)).toList();
  }

  @override
  Widget build(BuildContext context) {
    final tokens = context.prismTokens;
    final demos = _visible;
    final columns = switch (MediaQuery.sizeOf(context).width) {
      < 520 => 2,
      < 840 => 3,
      _ => 4,
    };

    return Scaffold(
      appBar: AppBar(
        title: const Text('Prism gallery'),
        actions: [
          IconButton(
            tooltip: widget.compactTokens
                ? 'Restore default tokens'
                : 'Apply a distinctly non-default token set',
            icon: Icon(widget.compactTokens ? Icons.tune : Icons.tune_outlined),
            onPressed: widget.onToggleTokens,
          ),
          IconButton(
            tooltip: 'Theme: ${widget.themeMode.name}',
            icon: Icon(switch (widget.themeMode) {
              ThemeMode.system => Icons.brightness_auto_outlined,
              ThemeMode.light => Icons.light_mode_outlined,
              ThemeMode.dark => Icons.dark_mode_outlined,
            }),
            onPressed: widget.onToggleTheme,
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.fromLTRB(
              tokens.spaceMd,
              tokens.spaceMd,
              tokens.spaceMd,
              tokens.spaceSm,
            ),
            // PrismSearch, used by Prism on itself.
            child: PrismSearch<_Demo>(
              items: _demos,
              itemLabel: (d) => d.name,
              hintText: 'Find a component…',
              onSelected: (demo) => _open(demo.name),
            ),
          ),
          Expanded(
            child: demos.isEmpty
                ? const PrismStateView<List<_Demo>>(
                    state: PrismViewEmpty(),
                    emptyMessage: 'No component matches that search.',
                    successBuilder: _never,
                  )
                : GridView.builder(
                    padding: EdgeInsets.all(tokens.spaceMd),
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: columns,
                      mainAxisSpacing: tokens.spaceSm,
                      crossAxisSpacing: tokens.spaceSm,
                    ),
                    itemCount: demos.length,
                    itemBuilder: (context, i) {
                      final demo = demos[i];
                      return Card(
                        margin: EdgeInsets.zero,
                        child: InkWell(
                          onTap: () => _open(demo.name),
                          child: Padding(
                            padding: EdgeInsets.all(tokens.spaceSm),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                Icon(demo.icon, size: tokens.spaceXl),
                                SizedBox(height: tokens.spaceSm),
                                Text(
                                  demo.name,
                                  textAlign: TextAlign.center,
                                  maxLines: 2,
                                  overflow: TextOverflow.ellipsis,
                                ),
                              ],
                            ),
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }

  static Widget _never(List<_Demo> _) => const SizedBox.shrink();

  void _open(String name) {
    final demo = _demos.firstWhere((d) => d.name == name);
    Navigator.push(
      context,
      MaterialPageRoute<void>(builder: (_) => demo.build()),
    );
  }
}

/// One entry in the gallery grid.
@immutable
class _Demo {
  const _Demo(this.name, this.icon, this.keywords, this.build);

  final String name;
  final IconData icon;
  final List<String> keywords;
  final Widget Function() build;

  bool matches(String query) =>
      name.toLowerCase().contains(query) ||
      keywords.any((k) => k.contains(query));
}

// ─── 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

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

One dependency, 51 adaptive Flutter components on a shared design system: data tables, calendar, tabs, toasts, forms, controls, loading and empty states, markdown, an AI chat client, and more.

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