flutter-duskmoon-design
Original:🇺🇸 English
Translated
Use when writing Flutter code that uses flutter_duskmoon_ui packages, when creating or modifying widgets that consume DmTheme or DmDesignTokens, when setting up theming in a DuskMoon app, when building adaptive widgets, or when reviewing code for DuskMoon design compliance. Also trigger when the user mentions duskmoon_theme, duskmoon_widgets, DuskmoonApp, DmTheme, DmAdaptiveWidget, DmDesignTokens, DmPlatformStyle, or any Dm* widget prefix. This skill defines the rules and patterns that ALL code touching the DuskMoon Flutter ecosystem must follow.
3installs
Sourcegsmlg-dev/code-agent
Added on
NPX Install
npx skill4agent add gsmlg-dev/code-agent flutter-duskmoon-designTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →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. must never import from . must never import from .
duskmoon_themeduskmoon_widgetsduskmoon_widgetsduskmoon_settingsWidget 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 CupertinoButtonRule 1: Theme Setup
Correct App Root
dart
// ✅ 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
dart
// ❌ NEVER wrap MaterialApp/CupertinoApp directly
void main() {
runApp(MaterialApp(
theme: DmDesignTokens.sunshine.toMaterial(), // wrong — skips DmTheme
home: MyHomePage(),
));
}Why: injects above the platform app. Without it, returns null and all Dm* widgets fail to resolve tokens.
DuskmoonAppDmThemeDmTheme.of(context)Rule 2: Accessing Design Tokens
Always use DmTheme.of(context)
DmTheme.of(context)dart
// ✅ 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
dart
// ❌ 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 , platform brightness, and possible ancestors. Only returns the correct resolved set.
ThemeModeDmThemeOverrideDmTheme.of(context)Exception — static adapter methods
DuskmoonAppThemeDataDmThemedart
// Inside DuskmoonApp.build() — acceptable
MaterialApp(theme: DmTheme.staticToMaterial(resolvedTokens));Rule 3: Color System
Token Structure (61 color tokens per theme)
| Group | Tokens | Usage |
|---|---|---|
| Primary | | Main brand actions, primary CTAs |
| Secondary | | Supporting actions, alternative CTAs |
| Tertiary | | Accent highlights, badges, special UI |
| Error | | Error states, destructive actions |
| Surface | | Backgrounds, cards, elevation |
| Outline | | Borders, dividers |
| Inverse | | Snackbars, contrast overlays |
| Scrim/Shadow | | Modal overlays, elevation shadows |
| Semantic | | Status indicators |
Color Format: OKLCH
All colors are defined in OKLCH in the source CSS. The Dart codegen converts to objects via inline OKLCH→sRGB math (zero external deps).
Color❌ NEVER hardcode color values
dart
// ❌ 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:
dart
// ✅ 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 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
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 layerRule 4: Available Themes
5 themes defined in , codegen'd to Dart:
duskmoon-dev/design| Theme | Mode | Primary Character |
|---|---|---|
| light | Warm amber/gold |
| dark | Cool blue/lavender |
| dark | Deep blue/teal |
| light | Natural green/earth |
| light | Warm orange/rose |
Access via , , etc.
DmDesignTokens.sunshineDmDesignTokens.moonlightRule 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 :
DmAdaptiveWidgetdart
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
dart
// ❌ 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.
dart
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 resolves tokens identically:
DmColorRole| DmColorRole | Background | Foreground | Container | On Container |
|---|---|---|---|---|
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
Size scale
| DmSize | Horizontal padding | Vertical padding | Font scale |
|---|---|---|---|
| 8 | 4 | 0.75rem (12) |
| 12 | 6 | 0.875rem (14) |
| 16 | 8 | 0.875rem (14) |
| 24 | 12 | 1rem (16) |
| 32 | 16 | 1.125rem (18) |
Rule 7: Component Design — Actions
DmButton
Default: , ,
variant: filledcolor: primarysize: md| Variant | Background | Foreground | Border | Use case |
|---|---|---|---|---|
| role color | onRole | none | Primary CTAs, main actions |
| transparent | role color | role color | Secondary actions, cancel |
| transparent | role color | none | Tertiary/inline actions, links |
| roleContainer | onRoleContainer | none | Soft emphasis, toggles |
Color role assignment convention:
| Action type | Color role | Example |
|---|---|---|
| Main CTA, save, submit, confirm | | "Save Changes" |
| Alternative action, secondary flow | | "Export", "Share" |
| Accent action, special highlight | | "Watch Demo", "Premium" |
| Destructive, delete, remove | | "Delete Account" |
| Neutral, dismiss, low emphasis | | "Cancel", "Skip" |
dart
// ✅ 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 .
semanticLabeldart
DmIconButton(
icon: Icons.delete,
color: DmColorRole.error,
semanticLabel: 'Delete item',
onPressed: () {},
)DmFab (Floating Action Button)
- Default color: — the single most important action on the screen
primary - Surface: background,
primaryContainericononPrimaryContainer - Rule: Maximum one FAB per screen. If you need multiple actions, use .
DmActionList
dart
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 |
dart
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 | | Brand presence, top-level identity |
| Title text | | Contrast on primary |
| Icon buttons | | Consistent with primary surface |
| Bottom border | none (primary fills) | Clean branded bar |
Scrolled/elevated state: Background transitions to , text to .
primaryContaineronPrimaryContainerdart
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 to fall back to /.
color: DmColorRole.neutralsurfaceonSurfaceDmBottomNav
| Element | Token |
|---|---|
| Background | |
| Selected icon/label | |
| Unselected icon/label | |
| Selected indicator | |
| Top border | none (primary fills) |
Rule: 3–5 destinations maximum. Labels always visible (not icon-only).
DmTabBar
| Element | Token |
|---|---|
| Background | |
| Selected tab | |
| Unselected tab | |
| Indicator | |
DmDrawer
| Element | Token |
|---|---|
| Background | |
| Header area | |
| Selected item bg | |
| Selected item text | |
| Unselected text | |
| Dividers | |
| Scrim (overlay behind drawer) | |
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) | |
| Ancestors (links) | |
| Separator | |
Rule 9: Component Design — Layout & Cards
DmCard
Elevation hierarchy via surface tokens:
| Card style | Background token | Use case |
|---|---|---|
| Flat | | Inline content, no separation |
| Outlined | | List items, settings rows |
| Elevated | | Standard cards |
| Filled | | Emphasized/grouped content |
Interior layout convention:
┌─────────────────────────────────┐
│ [optional media/image] │
├─────────────────────────────────┤
│ Title (onSurface) │
│ Subtitle (onSurfaceVariant)│
│ │
│ Body text (onSurface) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Actions: ghost/outlined btns│ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘dart
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 primary card background with text for regular content cards. Primary/secondary/tertiary containers are for interactive highlights (selected state, feature callout), not default card backgrounds.
filledonPrimaryDmDivider
| Variant | Token | Use case |
|---|---|---|
| Default | | Section separation |
| Strong | | Major section breaks |
DmScaffold
Responsive layout dispatch:
| Breakpoint | Navigation style |
|---|---|
| Compact (< 600) | |
| Medium (600–1200) | |
| Expanded (> 1200) | |
Page body background: . Rail/side nav background: .
surfacesecondaryRule 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: (notification convention),
color: errorsize: smdart
DmBadge(count: 3) // red notification dot
DmBadge(label: 'New', color: .tertiary, variant: .tonal) // soft accent tag
DmBadge(label: 'Draft', color: .neutral, variant: .tonal) // muted statusDmChip
Selectable/filterable labels. Takes .
DmColorRole| State | Background | Foreground | Border |
|---|---|---|---|
| Unselected | | | |
| Selected | | | none |
| Disabled | | | |
Default selection color: — secondary containers are for selection states.
secondaryDmAvatar
| Variant | Background | Foreground |
|---|---|---|
| With image | — | — |
| Initials (default) | | |
| Initials (group variety) | Cycle through | Matching |
Sizes follow enum. Default: (40dp diameter).
DmSizemdDmStat (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 | |
| Label | |
| Positive trend | |
| Negative trend | |
| Card background | |
DmTable / Data Grid
| Element | Token |
|---|---|
| Header row bg | |
| Header text | |
| Body row bg (even) | |
| Body row bg (odd) | |
| Body text | |
| Row hover | |
| Selected row | |
| Border/grid lines | |
| Sort indicator | |
Rule 11: Component Design — Feedback
DmAlert
| Semantic | Background | Foreground | Icon color |
|---|---|---|---|
| Info | | | |
| Success | | | |
| Warning | | | |
| Error | | | |
Convention: Alerts use semantic container tokens with full-width layout. For inline indicators, use .
DmBadgeDmDialog
| Element | Token |
|---|---|
| Scrim (backdrop) | |
| Dialog surface | |
| Title | |
| Body | |
| Confirm button | |
| Cancel button | |
| Destructive confirm | |
DmSnackbar
Uses inverse tokens for contrast against current theme:
| Element | Token |
|---|---|
| Background | |
| Text | |
| Action button | |
DmProgress
Linear and circular variants. Default color: .
primary| Variant | Track | Indicator |
|---|---|---|
| Default | | |
| With color role | | role color |
DmSkeleton
Loading placeholder. Uses with shimmer animation toward .
surfaceContainerHighsurfaceContainerLowRule 12: Component Design — Inputs
DmTextField
| Variant | Idle | Focused | Error |
|---|---|---|---|
| | | |
| | | |
| | | |
| Element | Token |
|---|---|
| Input text | |
| Placeholder/hint | |
| Label (floating) | |
| Helper text | |
| Error text | |
| Prefix/suffix icon | |
Default variant:
outlinedDmCheckbox / DmSwitch / DmSlider
| State | Token |
|---|---|
| Unchecked/off | |
| Checked/on | |
| Track (switch off) | |
| Track (switch on) | |
| Thumb | |
| Slider active track | |
| Slider inactive track | |
| Slider thumb | |
| 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 scale.
DmSizeElevation = surface tokens, not shadows
Use → for visual hierarchy. Shadows ( token) are supplementary, not the primary depth cue.
surfaceContainerLowestsurfaceContainerHighestshadowdart
// ✅ 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.
dart
// ❌ 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 valuesRule 14: Package Boundaries
(Architecture rules — same as above, renumbered for continuity)
What goes where
| Package | Contains | Does NOT contain |
|---|---|---|
| | Any widgets, any |
| | Token definitions, theme adapters |
| | Widget implementations |
| Settings UI widgets built on adaptive dispatch | Theme internals |
| Feedback/bug-report widgets | Theme internals |
| Umbrella — re-exports all above | No unique code |
❌ NEVER add duskmoon_widgets as dependency of duskmoon_theme
This creates a circular dependency. If needs to reference a widget concept, use an abstract interface or callback, not a concrete widget import.
duskmoon_themeRule 15: Code Engine Integration
duskmoon_code_engineduskmoon_themeduskmoon_themedart
// 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 are produced by codegen. Edit the YAML source in and re-run the pipeline.
packages/duskmoon_theme/lib/src/generated/duskmoon-dev/designRule 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
dart
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
dart
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 |
|---|---|
| |
| |
| |
| Extend |
Hand-edit | Edit YAML source, re-run codegen |
Import | Keep dependency direction strict |
Put theme adapter in | Extension method in |
| Pair |
AppBar background = | AppBar background = |
Drawer/side menu bg = | Drawer/side menu bg = |
Card default bg = | Card bg = |
| Shadows as primary depth cue | Surface container tokens for elevation hierarchy |
| Use the dark theme's own curated tokens |
| Multiple FABs on one screen | One FAB max; use |
Icon button without | Always provide |
Selection highlight with | Selection states use |
| 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 , not
DuskmoonApp/MaterialAppdirectlyCupertinoApp - Package imports flow downstream only (theme → widgets → settings)
- No generated files were hand-edited
- No dependency in
duskmoon_themeduskmoon_code_engine - Adaptive widgets extend , not manual platform checks
DmAdaptiveWidget
Color & Tokens:
- All color values come from , not hardcoded
DmTheme.of(context).tokens - Background/foreground token pairs match (primary↔onPrimary, etc.)
- No or
Colors.*literalsColor(0x...) - Dark mode uses separate theme tokens, no runtime color computation
Component Design:
- Buttons use convention (primary=main CTA, error=destructive, etc.)
DmColorRole - AppBar and BottomNav use /
primarydefaultsonPrimary - Drawer and side menu use /
secondarydefaultsonSecondary - Cards use surface container tokens, not primary/secondary containers for default bg
- Selection states use tokens
secondaryContainer - Surface elevation via container tokens, not shadow-only
- Maximum one FAB per screen section
- for multiple actions, not ad-hoc button rows
DmActionList - Snackbar uses inverse tokens
- Dialog scrim uses token with alpha
scrim
Accessibility:
- Semantic labels on all icon-only buttons
- Focus indicators visible on all themes
- Widget tests cover all three values
DmPlatformStyle - WCAG AA contrast on all bg/fg pairings