Skip to main content

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

A mobile-first Flutter implementation of Cloudflare's Kumo UI design system, built using pure widgets.dart.

example/lib/main.dart

// Everything Flutter-facing comes from kumo_ui: no `package:flutter/widgets.dart`
// import is needed. Only the Phosphor glyph constants come from elsewhere,
// because that is where the icon names are declared.
import 'package:go_router/go_router.dart';
import 'package:kumo_ui/kumo_ui.dart';
import 'package:phosphor_icons/phosphor_icons.dart';

void main() {
  // Refuses to boot on Flutter Web, by design.
  KumoTheme.ensureSupportedPlatform();
  runApp(const KumoExampleApp());
}

/// Root of the Kumo UI showcase.
///
/// Uses [KumoApp] so the entire example stays free of Material, and so the
/// theme, navigator and platform-brightness wiring live in one place.
class KumoExampleApp extends StatefulWidget {
  /// Creates the showcase app.
  const KumoExampleApp({super.key});

  @override
  State<KumoExampleApp> createState() => _KumoExampleAppState();
}

class _KumoExampleAppState extends State<KumoExampleApp> {
  KumoThemeMode _mode = KumoThemeMode.system;

  // Built once: a router holds the current location, so recreating it on every
  // rebuild would throw that state away. Note that the routes take no
  // arguments: a route builder runs once per location, not once per rebuild of
  // this State, so anything a page needs has to reach it through context.
  late final GoRouter _router = GoRouter(
    initialLocation: '/',
    routes: <RouteBase>[
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) => const KumoExampleHome(),
      ),
      GoRoute(
        path: '/settings',
        builder: (BuildContext context, GoRouterState state) => const KumoExampleSettings(),
      ),
    ],
  );

  void _setMode(KumoThemeMode mode) => setState(() => _mode = mode);

  @override
  Widget build(BuildContext context) {
    return KumoApp.router(
      title: 'Kumo UI',
      mode: _mode,
      debugShowCheckedModeBanner: false,
      routerConfig: _router,
      // Sits above the router's pages, so a routed screen can read and change
      // the mode. Handing it to a route builder instead would go stale.
      builder: (BuildContext context, Widget? child) => _ModeScope(mode: _mode, onChanged: _setMode, child: child!),
    );
  }
}

/// Scrollable screen exercising every component in the library.
class KumoExampleHome extends StatefulWidget {
  /// Creates the showcase screen.
  const KumoExampleHome({super.key});

  @override
  State<KumoExampleHome> createState() => _KumoExampleHomeState();
}

class _KumoExampleHomeState extends State<KumoExampleHome> {
  final TextEditingController _token = TextEditingController();

  String _segment = 'overview';
  String _plan = 'free';
  bool _notificationsEnabled = true;
  bool _cachingEnabled = true;
  bool _inlineToastVisible = true;
  int _tabIndex = 0;
  int _page = 2;
  String? _lastSheetAction;

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

  void _reset() {
    _token.clear();
    setState(() {
      _segment = 'overview';
      _plan = 'free';
      _notificationsEnabled = false;
      _cachingEnabled = false;
      _inlineToastVisible = true;
      _tabIndex = 0;
      _page = 2;
      _lastSheetAction = null;
    });
  }

  void _showToast(KumoToastKind kind) {
    KumoToastManager.show(
      context,
      title: switch (kind) {
        KumoToastKind.info => 'Sync queued',
        KumoToastKind.success => 'Worker deployed',
        KumoToastKind.warning => 'Quota nearly spent',
        KumoToastKind.error => 'Deploy failed',
      },
      message: 'kumo-worker is live on 3 routes.',
      kind: kind,
    );
    // Rebuild so the active count below reflects the new stack.
    setState(() {});
  }

  void _clearToasts() {
    KumoToastManager.clear();
    setState(() {});
  }

