happenings_ui 0.2.3 copy "happenings_ui: ^0.2.3" to clipboard
happenings_ui: ^0.2.3 copied to clipboard

Happenings design system — tokens, theme, and shared components for Flutter apps built on Material 3 with a Zinc color palette.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:happenings_ui/happenings_ui.dart';

void main() => runApp(const ExampleApp());

/// Example app that demonstrates how to use happenings_ui with a custom icon
/// library (Font Awesome). The [HappeningsUiConfig] at the root wires up a
/// custom [iconBuilder] and [icons] — every component in the tree automatically
/// picks up the Font Awesome icons instead of Material Icons.
class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return HappeningsUiConfig(
      // Swap the icon renderer to Font Awesome.
      iconBuilder: (IconData icon, {double? size, Color? color}) =>
          FaIcon(icon, size: size, color: color),
      // Map semantic icon slots to Font Awesome glyphs.
      icons: const HappeningsIcons(
        back: FontAwesomeIcons.chevronLeft,
        close: FontAwesomeIcons.xmark,
        check: FontAwesomeIcons.check,
        checkCircle: FontAwesomeIcons.solidCircleCheck,
        chevronDown: FontAwesomeIcons.chevronDown,
        chevronRight: FontAwesomeIcons.chevronRight,
        user: FontAwesomeIcons.user,
        image: FontAwesomeIcons.image,
        refresh: FontAwesomeIcons.arrowsRotate,
        minus: FontAwesomeIcons.minus,
        info: FontAwesomeIcons.circleInfo,
        warning: FontAwesomeIcons.triangleExclamation,
        error: FontAwesomeIcons.lock,
        exclamation: FontAwesomeIcons.circleExclamation,
      ),
      child: MaterialApp(
        title: 'happenings_ui',
        debugShowCheckedModeBanner: false,
        theme: AppTheme.light(),
        darkTheme: AppTheme.dark(),
        home: const _GalleryShell(),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Shell — bottom nav with gallery pages
// ---------------------------------------------------------------------------

class _GalleryShell extends StatefulWidget {
  const _GalleryShell();

  @override
  State<_GalleryShell> createState() => _GalleryShellState();
}

class _GalleryShellState extends State<_GalleryShell> {
  int _tab = 0;

  static const List<Widget> _pages = <Widget>[
    _ComponentsPage(),
    _FormsPage(),
    _FeedbackPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(index: _tab, children: _pages),
      bottomNavigationBar: HappeningsBottomNavBar(
        currentIndex: _tab,
        onTap: (int i) => setState(() => _tab = i),
        items: const <HappeningsNavItem>[
          HappeningsNavItem(
            icon: FontAwesomeIcons.shapes,
            activeIcon: FontAwesomeIcons.shapes,
            label: 'Components',
          ),
          HappeningsNavItem(
            icon: FontAwesomeIcons.penToSquare,
            activeIcon: FontAwesomeIcons.solidPenToSquare,
            label: 'Forms',
          ),
          HappeningsNavItem(
            icon: FontAwesomeIcons.bell,
            activeIcon: FontAwesomeIcons.solidBell,
            label: 'Feedback',
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Components page — buttons, cards, avatars, badges, shimmer, typography
// ---------------------------------------------------------------------------

class _ComponentsPage extends StatelessWidget {
  const _ComponentsPage();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(
        HapSpacing.lg,
        60,
        HapSpacing.lg,
        HapSpacing.xxxl,
      ),
      children: <Widget>[
        const H1('Components'),
        const SizedBox(height: HapSpacing.xxl),

        // -- Typography --
        const _SectionLabel('Typography'),
        const SizedBox(height: HapSpacing.sm),
        const H1('H1 — Page title'),
        const SizedBox(height: HapSpacing.xs),
        const H2('H2 — Section heading'),
        const SizedBox(height: HapSpacing.xs),
        const H3('H3 — Card title'),
        const SizedBox(height: HapSpacing.xs),
        const Body('Body — Default body text'),
        const SizedBox(height: HapSpacing.xs),
        const BodyMedium('BodyMedium — Emphasized body'),
        const SizedBox(height: HapSpacing.xs),
        const Caption('Caption — Small label'),
        const SizedBox(height: HapSpacing.xs),
        const Overline('OVERLINE — TAG'),
        const SizedBox(height: HapSpacing.xxl),

        // -- Buttons --
        const _SectionLabel('Buttons'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Primary button',
          onPressed: () {},
        ),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Secondary button',
          variant: HappeningsButtonVariant.secondary,
          onPressed: () {},
        ),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Ghost button',
          variant: HappeningsButtonVariant.ghost,
          onPressed: () {},
        ),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'With icon',
          icon: FontAwesomeIcons.plus,
          onPressed: () {},
        ),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsButton(label: 'Disabled'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Loading...',
          isLoading: true,
          onPressed: () {},
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Cards --
        const _SectionLabel('Card'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsCard(
          onTap: () {},
          child: const Row(
            children: <Widget>[
              HappeningsAvatar(name: 'Ada Lovelace', size: 44),
              SizedBox(width: HapSpacing.md),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    H3('Ada Lovelace'),
                    SizedBox(height: 2),
                    Caption('Tap this card — feel the scale-down'),
                  ],
                ),
              ),
            ],
          ),
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Avatars --
        const _SectionLabel('Avatars'),
        const SizedBox(height: HapSpacing.sm),
        const Row(
          children: <Widget>[
            HappeningsAvatar(name: 'Rasmus Jensing', size: 48),
            SizedBox(width: HapSpacing.sm),
            HappeningsAvatar(name: 'Ada Lovelace', size: 48),
            SizedBox(width: HapSpacing.sm),
            HappeningsAvatar(size: 48),
            SizedBox(width: HapSpacing.sm),
            HappeningsAvatar(name: 'K', size: 36),
            SizedBox(width: HapSpacing.sm),
            HappeningsAvatar(name: 'Small', size: 28),
          ],
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Badges --
        const _SectionLabel('Badges'),
        const SizedBox(height: HapSpacing.sm),
        Wrap(
          spacing: HapSpacing.sm,
          runSpacing: HapSpacing.sm,
          children: <Widget>[
            const HappeningsBadge(label: 'Default'),
            HappeningsBadge(
              label: 'Success',
              backgroundColor: BrandColors.green.withValues(alpha: 0.15),
              textColor: BrandColors.green700,
            ),
            HappeningsBadge(
              label: 'Info',
              backgroundColor: BrandColors.blue.withValues(alpha: 0.15),
              textColor: BrandColors.blue700,
            ),
            HappeningsBadge(
              label: 'Warning',
              backgroundColor: BrandColors.amber500.withValues(alpha: 0.15),
              textColor: BrandColors.amber700,
            ),
          ],
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Shimmer --
        const _SectionLabel('Shimmer loading'),
        const SizedBox(height: HapSpacing.sm),
        const Row(
          children: <Widget>[
            HappeningsShimmer(width: 48, height: 48, borderRadius: 999),
            SizedBox(width: HapSpacing.md),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  HappeningsShimmer(height: 14, borderRadius: 4),
                  SizedBox(height: HapSpacing.sm),
                  HappeningsShimmer(height: 14, borderRadius: 4),
                ],
              ),
            ),
          ],
        ),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsShimmer(height: 120),
        const SizedBox(height: HapSpacing.xxl),

        // -- Empty state --
        const _SectionLabel('Empty state'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsCard(
          child: HappeningsEmptyState(
            icon: FontAwesomeIcons.magnifyingGlass,
            title: 'No results',
            subtitle: 'Try a different search term.',
            actionLabel: 'Clear search',
            onAction: () {},
          ),
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Swipe to confirm --
        const _SectionLabel('Swipe to confirm'),
        const SizedBox(height: HapSpacing.sm),
        SwipeToConfirmButton.light(
          label: 'Swipe to confirm',
          onConfirmed: () async {
            await Future<void>.delayed(const Duration(seconds: 1));
            return true;
          },
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Forms page — inputs, textarea, select, OTP
// ---------------------------------------------------------------------------

class _FormsPage extends StatefulWidget {
  const _FormsPage();

  @override
  State<_FormsPage> createState() => _FormsPageState();
}

class _FormsPageState extends State<_FormsPage> {
  String? _selectedFruit;

  // OTP state — 6 controllers + 6 focus nodes.
  final List<TextEditingController> _otpControllers =
      List<TextEditingController>.generate(6, (_) => TextEditingController());
  final List<FocusNode> _otpFocusNodes =
      List<FocusNode>.generate(6, (_) => FocusNode());

  @override
  void dispose() {
    for (final TextEditingController c in _otpControllers) {
      c.dispose();
    }
    for (final FocusNode n in _otpFocusNodes) {
      n.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(
        HapSpacing.lg,
        60,
        HapSpacing.lg,
        HapSpacing.xxxl,
      ),
      children: <Widget>[
        const H1('Forms'),
        const SizedBox(height: HapSpacing.xxl),

        // -- Input --
        const _SectionLabel('Input'),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsInput(
          label: 'Name',
          hint: 'Enter your name',
        ),
        const SizedBox(height: HapSpacing.lg),
        HappeningsInput(
          label: 'Email',
          hint: 'you@example.com',
          prefixIcon: FontAwesomeIcons.envelope,
          keyboardType: TextInputType.emailAddress,
        ),
        const SizedBox(height: HapSpacing.lg),
        const HappeningsInput(
          label: 'With error',
          hint: 'Something went wrong',
          errorText: 'This field is required',
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Textarea --
        const _SectionLabel('Textarea'),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsTextarea(
          label: 'Bio',
          hint: 'Tell us about yourself...',
          maxLines: 4,
          maxLength: 200,
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Select --
        const _SectionLabel('Select'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsSelect<String>(
          label: 'Favourite fruit',
          placeholder: 'Choose one',
          value: _selectedFruit,
          options: const <HappeningsSelectOption<String>>[
            HappeningsSelectOption(value: 'apple', label: 'Apple'),
            HappeningsSelectOption(value: 'banana', label: 'Banana'),
            HappeningsSelectOption(value: 'cherry', label: 'Cherry'),
            HappeningsSelectOption(value: 'mango', label: 'Mango'),
          ],
          onChanged: (String v) => setState(() => _selectedFruit = v),
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- OTP --
        const _SectionLabel('OTP input'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsOtpInput(
          controllers: _otpControllers,
          focusNodes: _otpFocusNodes,
          onCompleted: () {
            final String code =
                _otpControllers.map((TextEditingController c) => c.text).join();
            showHappeningsToast(
              context,
              title: 'Code entered',
              description: code,
              icon: FontAwesomeIcons.solidCircleCheck,
            );
          },
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Feedback page — toasts, state cards, sheets
// ---------------------------------------------------------------------------

class _FeedbackPage extends StatelessWidget {
  const _FeedbackPage();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(
        HapSpacing.lg,
        60,
        HapSpacing.lg,
        HapSpacing.xxxl,
      ),
      children: <Widget>[
        const H1('Feedback'),
        const SizedBox(height: HapSpacing.xxl),

        // -- Toast --
        const _SectionLabel('Toast'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Show toast',
          icon: FontAwesomeIcons.bell,
          onPressed: () {
            showHappeningsToast(
              context,
              title: 'Event published',
              description: 'Your event is now live.',
              icon: FontAwesomeIcons.solidCircleCheck,
            );
          },
        ),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Show error toast',
          variant: HappeningsButtonVariant.secondary,
          onPressed: () {
            showErrorToast(
              context,
              title: 'Something went wrong',
              details: 'Connection timed out. Please try again.',
            );
          },
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- State cards --
        const _SectionLabel('State cards'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsStateCard(
          title: 'Email verification required',
          description: 'Check your inbox to verify your email address.',
          severity: StateCardSeverity.warning,
          actionLabel: 'Resend email',
          onAction: () {},
        ),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsStateCard(
          title: 'Account verified',
          severity: StateCardSeverity.success,
        ),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsStateCard(
          title: 'Payment processing',
          description: 'This usually takes a few seconds.',
          severity: StateCardSeverity.info,
        ),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsStateCard(
          title: 'Purchase blocked',
          description: 'Your account has been flagged for review.',
          severity: StateCardSeverity.error,
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Sheet --
        const _SectionLabel('Sheet'),
        const SizedBox(height: HapSpacing.sm),
        HappeningsButton(
          label: 'Open sheet',
          icon: FontAwesomeIcons.upRightFromSquare,
          onPressed: () {
            showHappeningsSheet<void>(
              context: context,
              builder: (_) => HappeningsSheetScaffold(
                title: 'Confirm action',
                subtitle: 'This is a standard Happenings sheet.',
                icon: FontAwesomeIcons.circleInfo,
                footer: Padding(
                  padding: const EdgeInsets.symmetric(
                    horizontal: HapSpacing.xl,
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      HappeningsButton(
                        label: 'Confirm',
                        onPressed: () => Navigator.of(context).pop(),
                      ),
                      const SizedBox(height: HapSpacing.sm),
                      HappeningsButton(
                        label: 'Cancel',
                        variant: HappeningsButtonVariant.ghost,
                        onPressed: () => Navigator.of(context).pop(),
                      ),
                    ],
                  ),
                ),
                children: const <Widget>[
                  Body(
                    'Sheets slide up with scroll physics and a drag handle. '
                    'The body scrolls independently while the header stays '
                    'pinned.',
                  ),
                  SizedBox(height: HapSpacing.lg),
                  Body(
                    'Use sheets for confirmations, selections, and actions '
                    'that need more space than a dialog but less commitment '
                    'than a full page.',
                  ),
                ],
              ),
            );
          },
        ),
        const SizedBox(height: HapSpacing.xxl),

        // -- Markdown --
        const _SectionLabel('Markdown'),
        const SizedBox(height: HapSpacing.sm),
        const HappeningsCard(
          child: HappeningsMarkdown(
            markdown: '## Markdown rendering\n\n'
                'Supports **bold**, *italic*, `inline code`, '
                '[links](https://happenings.social), and more.\n\n'
                '- List item one\n'
                '- List item two\n'
                '- List item three\n\n'
                '> Blockquotes work too.',
          ),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

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

  @override
  Widget build(BuildContext context) {
    return Overline(label.toUpperCase(), color: ZincColors.zinc500);
  }
}
0
likes
150
points
52
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Happenings design system — tokens, theme, and shared components for Flutter apps built on Material 3 with a Zinc color palette.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

cached_network_image, flutter, flutter_cache_manager, flutter_markdown_plus, google_fonts, markdown, shimmer, url_launcher

More

Packages that depend on happenings_ui