diff --git a/README.md b/README.md index 6d75e77..cf85687 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Colour roles use **grouped semantic naming** — `textPrimary`, `bgBase`, `actio | Opacity | `opacity_primitives.dart` | `opacity_semantic.dart` | | Duration | `duration_primitives.dart` | — | | Offset | `offset_primitives.dart` | `offset_semantic.dart` | -| Animation | — | `animation_semantic.dart` | +| Animation | `duration_primitives.dart` | `animation_semantic.dart` | | Breakpoint | `breakpoint_primitives.dart` | `breakpoint_semantic.dart` | | Easing | `easing_primitives.dart` | `easing_semantic.dart` | | Z-Index | — | `z_index_semantic.dart` | @@ -99,16 +99,16 @@ DievasThemeData ← abstract interface (the contract) └── [AppThemeData] ← consumer app's brand (lives in the app, not here) ``` -`DievasTheme` is an `InheritedModel`. Each sub system (`colors`, `typography`, `spacing`, `sizing`, `border`, `elevation`, `opacity`, `components`, `material`) is a named aspect; a widget that depends only on `colors` does not rebuild when `spacing` changes. +`DievasTheme` is an `InheritedModel`. Each sub system (`colours`, `typography`, `spacing`, `sizing`, `border`, `elevation`, `opacity`, `animation`, `components`, `material`) is a named aspect; a widget that depends only on `colours` does not rebuild when `spacing` changes. ### Context extension — flat API ```dart // Inside any widget's build method -final color = context.colors.action.actionPrimary; +final color = context.colours.action.actionPrimary; final style = context.typography.labelMd; -final gap = context.spacing.s4; -final cols = context.grid.columns; +final gap = context.spacing.md; +final cols = context.grid.columns; // inside a DievasGrid ``` ### Plugging in a brand theme @@ -120,9 +120,9 @@ Consumer apps never touch `DievasLightThemeData` directly. They extend `DievasGl class ExampleLightThemeData extends DievasGlobalThemeData { ExampleLightThemeData({super.components}) : super( - colors: DievasColourThemeData( + colours: DievasColourThemeData( brightness: Brightness.light, - action: ActionColors(actionPrimary: Color(0xFF7C3AED)), + action: ActionColours(actionPrimary: Color(0xFF7C3AED)), ), border: const DievasBorderThemeData(...), ); @@ -168,17 +168,17 @@ Rules components always follow: 37 components shipped across nine groups: -| Group | Count | Components | -| -------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Buttons** | 4 | `DievasFilledButton`, `DievasOutlinedButton`, `DievasTextButton`, `DievasIconButton` | -| **Display** | 9 | `DievasAvatar`, `DievasBadge`, `DievasCircularProgress`, `DievasDivider`, `DievasDotIndicator`, `DievasEmptyState`, `DievasIcon`, `DievasLinearProgress`, `DievasTag` | -| **Form** | 8 | `DievasAuthCode`, `DievasCheckbox`, `DievasRadio`, `DievasSegmentedControl`, `DievasSwitch`, `DievasTextArea`, `DievasTextInput`, `DievasTextInputGroup` | -| **Feedback** | 3 | `DievasAlert`, `DievasBanner`, `DievasSnackbar` | -| **Menu** | 2 | `DievasDropdown`, `DievasMenuItem` | -| **Overlays** | 3 | `DievasBottomSheet`, `DievasModal`, `DievasTooltip` | -| **Search** | 2 | `DievasSearchWithList`, `DievasSearchWithDropdown` | -| **Nav / Disclosure** | 5 | `DievasAccordion`, `DievasBreadcrumb`, `DievasDrawer`, `DievasPopover`, `DievasTabBar` | -| **Utility** | 1 | `DievasLoader` | +| Group | Count | Components | +| -------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Buttons** | 4 | `DievasFilledButton`, `DievasOutlinedButton`, `DievasTextButton`, `DievasIconButton` | +| **Display** | 9 | `DievasAvatar`, `DievasBadge`, `DievasCircularProgress`, `DievasDivider`, `DievasDotIndicator`, `DievasEmptyState`, `DievasIcon`, `DievasLinearProgress`, `DievasTag` | +| **Form** | 8 | `DievasAuthCode`, `DievasCheckbox`, `DievasRadio`, `DievasSegmentedControl`, `DievasSwitch`, `DievasTextArea`, `DievasTextInput`, `DievasTextInputGroup` | +| **Feedback** | 3 | `DievasAlert`, `DievasBanner`, `DievasSnackbar` | +| **Menu** | 2 | `DievasDropdown`, `DievasMenuItem` | +| **Overlays** | 3 | `DievasBottomSheet`, `DievasModal`, `DievasTooltip` | +| **Search** | 2 | `DievasSearchWithList`, `DievasSearchWithDropdown` | +| **Nav / Disclosure** | 5 | `DievasAccordion`, `DievasBreadcrumb`, `DievasDrawer`, `DievasPopover`, `DievasTabBar` | +| **Utility** | 1 | `DievasLoader` | ## Grid System @@ -221,15 +221,19 @@ flutter run SDK: >=3.11.4 <4.0.0 ``` -Dart 3 idioms throughout — pattern matching with `switch` expressions, sealed classes, records, exhaustive matching. No legacy pre null safety patterns. +Dart 3 idioms throughout: pattern matching with `switch` expressions, sealed classes, records, exhaustive matching. + +No legacy pre null safety patterns. ## Reference Systems | Reference | Role in Dievas | -| --------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------ | -| [Moon Design System](https://flutter.moon.io) | Component catalogue, visual language | **Landing page** — Jaspr SSR + Tailwind v4, deployed on Railway at `dievas.serticode.com`. | +| --------------------------------------------- | ------------------------------------ | +| [Moon Design System](https://flutter.moon.io) | Component catalogue, visual language | + +The landing page is a Jaspr SSR + Tailwind v4 site deployed on Railway at `dievas.serticode.com`. -Token bridge: `landing/tool/generate_theme.dart` reads `dievas_tokens` constants and emits Tailwind `@theme {}` CSS — the same token values power both the Flutter widgets and the server rendered landing page. +Token bridge: `landing/tool/generate_theme.dart` reads `dievas_tokens` constants and emits Tailwind `@theme {}` CSS; the same token values power both the Flutter widgets and the server rendered landing page. ## Team diff --git a/packages/dievas/example/.gitignore b/packages/dievas/example/.gitignore new file mode 100644 index 0000000..79f7eca --- /dev/null +++ b/packages/dievas/example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/packages/dievas/example/README.md b/packages/dievas/example/README.md index bbfad8b..96673fe 100644 --- a/packages/dievas/example/README.md +++ b/packages/dievas/example/README.md @@ -1,3 +1,66 @@ -# dievas_example +# Dievas Example App -A new Flutter project. +A complete, runnable reference app for the [Dievas design system](../README.md). + +This is the app to study before wiring Dievas into your own Flutter project. + +It evolves as the design system grows into maturity and at its core, it demonstrates two things end to end: + +1. **How to set up your app's theme properly**: brand tokens, a custom typography ramp, and light/dark theme data that extend `DievasGlobalThemeData` (the exact pattern every consumer app follows). +2. **How to consume the system**: every context extension, the `DievasTheme` statics, the responsive grid, overlays, localizations, and a broad slice of the component library. + +No names from any other design system appear here. Everything is branded "Example" so you can rename it to your own app. + +## Run it + +```bash +cd packages/dievas/example +flutter pub get +flutter run +``` + +Requires the Melos workspace to be bootstrapped first (`melos bootstrap` at the repo root), because the example depends on `dievas` and `dievas_tokens` by path. + +## What each file teaches + +### `lib/main.dart` + +The entry point. A single `DievasScope` wraps the whole app and: + +- resolves the active theme from `themeMode` + platform brightness, +- inserts the `DievasTheme` InheritedModel into the tree, +- bridges to Material by passing `theme.material` to `MaterialApp`, +- registers `DievasLocalizations` so component copy resolves via `context.l10n`, +- hosts a `NavigationBar` shell with three demo screens. + +### This is the only place an app talks to Dievas directly. Everything below reads the theme through `context.*` + +### `lib/example_theme/` — the brand theme (copy this folder) + +The template every consumer app copies. Study the files in order: + +| File | Teaches | +| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `example_tokens.dart` | Raw `int` brand colours + fonts. Pure values, no `Color` wrapping, no Flutter. | +| `example_typography.dart` | Building a `DievasTypographyThemeData` over the `DievasTypographySemantic` ramp with your own font (Cascadia Code via `google_fonts`). | +| `example_light_theme.dart` | `ExampleLightThemeData extends DievasGlobalThemeData` — supplies the light colour set and typography; everything else is derived. | +| `example_dark_theme.dart` | The same for dark mode. | + +To make it your brand: rename `Example*` → `YourBrand*`, replace +`ExampleColours` with your palette, and point the fonts at your typefaces. + +### `lib/screens/` + +| Screen | Teaches | +| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `example_overview_screen.dart` | Theme switching (`DievasScope.of(context).setThemeMode`), banner/snackbar overlays (`showBanner` / `showSnackbar`), the responsive grid (`DievasGrid` + `context.grid`), and `context.l10n`. | +| `example_foundations_screen.dart` | Every aspect scoped subsystem — `colours`, `typography`, `spacing`, `sizing`, `border`, `elevation`, `opacity`, `animation` — plus the `DievasTheme.of` statics and a per instance component override via `copyWith`. | +| `example_components_screen.dart` | The component library: buttons, form controls, display components, progress, feedback, overlays, tooltips, loaders, and empty states — all driven by the theme. | + +## Key concepts to notice + +- **Granular rebuilds.** `DievasTheme` is an `InheritedModel`; reading `context.colours` rebuilds only when colours change, never when `spacing` changes. +- **Nothing is hardcoded.** No component in the screens hardcodes a colour, size, or spacing; every value comes from `context.*`. +- **Contrast is a contract.** Dievas draws text on action surfaces using `colours.core.onBrand`, so `onBrand` must contrast `actionPrimary`. This brand's light-mode primary action is ink, so `onBrand` is lavender here — read the notes in `example_light_theme.dart` / `example_dark_theme.dart` before remapping any role. +- **Per instance overrides.** Wrap a subtree in `DievasTheme(data: copy)` to override a component for that subtree only (see the Foundations screen). +- **Material bridge.** Standard Flutter widgets (`NavigationBar`, `Scaffold`, `MaterialApp`) work automatically through `theme.material`. diff --git a/packages/dievas/example/analysis_options.yaml b/packages/dievas/example/analysis_options.yaml new file mode 100644 index 0000000..1b67a16 --- /dev/null +++ b/packages/dievas/example/analysis_options.yaml @@ -0,0 +1,11 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** diff --git a/packages/dievas/example/lib/example_theme/example_dark_theme.dart b/packages/dievas/example/lib/example_theme/example_dark_theme.dart new file mode 100644 index 0000000..f13e02b --- /dev/null +++ b/packages/dievas/example/lib/example_theme/example_dark_theme.dart @@ -0,0 +1,120 @@ +part of 'example_theme.dart'; + +/// Example App dark brand theme. +/// +/// Mirror of [ExampleLightThemeData] using the dark colour set. Consumer apps +/// subclass [DievasGlobalThemeData] directly (as done here) rather than the +/// built-in [DievasDarkThemeData], so both modes carry the same brand. +final class ExampleDarkThemeData extends DievasGlobalThemeData { + ExampleDarkThemeData({super.components, DievasTypographyThemeData? typography}) + : super( + colours: _darkColours, + typography: typography ?? buildExampleTypography(textColor: _darkColours.text.textPrimary), + ); + + static const DievasColourThemeData _darkColours = DievasColourThemeData( + brightness: .dark, + core: CoreColours( + brand: Color(ExampleColours.brand), + brandSubtle: Color(ExampleColours.brandSubtle), + onBrand: Color(ExampleColours.onBrand), + ), + text: TextColours( + textPrimary: Color(ExampleColours.textLight), + textSecondary: Color(DievasColourSemanticDark.textSecondary), + textTertiary: Color(DievasColourSemanticDark.textTertiary), + textDisabled: Color(DievasColourSemanticDark.textDisabled), + // textInverse colours text on elevated surfaces (e.g. tooltips). In dark + // mode those surfaces are dark, so the inverse text must be light. + textInverse: Color(ExampleColours.textLight), + textOnBrand: Color(ExampleColours.onBrand), + ), + icon: IconColours( + iconPrimary: Color(DievasColourSemanticDark.iconPrimary), + iconSecondary: Color(DievasColourSemanticDark.iconSecondary), + iconDisabled: Color(DievasColourSemanticDark.iconDisabled), + iconOnBrand: Color(ExampleColours.onBrand), + ), + background: BackgroundColours( + // Brand scaffold: the near-black indigo this product surfaces on. + bgBase: Color(ExampleColours.bgDark), + bgSubtle: Color(DievasColourSemanticDark.bgSubtle), + bgElevated: Color(DievasColourSemanticDark.bgElevated), + bgOverlay: Color(DievasColourSemanticDark.bgOverlay), + ), + border: BorderColours( + borderDefault: Color(DievasColourSemanticDark.borderDefault), + borderStrong: Color(DievasColourSemanticDark.borderStrong), + borderDisabled: Color(DievasColourSemanticDark.borderDisabled), + borderFocus: Color(DievasColourSemanticDark.borderFocus), + borderBrand: Color(ExampleColours.borderBrand), + ), + action: ActionColours( + actionPrimary: Color(ExampleColours.actionPrimaryDark), + actionPrimaryHover: Color(ExampleColours.actionPrimaryHoverDark), + actionPrimaryActive: Color(ExampleColours.actionPrimaryActiveDark), + actionPrimaryDisabled: Color(DievasColourSemanticDark.actionPrimaryDisabled), + actionSecondary: Color(ExampleColours.actionSecondary), + actionSecondaryHover: Color(ExampleColours.actionSecondaryHover), + actionError: Color(DievasColourSemanticDark.actionError), + actionErrorHover: Color(DievasColourSemanticDark.actionErrorHover), + actionSuccess: Color(DievasColourSemanticDark.actionSuccess), + ), + feedback: FeedbackColours( + feedbackSuccess: FeedbackColour( + background: Color(DievasColourSemanticDark.feedbackSuccessBackground), + border: Color(DievasColourSemanticDark.feedbackSuccessBorder), + icon: Color(DievasColourSemanticDark.feedbackSuccessIcon), + text: Color(DievasColourSemanticDark.feedbackSuccessText), + ), + feedbackWarning: FeedbackColour( + background: Color(DievasColourSemanticDark.feedbackWarningBackground), + border: Color(DievasColourSemanticDark.feedbackWarningBorder), + icon: Color(DievasColourSemanticDark.feedbackWarningIcon), + text: Color(DievasColourSemanticDark.feedbackWarningText), + ), + feedbackError: FeedbackColour( + background: Color(DievasColourSemanticDark.feedbackErrorBackground), + border: Color(DievasColourSemanticDark.feedbackErrorBorder), + icon: Color(DievasColourSemanticDark.feedbackErrorIcon), + text: Color(DievasColourSemanticDark.feedbackErrorText), + ), + feedbackInfo: FeedbackColour( + background: Color(DievasColourSemanticDark.feedbackInfoBackground), + border: Color(DievasColourSemanticDark.feedbackInfoBorder), + icon: Color(DievasColourSemanticDark.feedbackInfoIcon), + text: Color(DievasColourSemanticDark.feedbackInfoText), + ), + ), + input: InputColours( + inputBg: Color(DievasColourSemanticDark.inputBg), + inputBorder: Color(DievasColourSemanticDark.inputBorder), + inputBorderFocus: Color(DievasColourSemanticDark.inputBorderFocus), + inputBorderError: Color(DievasColourSemanticDark.inputBorderError), + inputBorderWarning: Color(DievasColourSemanticDark.inputBorderWarning), + inputText: Color(DievasColourSemanticDark.inputText), + inputPlaceholder: Color(DievasColourSemanticDark.inputPlaceholder), + ), + surface: SurfaceColours( + surfaceCanvas: Color(DievasColourSemanticDark.surfaceCanvas), + surfaceCode: Color(DievasColourSemanticDark.surfaceCode), + surfaceSidebar: Color(DievasColourSemanticDark.surfaceSidebar), + surfaceTableStriped: Color(DievasColourSemanticDark.surfaceTableStriped), + ), + switchColours: SwitchColours( + switchTrackOn: Color(DievasColourSemanticDark.switchTrackOn), + switchTrackOff: Color(DievasColourSemanticDark.switchTrackOff), + switchThumbOn: Color(DievasColourSemanticDark.switchThumbOn), + switchThumbOff: Color(DievasColourSemanticDark.switchThumbOff), + ), + staticColours: StaticColours( + staticWhite: Color(ExampleColours.staticWhite), + staticBlack: Color(ExampleColours.staticBlack), + staticTransparent: Color(DievasColourSemanticDark.staticTransparent), + ), + ); + + @override + DievasThemeData copyWith({DievasComponentThemeData? components, DievasTypographyThemeData? typography}) => + ExampleDarkThemeData(components: components, typography: typography); +} diff --git a/packages/dievas/example/lib/example_theme/example_light_theme.dart b/packages/dievas/example/lib/example_theme/example_light_theme.dart new file mode 100644 index 0000000..36186fd --- /dev/null +++ b/packages/dievas/example/lib/example_theme/example_light_theme.dart @@ -0,0 +1,127 @@ +part of 'example_theme.dart'; + +/// Example App light brand theme. +/// +/// Extends [DievasGlobalThemeData] and supplies the light colour set plus a +/// brand typography built over the Dievas type ramp. Everything else — +/// component theme data, elevation, Material bridge — is derived automatically +/// inside the constructor. +/// +/// Copy this file to create your own brand's light theme: swap +/// [ExampleColours] for your tokens and pass a brand `typography`. +final class ExampleLightThemeData extends DievasGlobalThemeData { + ExampleLightThemeData({super.components, DievasTypographyThemeData? typography}) + : super( + colours: _lightColours, + typography: typography ?? buildExampleTypography(textColor: _lightColours.text.textPrimary), + ); + + static final DievasColourThemeData _lightColours = DievasColourThemeData( + brightness: .light, + core: const CoreColours( + brand: Color(ExampleColours.brand), + brandSubtle: Color(ExampleColours.brandSubtle), + // Text on action surfaces. This brand's primary action is ink + // (ExampleColours.actionPrimary), so onBrand must be a light colour — + // lavender mirrors the brand's real ink-button + light-label look. + onBrand: Color(ExampleColours.textLight), + ), + text: const TextColours( + textPrimary: Color(ExampleColours.textDark), + textSecondary: Color(DievasColourSemanticLight.textSecondary), + textTertiary: Color(DievasColourSemanticLight.textTertiary), + textDisabled: Color(DievasColourSemanticLight.textDisabled), + // textInverse colours text on elevated surfaces (e.g. tooltips). In light + // mode those surfaces are white, so the inverse text must be dark. + textInverse: Color(ExampleColours.textDark), + textOnBrand: Color(ExampleColours.onBrand), + ), + icon: const IconColours( + iconPrimary: Color(DievasColourSemanticLight.iconPrimary), + iconSecondary: Color(DievasColourSemanticLight.iconSecondary), + iconDisabled: Color(DievasColourSemanticLight.iconDisabled), + iconOnBrand: Color(ExampleColours.onBrand), + ), + background: const BackgroundColours( + // Brand scaffold: the warm off-white this product surfaces on. + bgBase: Color(ExampleColours.scaffoldBG), + bgSubtle: Color(DievasColourSemanticLight.bgSubtle), + bgElevated: Color(DievasColourSemanticLight.bgElevated), + bgOverlay: Color(DievasColourSemanticLight.bgOverlay), + ), + border: const BorderColours( + borderDefault: Color(DievasColourSemanticLight.borderDefault), + borderStrong: Color(DievasColourSemanticLight.borderStrong), + borderDisabled: Color(DievasColourSemanticLight.borderDisabled), + borderFocus: Color(DievasColourSemanticLight.borderFocus), + borderBrand: Color(ExampleColours.borderBrand), + ), + action: const ActionColours( + actionPrimary: Color(ExampleColours.actionPrimary), + actionPrimaryHover: Color(ExampleColours.actionPrimaryHover), + actionPrimaryActive: Color(ExampleColours.actionPrimaryActive), + actionPrimaryDisabled: Color(DievasColourSemanticLight.actionPrimaryDisabled), + actionSecondary: Color(ExampleColours.actionSecondary), + actionSecondaryHover: Color(ExampleColours.actionSecondaryHover), + actionError: Color(DievasColourSemanticLight.actionError), + actionErrorHover: Color(DievasColourSemanticLight.actionErrorHover), + actionSuccess: Color(DievasColourSemanticLight.actionSuccess), + ), + feedback: const FeedbackColours( + feedbackSuccess: FeedbackColour( + background: Color(DievasColourSemanticLight.feedbackSuccessBackground), + border: Color(DievasColourSemanticLight.feedbackSuccessBorder), + icon: Color(DievasColourSemanticLight.feedbackSuccessIcon), + text: Color(DievasColourSemanticLight.feedbackSuccessText), + ), + feedbackWarning: FeedbackColour( + background: Color(DievasColourSemanticLight.feedbackWarningBackground), + border: Color(DievasColourSemanticLight.feedbackWarningBorder), + icon: Color(DievasColourSemanticLight.feedbackWarningIcon), + text: Color(DievasColourSemanticLight.feedbackWarningText), + ), + feedbackError: FeedbackColour( + background: Color(DievasColourSemanticLight.feedbackErrorBackground), + border: Color(DievasColourSemanticLight.feedbackErrorBorder), + icon: Color(DievasColourSemanticLight.feedbackErrorIcon), + text: Color(DievasColourSemanticLight.feedbackErrorText), + ), + feedbackInfo: FeedbackColour( + background: Color(DievasColourSemanticLight.feedbackInfoBackground), + border: Color(DievasColourSemanticLight.feedbackInfoBorder), + icon: Color(DievasColourSemanticLight.feedbackInfoIcon), + text: Color(DievasColourSemanticLight.feedbackInfoText), + ), + ), + input: const InputColours( + inputBg: Color(DievasColourSemanticLight.inputBg), + inputBorder: Color(DievasColourSemanticLight.inputBorder), + inputBorderFocus: Color(DievasColourSemanticLight.inputBorderFocus), + inputBorderError: Color(DievasColourSemanticLight.inputBorderError), + inputBorderWarning: Color(DievasColourSemanticLight.inputBorderWarning), + inputText: Color(DievasColourSemanticLight.inputText), + inputPlaceholder: Color(DievasColourSemanticLight.inputPlaceholder), + ), + surface: const SurfaceColours( + surfaceCanvas: Color(DievasColourSemanticLight.surfaceCanvas), + surfaceCode: Color(DievasColourSemanticLight.surfaceCode), + surfaceSidebar: Color(DievasColourSemanticLight.surfaceSidebar), + surfaceTableStriped: Color(DievasColourSemanticLight.surfaceTableStriped), + ), + switchColours: const SwitchColours( + switchTrackOn: Color(DievasColourSemanticLight.switchTrackOn), + switchTrackOff: Color(DievasColourSemanticLight.switchTrackOff), + switchThumbOn: Color(DievasColourSemanticLight.switchThumbOn), + switchThumbOff: Color(DievasColourSemanticLight.switchThumbOff), + ), + staticColours: const StaticColours( + staticWhite: Color(ExampleColours.staticWhite), + staticBlack: Color(ExampleColours.staticBlack), + staticTransparent: Color(DievasColourSemanticLight.staticTransparent), + ), + ); + + @override + DievasThemeData copyWith({DievasComponentThemeData? components, DievasTypographyThemeData? typography}) => + ExampleLightThemeData(components: components, typography: typography); +} diff --git a/packages/dievas/example/lib/example_theme/example_theme.dart b/packages/dievas/example/lib/example_theme/example_theme.dart new file mode 100644 index 0000000..0952493 --- /dev/null +++ b/packages/dievas/example/lib/example_theme/example_theme.dart @@ -0,0 +1,20 @@ +/// The Example App brand theme. +/// +/// This folder is the template every consumer app copies to define its own +/// brand: raw tokens → typography → light/dark theme data. Study the four +/// files in order: +/// +/// 1. `example_tokens.dart` — raw brand values (colours, fonts) +/// 2. `example_typography.dart` — the type ramp built over Dievas tokens +/// 3. `example_light_theme.dart` / `example_dark_theme.dart` — the theme classes +library; + +import 'package:dievas/dievas.dart'; +import 'package:dievas_tokens/dievas_tokens.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +part 'example_tokens.dart'; +part 'example_typography.dart'; +part 'example_light_theme.dart'; +part 'example_dark_theme.dart'; diff --git a/packages/dievas/example/lib/example_theme/example_tokens.dart b/packages/dievas/example/lib/example_theme/example_tokens.dart new file mode 100644 index 0000000..0fa7000 --- /dev/null +++ b/packages/dievas/example/lib/example_theme/example_tokens.dart @@ -0,0 +1,69 @@ +part of 'example_theme.dart'; + +/// Raw brand token values for the Example App. +/// +/// Pure `int` ARGB values — no `Color` wrapping here. Wrapping happens in the +/// colour sets of [ExampleLightThemeData] / [ExampleDarkThemeData]. +/// +/// Theme-independent values live on this class; anything that differs per mode +/// lives in the theme files. Values that don't need a brand identity fall back +/// to the Dievas semantic defaults (`DievasColourSemanticLight` / `Dark`). +abstract final class ExampleColours { + ExampleColours._(); + + /// Brand — theme-independent. + static const int brand = 0xFFC9C3F2; // lavender + static const int brandSubtle = 0xFFE6A889; // peach + static const int onBrand = 0xFF100C23; // near-black indigo + + /// Text / icon — one value per mode. + static const int textDark = 0xFF1E163E; + static const int textLight = 0xFFC9C3F2; + static const int textLavender = 0xFF6C609C; + + /// Borders. + static const int borderBrand = 0xFFC9C3F2; + + /// Actions — light mode. + static const int actionPrimary = 0xFF1E163E; + static const int actionPrimaryHover = 0xFF352D57; + static const int actionPrimaryActive = 0xFF474170; + static const int actionSecondary = 0xFFE6A889; + static const int actionSecondaryHover = 0xFFD9977A; + + /// Actions — dark mode. + static const int actionPrimaryDark = 0xFFC9C3F2; + static const int actionPrimaryHoverDark = 0xFFB4ADE8; + static const int actionPrimaryActiveDark = 0xFFA099D8; + + /// Static — theme-independent. + static const int staticWhite = 0xFFF2F0E8; + static const int staticBlack = 0xFF171717; + static const int staticGrey = 0xFFE7E6EE; + static const int actualWhite = 0xFFFFFFFF; + static const int warning = 0xFFAD6239; + static const int warningBG = 0xFFFAE9DF; + static const int transparent = 0x00000000; + + /// Backgrounds. + static const int bgLight = brand; + static const int bgDark = onBrand; + static const int scaffoldBG = 0xFFF2F0E8; + + /// Plan-card gradients. + static const int cardProGradientStart = 0xFF1A103B; + static const int cardProGradientEnd = 0xFF2B1B56; +} + +/// Resolved font families for the Example App. +/// +/// Uses [GoogleFonts] to bundle Cascadia Code at runtime. The Dievas default +/// themes ship MartianMono; brand apps override typography to use their own +/// typefaces, exactly as shown in [buildExampleTypography]. +abstract final class ExampleFonts { + ExampleFonts._(); + static String? _cachedFont; + + /// Cascadia Code — display, body, and code in this example brand. + static String get cascadiaCode => _cachedFont ??= GoogleFonts.cascadiaCode().fontFamily ?? 'Cascadia Code'; +} diff --git a/packages/dievas/example/lib/example_theme/example_typography.dart b/packages/dievas/example/lib/example_theme/example_typography.dart new file mode 100644 index 0000000..2b35357 --- /dev/null +++ b/packages/dievas/example/lib/example_theme/example_typography.dart @@ -0,0 +1,72 @@ +part of 'example_theme.dart'; + +/// Builds the Example App typography scale from Dievas semantic tokens. +/// +/// The Dievas type ramp is expressed as [DievasTypographySemantic] records (raw doubles/ints). +/// +/// This function wraps them into [TextStyle] objects with the brand font family and a baked in text colour; the same colour that is passed by [ExampleLightThemeData] / [ExampleDarkThemeData] so components never need to pass a colour to [Text]. +/// +/// Replace the font arguments to use your own typefaces. +DievasTypographyThemeData buildExampleTypography({ + required Color textColor, + String displayFont = 'Cascadia Code', + String bodyFont = 'Cascadia Code', + String codeFont = 'Cascadia Code', +}) { + final resolvedDisplay = displayFont == 'Cascadia Code' ? ExampleFonts.cascadiaCode : displayFont; + final resolvedBody = bodyFont == 'Cascadia Code' ? ExampleFonts.cascadiaCode : bodyFont; + final resolvedCode = codeFont == 'Cascadia Code' ? ExampleFonts.cascadiaCode : codeFont; + + return DievasTypographyThemeData( + // Display + displayLg: _exampleStyle(DievasTypographySemantic.displayLg, resolvedDisplay, textColor), + displayMd: _exampleStyle(DievasTypographySemantic.displayMd, resolvedDisplay, textColor), + displaySm: _exampleStyle(DievasTypographySemantic.displaySm, resolvedDisplay, textColor), + // Heading + headingXl: _exampleStyle(DievasTypographySemantic.headingXl, resolvedDisplay, textColor), + headingLg: _exampleStyle(DievasTypographySemantic.headingLg, resolvedDisplay, textColor), + headingMd: _exampleStyle(DievasTypographySemantic.headingMd, resolvedDisplay, textColor), + headingSm: _exampleStyle(DievasTypographySemantic.headingSm, resolvedDisplay, textColor), + headingXs: _exampleStyle(DievasTypographySemantic.headingXs, resolvedDisplay, textColor), + // Title + titleLg: _exampleStyle(DievasTypographySemantic.titleLg, resolvedDisplay, textColor), + titleMd: _exampleStyle(DievasTypographySemantic.titleMd, resolvedDisplay, textColor), + titleSm: _exampleStyle(DievasTypographySemantic.titleSm, resolvedDisplay, textColor), + titleXsm: _exampleStyle(DievasTypographySemantic.titleXsm, resolvedDisplay, textColor), + // Body + bodyLg: _exampleStyle(DievasTypographySemantic.bodyLg, resolvedBody, textColor), + bodyMd: _exampleStyle(DievasTypographySemantic.bodyMd, resolvedBody, textColor), + bodySm: _exampleStyle(DievasTypographySemantic.bodySm, resolvedBody, textColor), + bodyXs: _exampleStyle(DievasTypographySemantic.bodyXs, resolvedBody, textColor), + // Label + labelLg: _exampleStyle(DievasTypographySemantic.labelLg, resolvedBody, textColor), + labelMd: _exampleStyle(DievasTypographySemantic.labelMd, resolvedBody, textColor), + labelSm: _exampleStyle(DievasTypographySemantic.labelSm, resolvedBody, textColor), + labelXs: _exampleStyle(DievasTypographySemantic.labelXs, resolvedBody, textColor), + // Code + codeMd: _exampleStyle(DievasTypographySemantic.codeMd, resolvedCode, textColor), + codeSm: _exampleStyle(DievasTypographySemantic.codeSm, resolvedCode, textColor), + ); +} + +FontWeight _exampleWeight(int w) => switch (w) { + 100 => .w100, + 200 => .w200, + 300 => .w300, + 400 => .w400, + 500 => .w500, + 600 => .w600, + 700 => .w700, + 800 => .w800, + 900 => .w900, + _ => .w400, +}; + +TextStyle _exampleStyle(DievasTextStyleToken token, String fontFamily, Color color) => TextStyle( + fontSize: token.fontSize, + fontWeight: _exampleWeight(token.fontWeight), + letterSpacing: token.letterSpacing, + height: token.lineHeight, + fontFamily: fontFamily, + color: color, +); diff --git a/packages/dievas/example/lib/main.dart b/packages/dievas/example/lib/main.dart index aa76115..d9bc165 100644 --- a/packages/dievas/example/lib/main.dart +++ b/packages/dievas/example/lib/main.dart @@ -1,40 +1,90 @@ import 'package:flutter/material.dart'; + import 'package:dievas/dievas.dart'; -void main() => runApp( - DievasScope(lightTheme: DievasLightThemeData(), darkTheme: DievasDarkThemeData(), child: const DievasExampleApp()), -); +import 'example_theme/example_theme.dart'; +import 'screens/example_components_screen.dart'; +import 'screens/example_foundations_screen.dart'; +import 'screens/example_overview_screen.dart'; + +// 1. Entry point +// +// The DievasScope is the root of every Dievas app. It: +// - resolves the active theme from `themeMode` + platform brightness +// - inserts `DievasTheme` (an InheritedModel) into the tree, so every widget below can read the theme via `context.colours`, `context.spacing`, `DievasTheme.of(context)`, etc. +// - bridges to Material via `theme.material` (used below on MaterialApp) +// - owns the banner/snackbar overlay controller (`DievasScope.of(context)`) +// +// This is the only place the app talks to Dievas directly. +// +// Everything below reads the theme through the context — no theme plumbing ever leaks again. +void main() => runApp(const _DievasExampleApp()); -class DievasExampleApp extends StatelessWidget { - const DievasExampleApp({super.key}); +class _DievasExampleApp extends StatelessWidget { + const _DievasExampleApp(); @override - Widget build(BuildContext context) => const MaterialApp(debugShowCheckedModeBanner: false, home: ExampleScreen()); + Widget build(BuildContext context) => DievasScope( + // 2. Brand themes + // + // The Example App defines its own light and dark themes in `example_theme/` (tokens → typography → theme data). + // Swap these for your own brand and the whole design system re-skins itself. + themeMode: .system, + lightTheme: ExampleLightThemeData(), + darkTheme: ExampleDarkThemeData(), + + // 3. Material bridge + // + // DievasScope rebuilds `builder` whenever the resolved theme changes (manual switch or system brightness change), so `theme.material` is always the ThemeData for the active mode. + // Register the Dievas localizations so component internal copy resolves via `context.l10n`. + builder: (context, theme, child) => MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Dievas Example App', + theme: theme.material, + localizationsDelegates: DievasLocalizations.localizationsDelegates, + supportedLocales: DievasLocalizations.supportedLocales, + home: const _ExampleShell(), + ), + ); } -class ExampleScreen extends StatelessWidget { - const ExampleScreen({super.key}); +// 4. Shell +// +// A plain Material NavigationBar hosting the three demo screens. +// +// Using the Material bridge here shows that standard Flutter widgets (NavigationBar, +// Scaffold, AppBar) pick up the Dievas theme automatically via `theme.material`. +class _ExampleShell extends StatefulWidget { + const _ExampleShell(); @override - Widget build(BuildContext context) { - final spacing = context.spacing; - - return Scaffold( - backgroundColor: context.colours.background.bgBase, - appBar: AppBar(title: const Text('Dievas Example')), - body: Center( - child: Padding( - padding: .all(spacing.md), - child: Column( - mainAxisSize: .min, - children: [ - Text('Dievas Design System', style: context.typography.titleLg), - SizedBox(height: spacing.lg), - DievasFilledButton(label: 'Get Started', onPressed: () {}), - ], - ), + State<_ExampleShell> createState() => __ExampleShellState(); +} + +class __ExampleShellState extends State<_ExampleShell> { + int _index = 0; + + @override + Widget build(BuildContext context) => Scaffold( + body: IndexedStack(index: _index, children: _screens), + bottomNavigationBar: NavigationBar( + selectedIndex: _index, + onDestinationSelected: (i) => setState(() => _index = i), + destinations: const [ + NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Overview'), + NavigationDestination( + icon: Icon(Icons.palette_outlined), + selectedIcon: Icon(Icons.palette), + label: 'Foundations', ), - ), - ); - } + NavigationDestination( + icon: Icon(Icons.widgets_outlined), + selectedIcon: Icon(Icons.widgets), + label: 'Components', + ), + ], + ), + ); } + +const _screens = [ExampleOverviewScreen(), ExampleFoundationsScreen(), ExampleComponentsScreen()]; diff --git a/packages/dievas/example/lib/screens/example_components_screen.dart b/packages/dievas/example/lib/screens/example_components_screen.dart new file mode 100644 index 0000000..c50f68b --- /dev/null +++ b/packages/dievas/example/lib/screens/example_components_screen.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; + +import 'package:dievas/dievas.dart'; + +/// Components — the Dievas widget library in action. +/// +/// One section per component group. Every component reads its colours, sizes +/// and spacing from the theme — none of the values below are hardcoded. +class ExampleComponentsScreen extends StatefulWidget { + const ExampleComponentsScreen({super.key}); + + @override + State createState() => _ExampleComponentsScreenState(); +} + +class _ExampleComponentsScreenState extends State { + final TextEditingController _inputController = TextEditingController(); + final TextEditingController _areaController = TextEditingController(); + DievasCheckboxValue _checkbox = .checked; + bool _switchOn = true; + int _radioGroup = 1; + String _segmented = 'Compact'; + String? _dropdown; + + @override + void dispose() { + _inputController.dispose(); + _areaController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => SingleChildScrollView( + padding: .symmetric(horizontal: context.spacing.xl, vertical: context.spacing.x2l), + child: Column( + crossAxisAlignment: .start, + children: [ + Text('Components', style: context.typography.displaySm), + SizedBox(height: context.spacing.sm), + Text( + 'All values come from context.* — nothing is hardcoded.', + style: context.typography.bodyMd.copyWith(color: context.colours.text.textSecondary), + ), + SizedBox(height: context.spacing.x2l), + + const _GroupLabel('Buttons'), + _ButtonRow(), + SizedBox(height: context.spacing.x3l), + + const _GroupLabel('Form'), + DievasTextInput( + controller: _inputController, + label: 'Email', + hint: 'you@example.com', + helperText: 'We never share your email.', + ), + SizedBox(height: context.spacing.md), + DievasTextArea(controller: _areaController, label: 'Message', hint: 'Write something…'), + SizedBox(height: context.spacing.md), + DievasCheckbox( + value: _checkbox, + onChanged: (v) => setState(() => _checkbox = v), + label: 'Subscribe to updates', + ), + SizedBox(height: context.spacing.md), + DievasSwitch(value: _switchOn, onChanged: (v) => setState(() => _switchOn = v), label: 'Notifications'), + SizedBox(height: context.spacing.md), + DievasRadio( + value: 1, + groupValue: _radioGroup, + onChanged: (v) => setState(() => _radioGroup = v), + label: 'Option one', + ), + DievasRadio( + value: 2, + groupValue: _radioGroup, + onChanged: (v) => setState(() => _radioGroup = v), + label: 'Option two', + ), + SizedBox(height: context.spacing.md), + DievasSegmentedControl( + options: const ['Compact', 'Comfortable', 'Spacious'], + value: _segmented, + onChanged: (v) => setState(() => _segmented = v), + ), + SizedBox(height: context.spacing.md), + DievasDropdown( + options: const ['Personal', 'Business', 'Enterprise'], + value: _dropdown, + onChanged: (v) => setState(() => _dropdown = v), + hint: 'Select a plan', + label: 'Plan', + ), + SizedBox(height: context.spacing.x3l), + + const _GroupLabel('Display'), + const _DisplayRow(), + SizedBox(height: context.spacing.lg), + const _ProgressRow(), + SizedBox(height: context.spacing.x3l), + + const _GroupLabel('Feedback & overlays'), + _FeedbackRow(), + SizedBox(height: context.spacing.lg), + const DievasLoader(label: 'Loading…'), + SizedBox(height: context.spacing.x3l), + + const _GroupLabel('Empty state'), + const DievasEmptyState(title: 'No projects yet', description: 'Create your first project to get started.'), + SizedBox(height: context.spacing.x3l), + ], + ), + ); +} + +class _GroupLabel extends StatelessWidget { + const _GroupLabel(this.text); + + final String text; + + @override + Widget build(BuildContext context) => Padding( + padding: .only(bottom: context.spacing.md), + child: Text( + text.toUpperCase(), + style: context.typography.labelXs.copyWith(letterSpacing: 1.8, color: context.colours.text.textTertiary), + ), + ); +} + +class _ButtonRow extends StatelessWidget { + const _ButtonRow(); + + @override + Widget build(BuildContext context) => Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + crossAxisAlignment: .center, + children: [ + DievasFilledButton(label: 'Filled', onPressed: () {}), + DievasOutlinedButton(label: 'Outlined', onPressed: () {}), + DievasTextButton(label: 'Text', onPressed: () {}), + DievasIconButton(icon: const Icon(Icons.favorite_outline), semanticLabel: 'Like', onPressed: () {}), + DievasFilledButton(label: 'Disabled', onPressed: () {}, state: .disabled), + DievasOutlinedButton(label: 'Loading', onPressed: () {}, state: .loading), + ], + ); +} + +class _DisplayRow extends StatelessWidget { + const _DisplayRow(); + + @override + Widget build(BuildContext context) => Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + crossAxisAlignment: .center, + children: [ + // Per-instance colour overrides (added for per-component customization). + const DievasAvatar(initials: 'DX'), + DievasAvatar( + initials: 'DX', + backgroundColour: context.colours.action.actionPrimary, + initialsColour: context.colours.core.onBrand, + ), + const DievasBadge(label: 'New', tone: .primary), + const DievasBadge(label: 'Shipped', tone: .success), + const DievasTag(label: 'Flutter'), + DievasTag(label: 'Removable', onRemove: () {}, backgroundColor: context.colours.action.actionSecondary), + const DievasIcon(Icons.bolt, size: .lg, semanticLabel: 'Bolt'), + const DievasDivider(), + ], + ); +} + +class _ProgressRow extends StatelessWidget { + const _ProgressRow(); + + @override + Widget build(BuildContext context) => Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + crossAxisAlignment: .center, + children: [ + SizedBox(width: 160, child: DievasLinearProgress(value: 0.65)), + SizedBox(width: 160, child: DievasLinearProgress(value: null)), + const DievasCircularProgress(value: 0.65), + const DievasCircularProgress(value: null), + ], + ); +} + +class _FeedbackRow extends StatelessWidget { + const _FeedbackRow(); + + @override + Widget build(BuildContext context) => Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + children: [ + const DievasAlert(tone: .info, title: 'Heads up', description: 'The system will update at 2am.'), + const DievasAlert(tone: .success, title: 'Saved', description: 'Your changes are live.'), + const DievasAlert(tone: .warning, title: 'Almost there', description: 'Complete your profile to unlock invites.'), + const DievasAlert(tone: .error, title: 'Connection lost', description: 'Check your network and retry.'), + DievasFilledButton( + label: 'Bottom sheet', + onPressed: () => showDievasBottomSheet( + context: context, + builder: (context) => DievasBottomSheet( + child: Padding( + padding: .all(context.spacing.lg), + child: Text('Bottom sheet content', style: context.typography.bodyMd), + ), + ), + ), + ), + DievasOutlinedButton( + label: 'Modal', + onPressed: () => showDievasModal( + context: context, + builder: (context) => DievasModal( + title: 'Confirm', + body: 'Do you want to continue?', + actions: [DievasFilledButton(label: 'Confirm', onPressed: () => Navigator.of(context).pop())], + ), + ), + ), + DievasTooltip( + message: 'Tooltips wrap their child.', + child: DievasFilledButton(label: 'Hover me', onPressed: () {}), + ), + ], + ); +} diff --git a/packages/dievas/example/lib/screens/example_foundations_screen.dart b/packages/dievas/example/lib/screens/example_foundations_screen.dart new file mode 100644 index 0000000..d67a8a5 --- /dev/null +++ b/packages/dievas/example/lib/screens/example_foundations_screen.dart @@ -0,0 +1,321 @@ +import 'package:flutter/material.dart'; + +import 'package:dievas/dievas.dart'; + +/// Foundations — every Dievas sub-system, live. +/// +/// Dievas splits the theme into aspect-scoped sub-systems. Widgets read the +/// one they need and only rebuild when it changes. This screen shows each +/// getter (via the flat `context.*` extension and the [DievasTheme] statics), +/// plus a per-instance component override via `copyWith`. +class ExampleFoundationsScreen extends StatelessWidget { + const ExampleFoundationsScreen({super.key}); + + @override + Widget build(BuildContext context) => SingleChildScrollView( + padding: .symmetric(horizontal: context.spacing.xl, vertical: context.spacing.x2l), + child: Column( + crossAxisAlignment: .start, + children: [ + Text('Foundations', style: context.typography.displaySm), + SizedBox(height: context.spacing.sm), + Text( + 'Each card reads one aspect-scoped sub-system.', + style: context.typography.bodyMd.copyWith(color: context.colours.text.textSecondary), + ), + SizedBox(height: context.spacing.x2l), + + const _SubsystemCard( + title: 'context.colours', + description: 'Every semantic colour role, grouped by domain.', + child: _ColourSwatches(), + ), + SizedBox(height: context.spacing.lg), + + const _SubsystemCard( + title: 'context.typography', + description: 'The full type ramp as pre-coloured TextStyles.', + child: _TypographyScale(), + ), + SizedBox(height: context.spacing.lg), + + const _SubsystemCard( + title: 'context.spacing', + description: '4pt-grid spacing tokens.', + child: _SpacingReadout(), + ), + SizedBox(height: context.spacing.lg), + + const _SubsystemCard( + title: 'context.sizing / border / elevation / opacity / animation', + description: 'Component dimensions, radii, shadows, opacity values, motion durations.', + child: _ValueReadouts(), + ), + SizedBox(height: context.spacing.lg), + + const _SubsystemCard( + title: 'DievasTheme statics', + description: 'The same data via DievasTheme.of / coloursOf / … — identical values.', + child: _ThemeStatics(), + ), + SizedBox(height: context.spacing.lg), + + const _SubsystemCard( + title: 'Per-instance override (copyWith)', + description: + 'Wrap a subtree in DievasTheme with a copyWith-ed theme to override ' + 'a component for that subtree only.', + child: _ComponentOverride(), + ), + SizedBox(height: context.spacing.x3l), + ], + ), + ); +} + +class _SubsystemCard extends StatelessWidget { + const _SubsystemCard({required this.title, required this.description, required this.child}); + + final String title; + final String description; + final Widget child; + + @override + Widget build(BuildContext context) => Container( + width: .infinity, + padding: .all(context.spacing.lg), + decoration: BoxDecoration( + color: context.colours.background.bgSubtle, + border: .all(color: context.colours.border.borderDefault), + borderRadius: context.border.lg, + ), + child: Column( + crossAxisAlignment: .start, + children: [ + Text(title, style: context.typography.codeMd), + SizedBox(height: context.spacing.xs), + Text(description, style: context.typography.bodySm.copyWith(color: context.colours.text.textSecondary)), + SizedBox(height: context.spacing.lg), + child, + ], + ), + ); +} + +class _ColourSwatches extends StatelessWidget { + const _ColourSwatches(); + + @override + Widget build(BuildContext context) { + final colours = context.colours; + + return Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + children: [ + _Swatch(label: 'brand', colour: colours.core.brand), + _Swatch(label: 'textPrimary', colour: colours.text.textPrimary), + _Swatch(label: 'bgBase', colour: colours.background.bgBase), + _Swatch(label: 'bgSubtle', colour: colours.background.bgSubtle), + _Swatch(label: 'actionPrimary', colour: colours.action.actionPrimary), + _Swatch(label: 'actionError', colour: colours.action.actionError), + _Swatch(label: 'borderBrand', colour: colours.border.borderBrand), + ], + ); + } +} + +class _Swatch extends StatelessWidget { + const _Swatch({required this.label, required this.colour}); + + final String label; + final Color colour; + + @override + Widget build(BuildContext context) => Container( + width: 96, + padding: .all(context.spacing.sm), + decoration: BoxDecoration( + color: colour, + borderRadius: context.border.md, + border: .all(color: context.colours.border.borderDefault), + ), + child: Text( + label, + style: context.typography.labelXs.copyWith( + color: ThemeData.estimateBrightnessForColor(colour) == Brightness.dark + ? context.colours.staticColours.staticWhite + : context.colours.staticColours.staticBlack, + ), + ), + ); +} + +class _TypographyScale extends StatelessWidget { + const _TypographyScale(); + + @override + Widget build(BuildContext context) { + final t = context.typography; + + return Column( + crossAxisAlignment: .start, + children: [ + Text('displaySm — Display', style: t.displaySm), + SizedBox(height: context.spacing.md), + Text('headingMd — Heading', style: t.headingMd), + SizedBox(height: context.spacing.md), + Text('titleLg — Title', style: t.titleLg), + SizedBox(height: context.spacing.md), + Text('bodyMd — Body copy', style: t.bodyMd), + SizedBox(height: context.spacing.md), + Text('labelMd — Label', style: t.labelMd), + SizedBox(height: context.spacing.md), + Text('codeMd — Inline code', style: t.codeMd), + ], + ); + } +} + +class _SpacingReadout extends StatelessWidget { + const _SpacingReadout(); + + @override + Widget build(BuildContext context) { + final s = context.spacing; + + return Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + children: [ + _StatChip(label: 'xs', value: '${s.xs}'), + _StatChip(label: 'sm', value: '${s.sm}'), + _StatChip(label: 'md', value: '${s.md}'), + _StatChip(label: 'lg', value: '${s.lg}'), + _StatChip(label: 'xl', value: '${s.xl}'), + _StatChip(label: 'x2l', value: '${s.x2l}'), + ], + ); + } +} + +class _ValueReadouts extends StatelessWidget { + const _ValueReadouts(); + + @override + Widget build(BuildContext context) { + final sizing = context.sizing; + final border = context.border; + final elevation = context.elevation; + final opacity = context.opacity; + final animation = context.animation; + + return Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + children: [ + _StatChip(label: 'sizing.buttonHeightMd', value: '${sizing.buttonHeightMd}'), + _StatChip(label: 'sizing.inputHeightMd', value: '${sizing.inputHeightMd}'), + _StatChip(label: 'border.lg radius', value: '${border.lg.topLeft.x}'), + _StatChip(label: 'border.strokeThin', value: '${border.strokeThin}'), + _StatChip(label: 'elevation.md shadows', value: '${elevation.md.length}'), + _StatChip(label: 'opacity.disabled', value: opacity.disabled.toStringAsFixed(2)), + _StatChip(label: 'animation.standard', value: '${animation.standard.inMilliseconds}ms'), + ], + ); + } +} + +class _ThemeStatics extends StatelessWidget { + const _ThemeStatics(); + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: .start, + children: [ + _CodeLine('DievasTheme.of(context) → ${DievasTheme.of(context).runtimeType}'), + SizedBox(height: context.spacing.sm), + _CodeLine('DievasTheme.coloursOf(context).core.brand → ${DievasTheme.coloursOf(context).core.brand}'), + SizedBox(height: context.spacing.sm), + _CodeLine('DievasTheme.animationOf(context).fast → ${DievasTheme.animationOf(context).fast}'), + SizedBox(height: context.spacing.sm), + _CodeLine( + 'DievasTheme.materialOf(context).colorScheme.brightness → ' + '${DievasTheme.materialOf(context).colorScheme.brightness}', + ), + ], + ); +} + +class _CodeLine extends StatelessWidget { + const _CodeLine(this.text); + + final String text; + + @override + Widget build(BuildContext context) => Text(text, style: context.typography.codeSm); +} + +class _ComponentOverride extends StatelessWidget { + const _ComponentOverride(); + + @override + Widget build(BuildContext context) { + // Build a theme copy with the avatar component overridden for this subtree + // only. Everything else inherits from the scope theme above. + final overridden = DievasTheme.of(context).copyWith( + components: DievasTheme.of(context).components.copyWith( + avatar: DievasTheme.of(context).components.avatar.copyWith( + backgroundColour: context.colours.action.actionPrimary, + initialsColour: context.colours.core.onBrand, + ), + ), + ); + + return Row( + children: [ + Column( + crossAxisAlignment: .start, + children: [ + Text('Default', style: context.typography.labelSm), + SizedBox(height: context.spacing.md), + const DievasAvatar(initials: 'DX'), + ], + ), + SizedBox(width: context.spacing.x2l), + Column( + crossAxisAlignment: .start, + children: [ + Text('copyWith override', style: context.typography.labelSm), + SizedBox(height: context.spacing.md), + DievasTheme( + data: overridden, + child: const DievasAvatar(initials: 'DX'), + ), + ], + ), + ], + ); + } +} + +class _StatChip extends StatelessWidget { + const _StatChip({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) => Container( + padding: .symmetric(horizontal: context.spacing.md, vertical: context.spacing.sm), + decoration: BoxDecoration(color: context.colours.background.bgElevated, borderRadius: context.border.md), + child: Column( + crossAxisAlignment: .start, + children: [ + Text(label, style: context.typography.labelXs.copyWith(color: context.colours.text.textTertiary)), + SizedBox(height: context.spacing.xs), + Text(value, style: context.typography.labelMd), + ], + ), + ); +} diff --git a/packages/dievas/example/lib/screens/example_overview_screen.dart b/packages/dievas/example/lib/screens/example_overview_screen.dart new file mode 100644 index 0000000..fe93b46 --- /dev/null +++ b/packages/dievas/example/lib/screens/example_overview_screen.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; + +import 'package:dievas/dievas.dart'; + +/// Overview — how the app wires into the design system. +/// +/// This screen demonstrates: +/// - theme mode switching via [DievasScope.of] +/// - banner / snackbar overlays via [DievasScope.of] +/// - the responsive grid ([DievasGrid] + `context.grid`) +/// - localizations via `context.l10n` +class ExampleOverviewScreen extends StatelessWidget { + const ExampleOverviewScreen({super.key}); + + @override + Widget build(BuildContext context) { + final spacing = context.spacing; + + return DievasGrid( + child: SingleChildScrollView( + padding: .symmetric(horizontal: spacing.xl, vertical: spacing.x2l), + child: Column( + crossAxisAlignment: .start, + children: [ + Text('Dievas Example App', style: context.typography.displaySm), + SizedBox(height: spacing.sm), + Text( + 'Theme setup · overlays · grid · l10n', + style: context.typography.bodyMd.copyWith(color: context.colours.text.textSecondary), + ), + SizedBox(height: spacing.x2l), + + const _SectionCard( + title: 'Theme mode', + description: + 'DievasScope.of(context).setThemeMode(...) drives the active theme. ' + 'Try switching; every widget below updates instantly.', + child: _ThemeModeControl(), + ), + SizedBox(height: spacing.lg), + + const _SectionCard( + title: 'Overlays', + description: + 'DievasScope owns a banner and a snackbar overlay. ' + 'The builder returns a Dievas feedback component; the scope animates it.', + child: _OverlayControls(), + ), + SizedBox(height: spacing.lg), + + const _SectionCard( + title: 'Responsive grid', + description: + 'Wrap content in DievasGrid to read the current breakpoint ' + 'via context.grid. Columns, margin and gutter change with width.', + child: _GridReadout(), + ), + SizedBox(height: spacing.lg), + + _SectionCard( + title: 'Localizations', + description: + 'Component internal copy resolves through context.l10n. ' + 'Register DievasLocalizations delegates on MaterialApp to use it.', + child: _L10nReadout(), + ), + SizedBox(height: spacing.x3l), + ], + ), + ), + ); + } +} + +class _SectionCard extends StatelessWidget { + const _SectionCard({required this.title, required this.description, required this.child}); + + final String title; + final String description; + final Widget child; + + @override + Widget build(BuildContext context) { + final spacing = context.spacing; + final colours = context.colours; + + return Container( + width: .infinity, + padding: .all(spacing.lg), + decoration: BoxDecoration( + color: colours.background.bgSubtle, + border: .all(color: colours.border.borderDefault), + borderRadius: context.border.lg, + ), + child: Column( + crossAxisAlignment: .start, + children: [ + Text(title, style: context.typography.titleLg), + SizedBox(height: spacing.xs), + Text(description, style: context.typography.bodySm.copyWith(color: colours.text.textSecondary)), + SizedBox(height: spacing.lg), + child, + ], + ), + ); + } +} + +class _ThemeModeControl extends StatelessWidget { + const _ThemeModeControl(); + + @override + Widget build(BuildContext context) => DievasSegmentedControl( + options: const [ThemeMode.system, ThemeMode.light, ThemeMode.dark], + value: DievasScope.of(context).themeMode, + onChanged: (mode) => DievasScope.of(context).setThemeMode(mode), + labelBuilder: (mode) => switch (mode) { + .system => 'System', + .light => 'Light', + .dark => 'Dark', + }, + ); +} + +class _OverlayControls extends StatelessWidget { + const _OverlayControls(); + + @override + Widget build(BuildContext context) { + final scope = DievasScope.of(context); + + return Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + children: [ + DievasFilledButton( + label: 'Show banner', + onPressed: () => _showDievasBanner(scope: scope), + ), + DievasOutlinedButton( + label: 'Show snackbar', + onPressed: () => _showDievasSnackbar(scope: scope), + ), + ], + ); + } + + void _showDievasBanner({required DievasScopeController scope}) => scope.showBanner( + (context, _) => DievasBanner( + tone: .info, + message: 'This banner is rendered by DievasScope.showBanner.', + onDismiss: scope.hideBanner, + ), + ); + + void _showDievasSnackbar({required DievasScopeController scope}) => scope.showSnackbar( + (context, _) => DievasSnackbar( + message: 'Snackbar rendered by DievasScope.showSnackbar.', + action: DievasSnackbarAction(label: 'Dismiss', onPressed: scope.hideSnackbar), + ), + ); +} + +class _GridReadout extends StatelessWidget { + const _GridReadout(); + + @override + Widget build(BuildContext context) { + final grid = context.grid; + + return Wrap( + spacing: context.spacing.md, + runSpacing: context.spacing.md, + children: [ + _StatChip(label: 'Breakpoint', value: grid.name), + _StatChip(label: 'Columns', value: '${grid.columns}'), + _StatChip(label: 'Margin', value: '${grid.margin}'), + _StatChip(label: 'Gutter', value: '${grid.gutter}'), + _StatChip(label: 'Family', value: grid.family), + ], + ); + } +} + +class _StatChip extends StatelessWidget { + const _StatChip({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final spacing = context.spacing; + + return Container( + padding: .symmetric(horizontal: spacing.md, vertical: spacing.sm), + decoration: BoxDecoration(color: context.colours.background.bgElevated, borderRadius: context.border.md), + child: Column( + crossAxisAlignment: .start, + children: [ + Text(label, style: context.typography.labelXs.copyWith(color: context.colours.text.textTertiary)), + SizedBox(height: context.spacing.xs), + Text(value, style: context.typography.labelMd), + ], + ), + ); + } +} + +class _L10nReadout extends StatelessWidget { + const _L10nReadout(); + + @override + Widget build(BuildContext context) => Text( + 'context.l10n.emptyStateDefaultTitle → "${context.l10n.emptyStateDefaultTitle}"', + style: context.typography.codeMd.copyWith(color: context.colours.text.textSecondary), + ); +} diff --git a/packages/dievas/example/pubspec.yaml b/packages/dievas/example/pubspec.yaml index 3f0ad5e..6e8a7ef 100644 --- a/packages/dievas/example/pubspec.yaml +++ b/packages/dievas/example/pubspec.yaml @@ -1,6 +1,10 @@ name: dievas_example -description: Example usage of the Dievas design system. +description: >- + Example usage of the Dievas design system — brand theme setup and component + consumption. This is the reference app every consumer should study before + wiring Dievas into their own Flutter app. publish_to: none +resolution: workspace environment: sdk: ">=3.11.4 <4.0.0" @@ -11,3 +15,9 @@ dependencies: sdk: flutter dievas: path: ../ + dievas_tokens: + path: ../../dievas_tokens + google_fonts: ^8.1.0 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/packages/dievas/example/web/favicon.png b/packages/dievas/example/web/favicon.png new file mode 100644 index 0000000..7a4a7d8 Binary files /dev/null and b/packages/dievas/example/web/favicon.png differ diff --git a/packages/dievas/example/web/favicon.svg b/packages/dievas/example/web/favicon.svg new file mode 100644 index 0000000..d6f6ff9 --- /dev/null +++ b/packages/dievas/example/web/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/dievas/example/web/icons/Icon-192.png b/packages/dievas/example/web/icons/Icon-192.png new file mode 100644 index 0000000..7a4a7d8 Binary files /dev/null and b/packages/dievas/example/web/icons/Icon-192.png differ diff --git a/packages/dievas/example/web/icons/Icon-512.png b/packages/dievas/example/web/icons/Icon-512.png new file mode 100644 index 0000000..b7e734e Binary files /dev/null and b/packages/dievas/example/web/icons/Icon-512.png differ diff --git a/packages/dievas/example/web/icons/Icon-maskable-192.png b/packages/dievas/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..2f501e4 Binary files /dev/null and b/packages/dievas/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/dievas/example/web/icons/Icon-maskable-512.png b/packages/dievas/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..7078b3d Binary files /dev/null and b/packages/dievas/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/dievas/example/web/index.html b/packages/dievas/example/web/index.html new file mode 100644 index 0000000..6ab49cd --- /dev/null +++ b/packages/dievas/example/web/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + Dievas Example + + + + + + + diff --git a/packages/dievas/example/web/manifest.json b/packages/dievas/example/web/manifest.json new file mode 100644 index 0000000..8a2e852 --- /dev/null +++ b/packages/dievas/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "dievas_example", + "short_name": "dievas_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/dievas/lib/src/scope/dievas_scope.dart b/packages/dievas/lib/src/scope/dievas_scope.dart index d2491f6..6319636 100644 --- a/packages/dievas/lib/src/scope/dievas_scope.dart +++ b/packages/dievas/lib/src/scope/dievas_scope.dart @@ -60,10 +60,12 @@ class DievasScope extends StatefulWidget { /// Returns the [DievasScopeController] from the nearest [DievasScope] ancestor. /// - /// Use this to call [DievasScopeController.setThemeMode] programmatically, e.g. - /// from a settings screen. + /// Establishing a dependency on the scope, so the calling widget rebuilds + /// whenever the active [themeMode] changes. Use this to call + /// [DievasScopeController.setThemeMode] programmatically, e.g. from a settings + /// screen. static DievasScopeController of(BuildContext context) { - if (context.getInheritedWidgetOfExactType<_DievasScopeStateMarker>() case final result?) { + if (context.dependOnInheritedWidgetOfExactType<_DievasScopeStateMarker>() case final result?) { return result.state; } @@ -178,15 +180,16 @@ class DievasScopeState extends State } @override - Widget build(BuildContext context) => _DievasScopeStateMarker( - state: this, - child: ValueListenableBuilder( - valueListenable: _themeModeNotifier, - builder: (context, mode, child) { - final theme = _themeFor(mode); - final content = widget.builder?.call(context, theme, widget.child) ?? widget.child ?? child ?? const SizedBox(); - - return DievasTheme( + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: _themeModeNotifier, + builder: (context, mode, child) { + final theme = _themeFor(mode); + final content = widget.builder?.call(context, theme, widget.child) ?? widget.child ?? child ?? const SizedBox(); + + return _DievasScopeStateMarker( + state: this, + themeMode: mode, + child: DievasTheme( data: theme, child: Stack( alignment: Alignment.topCenter, @@ -205,10 +208,10 @@ class DievasScopeState extends State ), ], ), - ); - }, - child: widget.child, - ), + ), + ); + }, + child: widget.child, ); DievasThemeData _themeFor(ThemeMode mode) => switch (mode) { @@ -219,12 +222,14 @@ class DievasScopeState extends State } class _DievasScopeStateMarker extends InheritedWidget { - const _DievasScopeStateMarker({required super.child, required this.state}); + const _DievasScopeStateMarker({required super.child, required this.state, required this.themeMode}); final DievasScopeController state; + final ThemeMode themeMode; @override - bool updateShouldNotify(_DievasScopeStateMarker oldWidget) => oldWidget.state != state; + bool updateShouldNotify(_DievasScopeStateMarker oldWidget) => + oldWidget.state != state || oldWidget.themeMode != themeMode; } class _DievasOverlaySlot extends StatelessWidget { diff --git a/packages/dievas/lib/src/themes/dievas_global_theme_data.dart b/packages/dievas/lib/src/themes/dievas_global_theme_data.dart index 71ff453..c5615d2 100644 --- a/packages/dievas/lib/src/themes/dievas_global_theme_data.dart +++ b/packages/dievas/lib/src/themes/dievas_global_theme_data.dart @@ -53,7 +53,7 @@ base class DievasGlobalThemeData implements DievasThemeData { _elevation, components, ); - _material = _buildMaterial(colours); + _material = _buildMaterial(colours, _typography); } DievasGlobalThemeData._raw({ @@ -219,46 +219,30 @@ base class DievasGlobalThemeData implements DievasThemeData { ); } - static ThemeData _buildMaterial(DievasColourThemeData colours) { + static ThemeData _buildMaterial(DievasColourThemeData colours, DievasTypographyThemeData typography) { final brightness = colours.brightness; final seedColour = colours.action.actionPrimary; final scaffoldBg = colours.background.bgBase; final textColour = colours.text.textPrimary; + final baseFont = typography.bodyMd.fontFamily ?? DievasFontFamilyPrimitives.sans; + final displayFont = typography.displaySm.fontFamily ?? DievasFontFamilyPrimitives.sansExtended; final base = ThemeData( brightness: brightness, colorScheme: .fromSeed(seedColor: seedColour, brightness: brightness), scaffoldBackgroundColor: scaffoldBg, useMaterial3: true, - fontFamily: DievasFontFamilyPrimitives.sans, + fontFamily: baseFont, ); return base.copyWith( textTheme: base.textTheme.copyWith( - displayLarge: base.textTheme.displayLarge?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - color: textColour, - ), - displayMedium: base.textTheme.displayMedium?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - color: textColour, - ), - displaySmall: base.textTheme.displaySmall?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - color: textColour, - ), - headlineLarge: base.textTheme.headlineLarge?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - color: textColour, - ), - headlineMedium: base.textTheme.headlineMedium?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - color: textColour, - ), - headlineSmall: base.textTheme.headlineSmall?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - color: textColour, - ), + displayLarge: base.textTheme.displayLarge?.copyWith(fontFamily: displayFont, color: textColour), + displayMedium: base.textTheme.displayMedium?.copyWith(fontFamily: displayFont, color: textColour), + displaySmall: base.textTheme.displaySmall?.copyWith(fontFamily: displayFont, color: textColour), + headlineLarge: base.textTheme.headlineLarge?.copyWith(fontFamily: displayFont, color: textColour), + headlineMedium: base.textTheme.headlineMedium?.copyWith(fontFamily: displayFont, color: textColour), + headlineSmall: base.textTheme.headlineSmall?.copyWith(fontFamily: displayFont, color: textColour), titleLarge: base.textTheme.titleLarge?.copyWith(color: textColour), titleMedium: base.textTheme.titleMedium?.copyWith(color: textColour), titleSmall: base.textTheme.titleSmall?.copyWith(color: textColour), @@ -270,20 +254,12 @@ base class DievasGlobalThemeData implements DievasThemeData { labelSmall: base.textTheme.labelSmall?.copyWith(color: textColour), ), primaryTextTheme: base.primaryTextTheme.copyWith( - displayLarge: base.primaryTextTheme.displayLarge?.copyWith(fontFamily: DievasFontFamilyPrimitives.sansExtended), - displayMedium: base.primaryTextTheme.displayMedium?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - ), - displaySmall: base.primaryTextTheme.displaySmall?.copyWith(fontFamily: DievasFontFamilyPrimitives.sansExtended), - headlineLarge: base.primaryTextTheme.headlineLarge?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - ), - headlineMedium: base.primaryTextTheme.headlineMedium?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - ), - headlineSmall: base.primaryTextTheme.headlineSmall?.copyWith( - fontFamily: DievasFontFamilyPrimitives.sansExtended, - ), + displayLarge: base.primaryTextTheme.displayLarge?.copyWith(fontFamily: displayFont), + displayMedium: base.primaryTextTheme.displayMedium?.copyWith(fontFamily: displayFont), + displaySmall: base.primaryTextTheme.displaySmall?.copyWith(fontFamily: displayFont), + headlineLarge: base.primaryTextTheme.headlineLarge?.copyWith(fontFamily: displayFont), + headlineMedium: base.primaryTextTheme.headlineMedium?.copyWith(fontFamily: displayFont), + headlineSmall: base.primaryTextTheme.headlineSmall?.copyWith(fontFamily: displayFont), ), ); } diff --git a/packages/dievas/test/theme/dievas_theme_test.dart b/packages/dievas/test/theme/dievas_theme_test.dart index 075cc1e..aeac1b8 100644 --- a/packages/dievas/test/theme/dievas_theme_test.dart +++ b/packages/dievas/test/theme/dievas_theme_test.dart @@ -136,6 +136,15 @@ void main() { expect(DievasLightThemeData().material, isNotNull); }); + test('material bridge derives font family from typography', () { + final typography = _typographyWithFamily(DievasLightThemeData().typography, 'TestBrandFont'); + final theme = DievasLightThemeData(typography: typography); + + expect(theme.material.textTheme.bodyMedium?.fontFamily, 'TestBrandFont'); + expect(theme.material.textTheme.labelMedium?.fontFamily, 'TestBrandFont'); + expect(theme.material.textTheme.displaySmall?.fontFamily, 'TestBrandFont'); + }); + test('copyWith returns different instance', () { final t1 = DievasLightThemeData(); final t2 = t1.copyWith(); @@ -143,6 +152,41 @@ void main() { }); }); + group('DievasScope', () { + testWidgets('DievasScope.of dependents rebuild when themeMode changes', (tester) async { + var buildCount = 0; + var currentMode = ThemeMode.system; + + await tester.pumpWidget( + DievasScope( + lightTheme: DievasLightThemeData(), + darkTheme: DievasDarkThemeData(), + child: MaterialApp( + home: Builder( + builder: (context) { + buildCount++; + currentMode = DievasScope.of(context).themeMode; + return TextButton( + onPressed: () => DievasScope.of(context).setThemeMode(ThemeMode.dark), + child: const Text('toggle'), + ); + }, + ), + ), + ), + ); + + expect(currentMode, ThemeMode.system); + final buildsBeforeTap = buildCount; + + await tester.tap(find.text('toggle')); + await tester.pump(); + + expect(currentMode, ThemeMode.dark); + expect(buildCount, greaterThan(buildsBeforeTap)); + }); + }); + group('DievasDarkThemeData', () { test('default constructor does not throw', () { expect(DievasDarkThemeData(), isA()); @@ -366,3 +410,32 @@ void main() { }); }); } + +DievasTypographyThemeData _typographyWithFamily(DievasTypographyThemeData t, String family) { + TextStyle withFamily(TextStyle style) => style.copyWith(fontFamily: family); + + return DievasTypographyThemeData( + displayLg: withFamily(t.displayLg), + displayMd: withFamily(t.displayMd), + displaySm: withFamily(t.displaySm), + headingXl: withFamily(t.headingXl), + headingLg: withFamily(t.headingLg), + headingMd: withFamily(t.headingMd), + headingSm: withFamily(t.headingSm), + headingXs: withFamily(t.headingXs), + titleLg: withFamily(t.titleLg), + titleMd: withFamily(t.titleMd), + titleSm: withFamily(t.titleSm), + titleXsm: withFamily(t.titleXsm), + bodyLg: withFamily(t.bodyLg), + bodyMd: withFamily(t.bodyMd), + bodySm: withFamily(t.bodySm), + bodyXs: withFamily(t.bodyXs), + labelLg: withFamily(t.labelLg), + labelMd: withFamily(t.labelMd), + labelSm: withFamily(t.labelSm), + labelXs: withFamily(t.labelXs), + codeMd: withFamily(t.codeMd), + codeSm: withFamily(t.codeSm), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 7db5777..d16c4e0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: cfd4f5f575a49c5f10ca856e9846073f1e6c3ee94912377eea5f6cefc5272941 + url: "https://pub.dev" + source: hosted + version: "2.0.0" collection: dependency: transitive description: @@ -248,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_fonts: + dependency: transitive + description: + name: google_fonts + sha256: e3cb3ee6b47fd2472c23de6da5744796a4da195137759ddb3fbcc9467b7b3c7d + url: "https://pub.dev" + source: hosted + version: "8.2.1" graphs: dependency: transitive description: @@ -256,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f + url: "https://pub.dev" + source: hosted + version: "2.2.0" http: dependency: transitive description: @@ -296,6 +320,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" json_annotation: dependency: transitive description: @@ -408,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 + url: "https://pub.dev" + source: hosted + version: "9.6.0" package_config: dependency: transitive description: @@ -424,6 +480,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -496,6 +600,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" + url: "https://pub.dev" + source: hosted + version: "1.1.1" resizable_widget: dependency: transitive description: @@ -765,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.11.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9cd5ee3..3f3d154 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: workspace: - packages/dievas + - packages/dievas/example - packages/dievas_gallery - packages/dievas_tokens