flutter-duskmoon-design
Flutter DuskMoon UI — Design Principles & Usage Rules
Architecture Overview
Package Dependency Graph (import direction →)
duskmoon-dev/design (YAML → codegen)
→ duskmoon_theme
├ DmDesignTokens (generated const data)
├ DmTheme (InheritedWidget)
├ DmPlatformStyle { material, cupertino, fluent }
└ toMaterial() / toCupertino() / toFluent()
duskmoon_theme
→ duskmoon_widgets
├ DuskmoonApp (root shell)
├ DmAdaptiveWidget (base class)
└ Dm* widgets (Button, TextField, Switch, etc.)
duskmoon_widgets
→ duskmoon_settings
→ duskmoon_feedback
→ duskmoon_ui (umbrella re-export)
Rule: Never import downstream. duskmoon_theme must never import from duskmoon_widgets. duskmoon_widgets must never import from duskmoon_settings.
Widget Tree (Runtime)
DuskmoonApp(tokens: .sunshine, darkTokens: .moonlight, platformStyle: .cupertino)
└→ DmTheme (InheritedWidget — .of(context) available everywhere below)
└→ CupertinoApp(theme: tokens.toCupertino())
└→ user's widget tree
└→ DmButton(label: "Save") // dispatches to CupertinoButton
Rule 1: Theme Setup
Correct App Root
// ✅ ALWAYS — use DuskmoonApp as the root widget
void main() {
runApp(
DuskmoonApp(
tokens: DmDesignTokens.sunshine,
darkTokens: DmDesignTokens.moonlight,
themeMode: ThemeMode.system,
platformStyle: DmPlatformStyle.material,
home: const MyHomePage(),
),
);
}
❌ NEVER — bypass DuskmoonApp
// ❌ NEVER wrap MaterialApp/CupertinoApp directly
void main() {
runApp(MaterialApp(
theme: DmDesignTokens.sunshine.toMaterial(), // wrong — skips DmTheme
home: MyHomePage(),
));
}
Why: DuskmoonApp injects DmTheme above the platform app. Without it, DmTheme.of(context) returns null and all Dm* widgets fail to resolve tokens.
Rule 2: Accessing Design Tokens
Always use DmTheme.of(context)
// ✅ Read tokens from the widget tree
Widget build(BuildContext context) {
final tokens = DmTheme.of(context).tokens;
return Container(
color: tokens.primaryContainer,
child: Text('Hello', style: TextStyle(color: tokens.onPrimaryContainer)),
);
}
❌ NEVER reference generated constants directly in widget builds
// ❌ This ignores dark mode, theme overrides, and subtree overrides
Widget build(BuildContext context) {
return Container(color: DmDesignTokens.sunshine.primaryContainer);
}
Why: The resolved tokens depend on ThemeMode, platform brightness, and possible DmThemeOverride ancestors. Only DmTheme.of(context) returns the correct resolved set.
Exception — static adapter methods
DuskmoonApp itself must convert tokens to platform ThemeData before DmTheme exists in the tree. For this case only, use the static adapters:
// Inside DuskmoonApp.build() — acceptable
MaterialApp(theme: DmTheme.staticToMaterial(resolvedTokens));
Rule 3: Color System
Token Structure (61 color tokens per theme)
| Group | Tokens | Usage |
|---|---|---|
| Primary | primary, onPrimary, primaryContainer, onPrimaryContainer |
Main brand actions, primary CTAs |
| Secondary | secondary, onSecondary, secondaryContainer, onSecondaryContainer |
Supporting actions, alternative CTAs |
| Tertiary | tertiary, onTertiary, tertiaryContainer, onTertiaryContainer |
Accent highlights, badges, special UI |
| Error | error, onError, errorContainer, onErrorContainer |
Error states, destructive actions |
| Surface | surface, onSurface, surfaceDim, surfaceBright, surfaceContainerLowest→Highest, surfaceVariant, onSurfaceVariant |
Backgrounds, cards, elevation |
| Outline | outline, outlineVariant |
Borders, dividers |
| Inverse | inverseSurface, inverseOnSurface, inversePrimary |
Snackbars, contrast overlays |
| Scrim/Shadow | scrim (with alpha), shadow |
Modal overlays, elevation shadows |
| Semantic | info, success, warning (+ content variants) |
Status indicators |
Color Format: OKLCH
All colors are defined in OKLCH in the source CSS. The Dart codegen converts to Color objects via inline OKLCH→sRGB math (zero external deps).
❌ NEVER hardcode color values
// ❌ Hardcoded hex
Container(color: Color(0xFF60A5FA))
// ❌ Hardcoded Material color
Container(color: Colors.blue)
// ✅ Use design tokens
Container(color: DmTheme.of(context).tokens.primary)
Semantic color pairing rule
Every background token has a corresponding foreground token. Always pair them:
// ✅ Correct pairing
Container(
color: tokens.primaryContainer,
child: Text('Label', style: TextStyle(color: tokens.onPrimaryContainer)),
)
// ❌ Mismatched — onSurface on primaryContainer may fail contrast
Container(
color: tokens.primaryContainer,
child: Text('Label', style: TextStyle(color: tokens.onSurface)),
)
| Background | Foreground |
|---|---|
primary |
onPrimary |
primaryContainer |
onPrimaryContainer |
secondary |
onSecondary |
secondaryContainer |
onSecondaryContainer |
tertiary |
onTertiary |
tertiaryContainer |
onTertiaryContainer |
surface |
onSurface |
surfaceVariant |
onSurfaceVariant |
error |
onError |
errorContainer |
onErrorContainer |
inverseSurface |
inverseOnSurface |
Surface elevation hierarchy
Use surface container tokens for visual depth, not opacity or shadows alone:
surfaceContainerLowest → bottom layer (behind everything)
surfaceContainerLow → low-elevation cards
surfaceContainer → standard cards/containers
surfaceContainerHigh → elevated cards, menus
surfaceContainerHighest → dialogs, tooltips, top layer
Rule 4: Available Themes
5 themes defined in duskmoon-dev/design, codegen'd to Dart:
| Theme | Mode | Primary Character |
|---|---|---|
sunshine |
light | Warm amber/gold |
moonlight |
dark | Cool blue/lavender |
ocean |
dark | Deep blue/teal |
forest |
light | Natural green/earth |
sunset |
light | Warm orange/rose |
Access via DmDesignTokens.sunshine, DmDesignTokens.moonlight, etc.
Rule 5: Platform Adaptive Widgets
Resolution Stack (highest priority first)
L1: Per-widget `platformOverride` parameter
L2: Nearest DmPlatformOverride ancestor (subtree override)
L3: DmTheme.of(context).platformStyle (from DuskmoonApp)
L4: defaultTargetPlatform (auto-detect)
Writing an adaptive widget
All adaptive widgets extend DmAdaptiveWidget:
class DmButton extends DmAdaptiveWidget {
const DmButton({super.key, required this.label, super.platformOverride});
final String label;
Widget buildMaterial(BuildContext context, DmDesignTokens tokens) {
return FilledButton(onPressed: () {}, child: Text(label));
}
Widget buildCupertino(BuildContext context, DmDesignTokens tokens) {
return CupertinoButton.filled(onPressed: () {}, child: Text(label));
}
// buildFluent defaults to buildMaterial unless overridden
}
❌ NEVER check platform manually
// ❌ Manual platform switching
if (Platform.isIOS) {
return CupertinoButton(...);
} else {
return ElevatedButton(...);
}
// ✅ Use DmAdaptiveWidget dispatch or DmPlatformStyle resolution
class MyWidget extends DmAdaptiveWidget { ... }
File structure for adaptive widgets
dm_button/
├── dm_button.dart # Public API, extends DmAdaptiveWidget
├── dm_button_material.dart # buildMaterial implementation
├── dm_button_cupertino.dart # buildCupertino implementation
└── dm_button_fluent.dart # buildFluent (optional, falls through to material)
Rule 6: Shared Design Enums
All Dm* widgets share these semantic enums. Use them consistently — never invent ad-hoc parameters.
enum DmColorRole { primary, secondary, tertiary, error, neutral }
enum DmSize { xs, sm, md, lg, xl }
enum DmButtonVariant { filled, outlined, ghost, tonal }
enum DmInputVariant { outlined, filled, underlined }
Color resolution from DmColorRole
Every widget that takes DmColorRole resolves tokens identically:
| DmColorRole | Background | Foreground | Container | On Container |
|---|---|---|---|---|
primary |
tokens.primary |
tokens.onPrimary |
tokens.primaryContainer |
tokens.onPrimaryContainer |
secondary |
tokens.secondary |
tokens.onSecondary |
tokens.secondaryContainer |
tokens.onSecondaryContainer |
tertiary |
tokens.tertiary |
tokens.onTertiary |
tokens.tertiaryContainer |
tokens.onTertiaryContainer |
error |
tokens.error |
tokens.onError |
tokens.errorContainer |
tokens.onErrorContainer |
neutral |
tokens.surface |
tokens.onSurface |
tokens.surfaceContainerHigh |
tokens.onSurface |
Size scale
| DmSize | Horizontal padding | Vertical padding | Font scale |
|---|---|---|---|
xs |
8 | 4 | 0.75rem (12) |
sm |
12 | 6 | 0.875rem (14) |
md |
16 | 8 | 0.875rem (14) |
lg |
24 | 12 | 1rem (16) |
xl |
32 | 16 | 1.125rem (18) |
Rule 7: Component Design — Actions
DmButton
Default: variant: filled, color: primary, size: md
| Variant | Background | Foreground | Border | Use case |
|---|---|---|---|---|
filled |
role color | onRole | none | Primary CTAs, main actions |
outlined |
transparent | role color | role color | Secondary actions, cancel |
ghost |
transparent | role color | none | Tertiary/inline actions, links |
tonal |
roleContainer | onRoleContainer | none | Soft emphasis, toggles |
Color role assignment convention:
| Action type | Color role | Example |
|---|---|---|
| Main CTA, save, submit, confirm | primary |
"Save Changes" |
| Alternative action, secondary flow | secondary |
"Export", "Share" |
| Accent action, special highlight | tertiary |
"Watch Demo", "Premium" |
| Destructive, delete, remove | error |
"Delete Account" |
| Neutral, dismiss, low emphasis | neutral |
"Cancel", "Skip" |
// ✅ Typical action group
Row(children: [
DmButton(variant: .ghost, color: .neutral, child: Text('Cancel')),
DmButton(variant: .outlined, color: .secondary, child: Text('Save Draft')),
DmButton(variant: .filled, color: .primary, child: Text('Publish')),
])
DmIconButton
Same color/size system as DmButton. Must always have semanticLabel.
DmIconButton(
icon: Icons.delete,
color: DmColorRole.error,
semanticLabel: 'Delete item',
onPressed: () {},
)
DmFab (Floating Action Button)
- Default color:
primary— the single most important action on the screen - Surface:
primaryContainerbackground,onPrimaryContainericon - Rule: Maximum one FAB per screen. If you need multiple actions, use
DmActionList.
DmFab(
onPressed: () {},
icon: Icons.add,
// FAB always uses primaryContainer/onPrimaryContainer — no color param
)
DmActionList
Adapts rendering to available space:
| Breakpoint | Rendering |
|---|---|
| Small (< 600) | Popup menu (overflow) |
| Medium (600–1200) | Icon buttons in row |
| Large (> 1200) | Text buttons with icons |
DmActionList(
actions: [
DmAction(icon: Icons.edit, label: 'Edit', onPressed: ...),
DmAction(icon: Icons.share, label: 'Share', onPressed: ...),
DmAction(icon: Icons.delete, label: 'Delete', color: DmColorRole.error, onPressed: ...),
],
)
Rule 8: Component Design — Navigation
DmAppBar
Default token mapping:
| Element | Token | Rationale |
|---|---|---|
| Background | primary |
Brand presence, top-level identity |
| Title text | onPrimary |
Contrast on primary |
| Icon buttons | onPrimary |
Consistent with primary surface |
| Bottom border | none (primary fills) | Clean branded bar |
Scrolled/elevated state: Background transitions to primaryContainer, text to onPrimaryContainer.
DmAppBar(
title: Text('Settings'),
leading: DmIconButton(icon: Icons.arrow_back, semanticLabel: 'Back'),
actions: [
DmIconButton(icon: Icons.search, semanticLabel: 'Search'),
DmIconButton(icon: Icons.more_vert, semanticLabel: 'More options'),
],
)
Neutral variant: For screens where the app bar should not compete with content (e.g., content-heavy reading views), pass color: DmColorRole.neutral to fall back to surface/onSurface.
DmBottomNav
| Element | Token |
|---|---|
| Background | primary |
| Selected icon/label | onPrimary |
| Unselected icon/label | onPrimary at 70% opacity |
| Selected indicator | primaryContainer (pill behind icon) |
| Top border | none (primary fills) |
Rule: 3–5 destinations maximum. Labels always visible (not icon-only).
DmTabBar
| Element | Token |
|---|---|
| Background | surface |
| Selected tab | primary (indicator + text) |
| Unselected tab | onSurfaceVariant |
| Indicator | primary (bottom line in Material, pill in Cupertino) |
DmDrawer
| Element | Token |
|---|---|
| Background | secondary |
| Header area | secondaryContainer |
| Selected item bg | onSecondary at 15% opacity |
| Selected item text | onSecondary |
| Unselected text | onSecondary at 70% opacity |
| Dividers | onSecondary at 20% opacity |
| Scrim (overlay behind drawer) | scrim with alpha |
Side menus and drawers use the secondary color family to visually distinguish navigation chrome from the primary-branded top bar.
DmBreadcrumbs
| Element | Token |
|---|---|
| Active (current) | onSurface (no link) |
| Ancestors (links) | primary |
| Separator | onSurfaceVariant |
Rule 9: Component Design — Layout & Cards
DmCard
Elevation hierarchy via surface tokens:
| Card style | Background token | Use case |
|---|---|---|
| Flat | surface |
Inline content, no separation |
| Outlined | surface + outlineVariant border |
List items, settings rows |
| Elevated | surfaceContainerLow |
Standard cards |
| Filled | surfaceContainerHigh |
Emphasized/grouped content |
Interior layout convention:
┌─────────────────────────────────┐
│ [optional media/image] │
├─────────────────────────────────┤
│ Title (onSurface) │
│ Subtitle (onSurfaceVariant)│
│ │
│ Body text (onSurface) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Actions: ghost/outlined btns│ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
DmCard(
style: DmCardStyle.elevated,
child: Column(children: [
Image(...),
Padding(
padding: EdgeInsets.all(16),
child: Column(children: [
Text('Title', style: TextStyle(color: tokens.onSurface)),
Text('Subtitle', style: TextStyle(color: tokens.onSurfaceVariant)),
Row(children: [
DmButton(variant: .ghost, child: Text('Cancel')),
DmButton(variant: .filled, child: Text('Confirm')),
]),
]),
),
]),
)
❌ NEVER put a filled primary card background with onPrimary text for regular content cards. Primary/secondary/tertiary containers are for interactive highlights (selected state, feature callout), not default card backgrounds.
DmDivider
| Variant | Token | Use case |
|---|---|---|
| Default | outlineVariant |
Section separation |
| Strong | outline |
Major section breaks |
DmScaffold
Responsive layout dispatch:
| Breakpoint | Navigation style |
|---|---|
| Compact (< 600) | DmBottomNav |
| Medium (600–1200) | NavigationRail (collapsed) |
| Expanded (> 1200) | NavigationRail (expanded with labels) |
Page body background: surface. Rail/side nav background: secondary.
Rule 10: Component Design — Data Display (Bricks)
DmBadge
Small status/count indicator. Takes DmColorRole.
| Variant | Background | Foreground | Use case |
|---|---|---|---|
| Filled | role color | onRole | Notification count, status dot |
| Tonal | roleContainer | onRoleContainer | Soft label, category tag |
Default: color: error (notification convention), size: sm
DmBadge(count: 3) // red notification dot
DmBadge(label: 'New', color: .tertiary, variant: .tonal) // soft accent tag
DmBadge(label: 'Draft', color: .neutral, variant: .tonal) // muted status
DmChip
Selectable/filterable labels. Takes DmColorRole.
| State | Background | Foreground | Border |
|---|---|---|---|
| Unselected | surface |
onSurfaceVariant |
outline |
| Selected | secondaryContainer |
onSecondaryContainer |
none |
| Disabled | surface at 38% opacity |
onSurface at 38% |
outline at 12% |
Default selection color: secondary — secondary containers are for selection states.
DmAvatar
| Variant | Background | Foreground |
|---|---|---|
| With image | — | — |
| Initials (default) | primaryContainer |
onPrimaryContainer |
| Initials (group variety) | Cycle through primary/secondary/tertiary containers |
Matching onContainer |
Sizes follow DmSize enum. Default: md (40dp diameter).
DmStat (Data Brick)
Statistics display block:
┌───────────────┐
│ 1,234 │ ← value: onSurface, large/bold
│ Active Users │ ← label: onSurfaceVariant, small
│ ▲ 12.5% │ ← trend: success or error token
└───────────────┘
| Element | Token |
|---|---|
| Value | onSurface |
| Label | onSurfaceVariant |
| Positive trend | success (or tokens.success) |
| Negative trend | error |
| Card background | surfaceContainerLow (when in card) |
DmTable / Data Grid
| Element | Token |
|---|---|
| Header row bg | surfaceContainerHigh |
| Header text | onSurface (bold) |
| Body row bg (even) | surface |
| Body row bg (odd) | surfaceContainerLowest |
| Body text | onSurface |
| Row hover | surfaceContainerLow |
| Selected row | secondaryContainer |
| Border/grid lines | outlineVariant |
| Sort indicator | primary |
Rule 11: Component Design — Feedback
DmAlert
| Semantic | Background | Foreground | Icon color |
|---|---|---|---|
| Info | infoContainer (or surfaceContainerHigh + info icon) |
onSurface |
info |
| Success | successContainer |
onSurface |
success |
| Warning | warningContainer |
onSurface |
warning |
| Error | errorContainer |
onErrorContainer |
error |
Convention: Alerts use semantic container tokens with full-width layout. For inline indicators, use DmBadge.
DmDialog
| Element | Token |
|---|---|
| Scrim (backdrop) | scrim with alpha |
| Dialog surface | surfaceContainerHighest |
| Title | onSurface |
| Body | onSurfaceVariant |
| Confirm button | DmButton(variant: .filled, color: .primary) |
| Cancel button | DmButton(variant: .ghost, color: .neutral) |
| Destructive confirm | DmButton(variant: .filled, color: .error) |
DmSnackbar
Uses inverse tokens for contrast against current theme:
| Element | Token |
|---|---|
| Background | inverseSurface |
| Text | inverseOnSurface |
| Action button | inversePrimary |
DmProgress
Linear and circular variants. Default color: primary.
| Variant | Track | Indicator |
|---|---|---|
| Default | surfaceContainerHighest |
primary |
| With color role | roleContainer (at low opacity) |
role color |
DmSkeleton
Loading placeholder. Uses surfaceContainerHigh with shimmer animation toward surfaceContainerLow.
Rule 12: Component Design — Inputs
DmTextField
| Variant | Idle | Focused | Error |
|---|---|---|---|
outlined |
outlineVariant border |
primary border (2px) |
error border |
filled |
surfaceContainerHighest bg |
primary bottom indicator |
error indicator |
underlined |
outlineVariant bottom line |
primary bottom line (2px) |
error line |
| Element | Token |
|---|---|
| Input text | onSurface |
| Placeholder/hint | onSurfaceVariant |
| Label (floating) | onSurfaceVariant → primary when focused |
| Helper text | onSurfaceVariant |
| Error text | error |
| Prefix/suffix icon | onSurfaceVariant |
Default variant: outlined
DmCheckbox / DmSwitch / DmSlider
| State | Token |
|---|---|
| Unchecked/off | onSurfaceVariant (border), surface (fill) |
| Checked/on | primary (fill), onPrimary (checkmark) |
| Track (switch off) | surfaceContainerHighest |
| Track (switch on) | primary at 50% → primaryContainer |
| Thumb | outline (off) → onPrimary (on, over primary track) |
| Slider active track | primary |
| Slider inactive track | surfaceContainerHighest |
| Slider thumb | primary |
| Disabled | All at 38% opacity |
Rule 13: Visual Design Principles
Hierarchy through token roles, not through ad-hoc colors
Primary → THE action (one per screen section)
Secondary → supporting actions, selection states
Tertiary → accents, highlights, special callouts
Surface → everything else (backgrounds, text, structure)
If you need emphasis, promote the token role — don't invent a color.
Density and spacing
DuskMoon follows MD3 density: default padding 16dp, compact 12dp, comfortable 24dp. Widget padding follows the DmSize scale.
Elevation = surface tokens, not shadows
Use surfaceContainerLowest → surfaceContainerHighest for visual hierarchy. Shadows (shadow token) are supplementary, not the primary depth cue.
// ✅ Surface-token elevation
Container(color: tokens.surfaceContainerHigh) // elevated
Container(color: tokens.surface) // base level
// ❌ Shadow-only elevation
Container(
decoration: BoxDecoration(
color: tokens.surface,
boxShadow: [BoxShadow(blurRadius: 8)], // shadow without surface distinction
),
)
Dark mode is not "invert everything"
Each theme has its own curated token set. The codegen produces distinct values per theme. Never compute dark colors by inverting or dimming light colors at runtime.
// ❌ Never compute dark variants
final darkBg = Color.lerp(tokens.surface, Colors.black, 0.3);
// ✅ Use the dark theme's own tokens
DuskmoonApp(tokens: .sunshine, darkTokens: .moonlight) // moonlight has its own curated values
Rule 14: Package Boundaries
(Architecture rules — same as above, renumbered for continuity)
What goes where
| Package | Contains | Does NOT contain |
|---|---|---|
duskmoon_theme |
DmDesignTokens, DmTheme, DmPlatformStyle, toMaterial() / toCupertino() / toFluent() adapters |
Any widgets, any BuildContext-dependent rendering |
duskmoon_widgets |
DuskmoonApp, DmAdaptiveWidget, all Dm* widgets |
Token definitions, theme adapters |
duskmoon_theme_bloc |
DmThemeBloc, DmThemeCubit for runtime theme switching |
Widget implementations |
duskmoon_settings |
Settings UI widgets built on adaptive dispatch | Theme internals |
duskmoon_feedback |
Feedback/bug-report widgets | Theme internals |
duskmoon_ui |
Umbrella — re-exports all above | No unique code |
❌ NEVER add duskmoon_widgets as dependency of duskmoon_theme
This creates a circular dependency. If duskmoon_theme needs to reference a widget concept, use an abstract interface or callback, not a concrete widget import.
Rule 15: Code Engine Integration
duskmoon_code_engine has zero dependency on duskmoon_theme. The theme adapter lives as an extension method in duskmoon_theme:
// In duskmoon_theme — NOT in duskmoon_code_engine
extension DmCodeEngineTheme on DmDesignTokens {
CodeEditorTheme toCodeEditorTheme() => CodeEditorTheme(
background: surface,
foreground: onSurface,
// ...
);
}
Rule 16: Codegen Pipeline
duskmoon-dev/design YAML
→ Bun/TypeScript emitter
→ CSS (duskmoonui consumption)
→ TypeScript (duskmoonui/duskmoon-elements)
→ Dart (flutter_duskmoon_ui — committed, CI never needs Bun)
→ JSON (documentation/tooling)
Generated Dart files are committed to git. CI must never require Bun or Node to build the Flutter packages.
❌ NEVER hand-edit generated files
Files in packages/duskmoon_theme/lib/src/generated/ are produced by codegen. Edit the YAML source in duskmoon-dev/design and re-run the pipeline.
Rule 17: Accessibility
- All color pairings must meet WCAG 2.1 AA contrast (4.5:1 normal text, 3:1 large text)
- Every interactive Dm* widget must support keyboard navigation
- Semantic labels required on all icon-only buttons
- Focus indicators must be visible on all themes
Rule 18: Testing Patterns
Widget tests must verify all three platforms
for (final style in DmPlatformStyle.values) {
testWidgets('DmButton renders on $style', (tester) async {
await tester.pumpWidget(
DuskmoonApp(
tokens: DmDesignTokens.sunshine,
platformStyle: style,
home: const DmButton(label: 'Test'),
),
);
expect(find.text('Test'), findsOneWidget);
});
}
Theme tests must verify token resolution
testWidgets('DmTheme.of resolves correct tokens', (tester) async {
late DmDesignTokens resolved;
await tester.pumpWidget(
DuskmoonApp(
tokens: DmDesignTokens.sunshine,
home: Builder(builder: (context) {
resolved = DmTheme.of(context).tokens;
return const SizedBox();
}),
),
);
expect(resolved.primary, equals(DmDesignTokens.sunshine.primary));
});
Quick Reference: Anti-Patterns
| ❌ Don't | ✅ Do |
|---|---|
MaterialApp(theme: tokens.toMaterial()) as root |
DuskmoonApp(tokens: ...) as root |
DmDesignTokens.sunshine.primary in widget build |
DmTheme.of(context).tokens.primary |
Color(0xFF...) or Colors.blue |
tokens.primary / tokens.secondary |
Platform.isIOS for widget dispatch |
Extend DmAdaptiveWidget |
Hand-edit generated/ Dart files |
Edit YAML source, re-run codegen |
Import duskmoon_widgets from duskmoon_theme |
Keep dependency direction strict |
Put theme adapter in duskmoon_code_engine |
Extension method in duskmoon_theme |
primaryContainer bg + onSurface text |
Pair primaryContainer + onPrimaryContainer |
AppBar background = surface |
AppBar background = primary (DuskMoon convention) |
Drawer/side menu bg = primary |
Drawer/side menu bg = secondary (navigation chrome distinction) |
Card default bg = primaryContainer |
Card bg = surfaceContainerLow / surface + outline |
| Shadows as primary depth cue | Surface container tokens for elevation hierarchy |
Color.lerp(x, Colors.black, 0.3) for dark mode |
Use the dark theme's own curated tokens |
| Multiple FABs on one screen | One FAB max; use DmActionList for multiple |
Icon button without semanticLabel |
Always provide semanticLabel on DmIconButton |
Selection highlight with primary |
Selection states use secondaryContainer |
| Inventing colors outside the token system | Promote token role (primary→secondary→tertiary) |
Checklist for Code Review
When reviewing Flutter code that uses DuskMoon UI, verify:
Architecture:
- App root is
DuskmoonApp, notMaterialApp/CupertinoAppdirectly - Package imports flow downstream only (theme → widgets → settings)
- No generated files were hand-edited
- No
duskmoon_themedependency induskmoon_code_engine - Adaptive widgets extend
DmAdaptiveWidget, not manual platform checks
Color & Tokens:
- All color values come from
DmTheme.of(context).tokens, not hardcoded - Background/foreground token pairs match (primary↔onPrimary, etc.)
- No
Colors.*orColor(0x...)literals - Dark mode uses separate theme tokens, no runtime color computation
Component Design:
- Buttons use
DmColorRoleconvention (primary=main CTA, error=destructive, etc.) - AppBar and BottomNav use
primary/onPrimarydefaults - Drawer and side menu use
secondary/onSecondarydefaults - Cards use surface container tokens, not primary/secondary containers for default bg
- Selection states use
secondaryContainertokens - Surface elevation via container tokens, not shadow-only
- Maximum one FAB per screen section
-
DmActionListfor multiple actions, not ad-hoc button rows - Snackbar uses inverse tokens
- Dialog scrim uses
scrimtoken with alpha
Accessibility:
- Semantic labels on all icon-only buttons
- Focus indicators visible on all themes
- Widget tests cover all three
DmPlatformStylevalues - WCAG AA contrast on all bg/fg pairings
More from gsmlg-dev/code-agent
elixir-architect
Use when designing or architecting Elixir/Phoenix applications, creating comprehensive project documentation, planning OTP supervision trees, defining domain models with Ash Framework, structuring multi-app projects with path-based dependencies, or preparing handoff documentation for Director/Implementor AI collaboration
17flutter-reducing-app-size
Measures and optimizes the size of Flutter application bundles for deployment. Use when minimizing download size or meeting app store package constraints.
17flutter-animating-apps
Implements animated effects, transitions, and motion in a Flutter app. Use when adding visual feedback, shared element transitions, or physics-based animations.
17flutter-managing-state
Manages application and ephemeral state in a Flutter app. Use when sharing data between widgets or handling complex UI state transitions.
17flutter-handling-concurrency
Executes long-running tasks in background isolates to keep the UI responsive. Use when performing heavy computations or parsing large datasets.
16flutter-caching-data
Implements caching strategies for Flutter apps to improve performance and offline support. Use when retaining app data locally to reduce network requests or speed up startup.
16