  Future<void> _openSheet() async {
    final String? action = await KumoBottomSheet.show<String>(
      context: context,
      title: 'Zone actions',
      child: Builder(
        builder: (BuildContext sheetContext) => Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            for (final String label in <String>['Rename', 'Duplicate', 'Delete'])
              KumoBottomSheetItem(label: label, onTap: () => Navigator.of(sheetContext).pop(label)),
            const SizedBox(height: 8),
          ],
        ),
      ),
    );
    if (action != null && mounted) {
      setState(() => _lastSheetAction = action);
    }
  }

  void _openDialog() {
    KumoModal.show<void>(
      context: context,
      title: 'Deploy worker',
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'This dialog is drawn from a RawDialogRoute, so no Material '
            'scaffolding is involved.',
            style: KumoTheme.textStylesOf(context).bodyMuted,
          ),
          const SizedBox(height: 16),
          const KumoCodeBlock(code: 'npx wrangler deploy', language: 'bash'),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final colors = KumoTheme.of(context);
    final styles = KumoTheme.textStylesOf(context);
    final _ModeScope modeScope = _ModeScope.of(context);
    final bool isLight = colors.brightness == Brightness.light;

    final edgeRules = KumoListGroup(
      title: 'Edge rules',
      children: [
        KumoListItem(
          title: 'Always use HTTPS',
          subtitle: 'On',
          leading: const PhosphorIcon(PhosphorIconsRegular.shieldCheck, size: 18),
          onTap: () {},
        ),
        const KumoListItem(title: 'Automatic HTTPS rewrites', subtitle: 'On'),
      ],
    );

    final cachingRules = KumoListGroup(
      title: 'Caching rules',
      children: [
        KumoListItem(
          title: 'Cache level',
          subtitle: 'Standard',
          leading: const PhosphorIcon(PhosphorIconsRegular.lightning, size: 18),
          onTap: () {},
        ),
        const KumoListItem(title: 'Browser cache TTL', subtitle: '4 hours'),
      ],
    );

    // The raw scale is per-scheme, so the swatches follow the active one.
    final List<(String, Color)> accents = isLight
        ? <(String, Color)>[
            ('orange5', KumoLightPalette.orange5),
            ('blue5', KumoLightPalette.blue5),
            ('green5', KumoLightPalette.green5),
            ('amber5', KumoLightPalette.amber5),
            ('red5', KumoLightPalette.red5),
          ]
        : <(String, Color)>[
            ('orange5', KumoPalette.orange5),
            ('blue5', KumoPalette.blue5),
            ('green5', KumoPalette.green5),
            ('amber5', KumoPalette.amber5),
            ('red5', KumoPalette.red5),
          ];

    return ColoredBox(
      color: colors.canvas,
      child: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              KumoHeader(
                title: 'Kumo UI',
                subtitle: 'A widgets-only take on the Cloudflare dashboard.',
                action: KumoButton(
                  label: 'Reset fields',
                  variant: KumoButtonVariant.secondary,
                  icon: PhosphorIconsRegular.arrowCounterClockwise,
                  onPressed: _reset,
                ),
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Theme'),
              KumoSegmentedControl<KumoThemeMode>(
                segments: const <KumoThemeMode, String>{
                  KumoThemeMode.system: 'System',
                  KumoThemeMode.light: 'Light',
                  KumoThemeMode.dark: 'Dark',
                },
                selected: modeScope.mode,
                onSelected: modeScope.onChanged,
              ),
              const SizedBox(height: 10),
              Text('Painting the ${isLight ? 'light' : 'dark'} scheme.', style: styles.caption),
              const SizedBox(height: 28),
              const _SectionLabel('Navigation'),
              KumoButton(
                label: 'Open settings',
                variant: KumoButtonVariant.secondary,
                icon: PhosphorIconsRegular.arrowRight,
                onPressed: () => context.push('/settings'),
              ),
              const SizedBox(height: 10),
              Text('Pushed onto the go_router stack.', style: styles.caption),
              const SizedBox(height: 28),
              const _SectionLabel('Segmented control'),
              KumoSegmentedControl<String>(
                segments: const <String, String>{'overview': 'Overview', 'dns': 'DNS', 'workers': 'Workers'},
                selected: _segment,
                onSelected: (value) => setState(() => _segment = value),
              ),
              const SizedBox(height: 10),
              Text('Showing the $_segment view.', style: styles.caption),
              const SizedBox(height: 28),
              const _SectionLabel('Text input'),
              KumoInput(
                label: 'Api token',
                placeholder: 'Paste token',
                controller: _token,
                prefixIcon: PhosphorIcon(PhosphorIconsRegular.key, size: 16, color: colors.textSecondary),
                onChanged: (_) => setState(() {}),
              ),
              const SizedBox(height: 12),
              KumoInput(
                label: 'Account id',
                placeholder: '32 character hex id',
                obscureText: true,
                errorMessage: _token.text.isEmpty ? 'Required to deploy' : null,
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Toggle'),
              Row(
                children: [
                  Expanded(child: Text('Notifications', style: styles.body)),
                  KumoSwitch(
                    value: _notificationsEnabled,
                    onChanged: (value) => setState(() => _notificationsEnabled = value),
                  ),
                ],
              ),
              const SizedBox(height: 12),
              Row(
                children: [
                  Expanded(child: Text('Analytics', style: styles.body)),
                  const KumoSwitch(value: true, onChanged: _noop, isDisabled: true),
                ],
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Accordion'),
              const KumoAccordion(
                title: 'Advanced settings',
                child: Text(
                  'Changes here apply to every request routed through the '
                  'selected zone.',
                ),
              ),
              const SizedBox(height: 28),
              const _SectionLabel('List group'),
              KumoListGroup(
                title: 'Zone settings',
                children: [
                  const KumoListItem(title: 'DNS records', subtitle: '42 records'),
                  const KumoListItem(title: 'Caching', subtitle: 'Standard'),
                  KumoListItem(
                    title: 'Add a record',
                    leading: const PhosphorIcon(PhosphorIconsRegular.plus, size: 18),
                    onTap: () {},
                  ),
                ],
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Resource grid'),
              KumoDataGrid(
                maxColumns: 3,
                children: [
                  const KumoDataCard(
                    title: 'example.com',
                    subtitle: 'Zone · Pro plan',
                    statusLabel: 'Active',
                    details: [
                      KumoDataPair(label: 'Nameservers', value: 'ada.ns.cloudflare.com'),
                      KumoDataPair(label: 'Records', value: '42'),
                    ],
                  ),
                  KumoDataCard(
                    title: 'Global DNS',
                    subtitle: 'Zone · Free plan',
                    statusLabel: 'Pending',
                    // An explicit status chip color, instead of the default.
                    statusColor: colors.warning,
                    initiallyExpanded: true,
                    details: const [KumoDataPair(label: 'Nameservers', value: 'bob.ns.cloudflare.com')],
                  ),
                  const KumoDataCard(
                    title: 'workers.dev',
                    subtitle: 'Worker · Bundled',
                    statusLabel: 'Active',
                    details: [
                      KumoDataPair(label: 'Routes', value: '3'),
                      KumoDataPair(label: 'Requests', value: '1.2M'),
                    ],
                  ),
                ],
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Responsive split'),
              KumoResponsiveLayout(
                breakpoint: kKumoBreakpoint,
                mobile: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [edgeRules, const SizedBox(height: 12), cachingRules],
                ),
                desktop: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Expanded(child: edgeRules),
                    const SizedBox(width: 16),
                    Expanded(child: cachingRules),
                  ],
                ),
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Breadcrumb'),
              KumoBreadcrumb(
                items: <KumoBreadcrumbItem>[
                  KumoBreadcrumbItem(label: 'Accounts', onTap: () => setState(() => _tabIndex = 0)),
                  KumoBreadcrumbItem(label: 'example.com', onTap: () => setState(() => _tabIndex = 1)),
                  const KumoBreadcrumbItem(label: 'DNS'),
                ],
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Tabs'),
              KumoTabs(
                tabs: const <String>['General', 'Traffic', 'Security'],
                selectedIndex: _tabIndex,
                onTabChanged: (int index) => setState(() => _tabIndex = index),
              ),
              const SizedBox(height: 12),
              Text('Tab index $_tabIndex is selected.', style: styles.caption),
              const SizedBox(height: 28),
              const _SectionLabel('Badges'),
              const Wrap(
                spacing: 8,
                runSpacing: 8,
                children: <Widget>[
                  KumoBadge(label: 'Pro plan', variant: KumoBadgeVariant.info),
                  KumoBadge(label: 'Active', variant: KumoBadgeVariant.success, icon: PhosphorIconsRegular.checkCircle),
                  KumoBadge(label: 'Proxied', variant: KumoBadgeVariant.warning),
                  KumoBadge(
                    label: 'Errored',
                    variant: KumoBadgeVariant.error,
                    icon: PhosphorIconsRegular.warningCircle,
                  ),
                  KumoBadge(label: 'Draft'),
                ],
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Checkbox and select'),
              KumoCheckbox(
                value: _cachingEnabled,
                label: 'Enable caching',
                onChanged: (bool value) => setState(() => _cachingEnabled = value),
              ),
              const SizedBox(height: 12),
              const KumoCheckbox(value: true, label: 'Always online', onChanged: _noop, isDisabled: true),
              const SizedBox(height: 12),
              KumoSelect<String>(
                value: _plan,
                label: 'Plan',
                options: const <String, String>{'free': 'Free', 'pro': 'Pro', 'business': 'Business'},
                onChanged: (String value) => setState(() => _plan = value),
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Pagination'),
              KumoPagination(
                currentPage: _page,
                totalPages: 5,
                onPageChanged: (int page) => setState(() => _page = page),
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Bottom sheet'),
              KumoButton(
                label: 'Zone actions',
                variant: KumoButtonVariant.secondary,
                icon: PhosphorIconsRegular.caretUp,
                onPressed: _openSheet,
              ),
              if (_lastSheetAction != null) ...[
                const SizedBox(height: 10),
                Text('Chose $_lastSheetAction.', style: styles.caption),
              ],
              const SizedBox(height: 28),
              const _SectionLabel('Toast'),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: <Widget>[
                  KumoButton(
                    label: 'Show success toast',
                    icon: PhosphorIconsRegular.checkCircle,
                    onPressed: () => _showToast(KumoToastKind.success),
                  ),
                  KumoButton(
                    label: 'Info',
                    variant: KumoButtonVariant.secondary,
                    onPressed: () => _showToast(KumoToastKind.info),
                  ),
                  KumoButton(
                    label: 'Warning',
                    variant: KumoButtonVariant.secondary,
                    onPressed: () => _showToast(KumoToastKind.warning),
                  ),
                  KumoButton(
                    label: 'Error',
                    variant: KumoButtonVariant.secondary,
                    onPressed: () => _showToast(KumoToastKind.error),
                  ),
                  KumoButton(label: 'Clear toasts', variant: KumoButtonVariant.secondary, onPressed: _clearToasts),
                ],
              ),
              const SizedBox(height: 10),
              Text('Active toasts: ${KumoToastManager.activeCount}', style: styles.caption),
              const SizedBox(height: 12),
              // The banner itself, rendered in place rather than queued.
              if (_inlineToastVisible)
                KumoToast(
                  title: 'Inline banner',
                  message: 'Rendered directly, not pushed to the overlay.',
                  kind: KumoToastKind.info,
                  onClose: () => setState(() => _inlineToastVisible = false),
                )
              else
                KumoButton(
                  label: 'Restore inline banner',
                  variant: KumoButtonVariant.secondary,
                  onPressed: () => setState(() => _inlineToastVisible = true),
                ),
              const SizedBox(height: 28),
              const _SectionLabel('Code block'),
              const KumoCodeBlock(code: 'npx wrangler deploy --env production', language: 'bash'),
              const SizedBox(height: 12),
              const KumoCodeBlock(code: 'console.log("copy me")'),
              const SizedBox(height: 28),
              const _SectionLabel('Palette'),
              Wrap(
                spacing: 12,
                runSpacing: 12,
                children: <Widget>[
                  for (final (String name, Color swatch) in accents)
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: <Widget>[
                        Container(
                          width: 20,
                          height: 20,
                          decoration: BoxDecoration(
                            color: swatch,
                            borderRadius: BorderRadius.circular(4),
                            border: Border.all(color: colors.border),
                          ),
                        ),
                        const SizedBox(width: 6),
                        Text(name, style: styles.caption),
                      ],
                    ),
                ],
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Primitives'),
              const KumoLabel('Field label'),
              const SizedBox(height: 12),
              KumoLink(
                label: 'View the deployment logs',
                icon: PhosphorIconsRegular.arrowUpRight,
                onPressed: () {},
              ),
              const SizedBox(height: 12),
              const KumoSkeleton(lines: 2),
              const SizedBox(height: 12),
              const KumoMeter(label: 'Cache hit rate', value: 0.72),
              const SizedBox(height: 12),
              const KumoEmpty(
                title: 'No logs yet',
                message: 'Requests appear here once the Worker takes traffic.',
                icon: PhosphorIconsRegular.tray,
              ),
              const SizedBox(height: 12),
              KumoSensitiveInput(label: 'Api token', placeholder: 'Paste a token'),
              const SizedBox(height: 12),
              KumoRadio<String>(
                value: 'standard',
                groupValue: 'standard',
                label: 'Standard caching',
                onChanged: (String _) {},
              ),
              const SizedBox(height: 12),
              KumoTooltip(
                message: 'Publishes the Worker to every route',
                child: KumoButton(
                  label: 'Deploy',
                  variant: KumoButtonVariant.secondary,
                  onPressed: () {},
                ),
              ),
              const SizedBox(height: 12),
              KumoBanner(
                kind: KumoBannerKind.warning,
                title: 'Quota nearly spent',
                message: 'This zone has 8% of its monthly requests left.',
                onDismiss: () {},
              ),
              const SizedBox(height: 28),
              const _SectionLabel('Modal'),
              KumoButton(label: 'Open dialog', icon: PhosphorIconsRegular.arrowSquareOut, onPressed: _openDialog),
              const SizedBox(height: 40),
            ],
          ),
        ),
      ),
    );
  }

  static void _noop(bool _) {}
}

/// The second route, reached by pushing '/settings' rather than a Navigator 1.0
/// route, which is what makes this a Navigator 2.0 app.
class KumoExampleSettings extends StatelessWidget {
  /// Creates the settings screen.
  const KumoExampleSettings({super.key});

  @override
  Widget build(BuildContext context) {
    final styles = KumoTheme.textStylesOf(context);

    return KumoScaffold(
      header: KumoHeader(
        title: 'Settings',
        subtitle: 'Routed at /settings.',
        leading: KumoButton(
          label: 'Back',
          variant: KumoButtonVariant.secondary,
          icon: PhosphorIconsRegular.arrowLeft,
          // A deep link can land here with nothing to pop, so fall back
          // to the gallery instead of throwing.
          onPressed: () => context.canPop() ? context.pop() : context.go('/'),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          const _SectionLabel('Notifications'),
          const KumoListGroup(
            title: 'Deploy alerts',
            children: <Widget>[
              KumoListItem(title: 'Deploy finished', subtitle: 'Push and email'),
              KumoListItem(title: 'Weekly digest', subtitle: 'Mondays, 09:00'),
            ],
          ),
          const SizedBox(height: 28),
          const _SectionLabel('Route'),
          Text('This screen is a page of the same RouterConfig.', style: styles.bodyMuted),
          const SizedBox(height: 12),
          const KumoCodeBlock(code: "context.push('/settings')", language: 'dart'),
        ],
      ),
    );
  }
}

/// Carries the app's theme mode to routed pages.
///
/// The app owns the mode, but the pages are built by the router, so they cannot
/// receive it as a constructor argument — a route builder runs once per
/// location. Passing it down through context is what keeps the selector in sync
/// after the mode changes.
class _ModeScope extends InheritedWidget {
  const _ModeScope({required this.mode, required this.onChanged, required super.child});

  /// The mode the app is currently painting.
  final KumoThemeMode mode;

  /// Called when a screen reports a new mode.
  final ValueChanged<KumoThemeMode> onChanged;

  static _ModeScope of(BuildContext context) {
    final _ModeScope? scope = context.dependOnInheritedWidgetOfExactType<_ModeScope>();
    assert(scope != null, 'No _ModeScope above this widget.');
    return scope!;
  }

  @override
  bool updateShouldNotify(_ModeScope oldWidget) => mode != oldWidget.mode || onChanged != oldWidget.onChanged;
}

class _SectionLabel extends StatelessWidget {
  const _SectionLabel(this.label);

  final String label;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Text(label, style: KumoTheme.textStylesOf(context).h2),
    );
  }
}
0
likes
0
points
344
downloads

Publisher

verified publisherhafizhfadh.id

Weekly Downloads

A mobile-first Flutter implementation of Cloudflare's Kumo UI design system, built using pure widgets.dart.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, phosphor_icons

More

Packages that depend on kumo_ui