Instruction file imported from anilcancakir/uptizm (
.github/instructions/design.instructions.md). Copyright stays with the author.
Design Rules (UI surface)
These rules apply whenever you touch any file under lib/. They complement .github/copilot-instructions.md with the implementation-level specifics.
Atomic Component Folder Contract
Every component in the uptizm app lives in a 4-file atomic folder:
lib/ui/components/<name>/
<name>.dart # class <Name> extends StatelessWidget, @immutable
<name>.recipe.dart # WindRecipe or WindSlotRecipe
<name>.preview.dart # ONE preview widget rendering all variant x state combos
index.dart # exports <name>.dart + <name>.recipe.dart (NOT preview)
- Folder and file names are
lower_snake_case(dotted suffixes.recipe.dart/.preview.dartare valid). - Component class name is unprefixed
UpperCamelCase(card.dart->class Card). index.dartexports the component class, variant enums, and the recipe. Never export the preview file (it is dev-only).previews:refreshdiscovers components by scanning*.preview.dartfiles. One preview class per file, no exceptions.
To scaffold: dart run bin/dispatcher.dart make:component <Name> [--variants=intent,size] [--slots]
WindRecipe Usage
All variant logic lives in a WindRecipe (or WindSlotRecipe for slot-based components):
final myRecipe = WindRecipe(
base: 'flex items-center gap-2',
variants: {
'intent': {
'primary': 'bg-primary text-on-primary',
'secondary': 'bg-surface-container text-fg border border-color-border',
'ghost': 'text-fg',
},
'size': {
'sm': 'px-3 py-1.5 text-xs',
'md': 'px-4 py-2 text-sm',
'lg': 'px-5 py-2.5 text-base',
},
},
defaultVariants: {'intent': 'primary', 'size': 'md'},
);
- Emission order is always:
base ++ variant (definition order) ++ compound ++ caller. Never sort or deduplicate. - Pass variant values as strings matching the map keys. Pass
nullto clear a default. - The caller
classNameargument appends last; it can override variant output at the same granularity. - Import
WindRecipeviapackage:magic/magic.dartinside component files (it re-exports the wind barrel). Directpackage:fluttersdk_wind/...imports tripdepend_on_referenced_packages.
Token-Only Rule
All colors go through semantic alias keys defined in DESIGN.md. Never use raw hex, Color(0xFF...), or Colors.* in component or view code.
This is the one design rule with a gate behind it: bin/check's design-tokens job scans lib/**/*.dart for Color(0x and Colors., strips comments first, and exits non-zero outside four allowlisted paths that each carry their reason. It is a regex and not an AST, so Color.fromARGB, Color.fromRGBO and Color.from walk straight past it. They are violations all the same; the job being quiet is not a licence.
The 17 semantic alias keys:
| Key | Role |
|---|---|
bg-surface |
Page background |
bg-surface-container |
Card, panel background |
bg-surface-container-high |
Input background, nested panels |
text-fg |
Primary text |
text-fg-muted |
Secondary text |
text-fg-disabled |
Disabled/meta text |
bg-primary |
Brand action background |
text-on-primary |
Text on brand action surface |
bg-primary-container |
Tinted brand surface |
bg-accent |
Secondary accent |
border-color-border |
Dividers, card borders |
border-color-border-subtle |
Hairline borders |
bg-destructive |
Danger action background |
text-on-destructive |
Text on danger surface |
bg-destructive-container |
Tinted danger surface |
bg-success |
Success tone |
bg-warning |
Warning tone |
Each alias already expands to a '<light> dark:<dark>' pair (lib/config/wind_theme.g.dart shows 'bg-surface': 'bg-[#F9FAFB] dark:bg-[#07090C]'), so write bg-surface on its own. Adding dark:bg-surface is nonsense. An explicit dark: is only ever needed for a raw arbitrary value, which the rule above already bans.
To add or change a semantic token: edit DESIGN.md and run dart run bin/dispatcher.dart design:sync.
Preview-Required Rule
No component ships without a preview widget.
- The preview file (
<name>.preview.dart) must render every variant x state combination so the catalog shows the full range. - After adding or modifying a preview, regenerate the catalog:
dart run bin/dispatcher.dart previews:refresh - Verify dark/light parity by navigating to
/previewin debug mode:./bin/fsa dusk:navigate --route=/preview - Take light and dark screenshots and run the
component-visual-revieweragent (a Claude Code agent definition; Copilot has no equivalent, so apply those criteria by hand) before marking a component ship-ready.
Material Import Discipline
Component files that share a name with a Material widget (Card, Switch, Badge, Tooltip, Checkbox, etc.) must import Flutter as:
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart' show Icons; // only if icons are needed
Never import 'package:flutter/material.dart' without show. Build exclusively on Wind W-widgets inside component bodies.
Anti-Patterns
Each of these is a blocker, and exactly one of them is measured: bin/check's design-tokens job fails the first row. The other seven are reviewer-enforced, so a green bin/check says nothing about them and the component-visual-reviewer agent is what flags them.
| Anti-pattern | Correct approach |
|---|---|
Color(0xFF...) or Colors.* in component code |
A semantic alias key from the table above |
An OFF-scale pixel value (SizedBox(height: 13)) |
Wind spacing utilities on the 4px scale. A SizedBox(width: 8) between a glyph and its label is on-scale and stays: the rule is the grid, not the widget |
| A one-off widget when a library component exists | Check docs/component-registry.md first |
Icons.* inline in a component body |
Extract as static const IconData _icon = Icons.x; |
Several preview classes in one .preview.dart |
One preview class per file |
CSS-only Wind utilities (box-shadow, filter, transform, group-*) |
Unsupported in wind; use Flutter animation APIs |
Hand-editing lib/config/wind_theme.g.dart |
dart run bin/dispatcher.dart design:sync |
| Shipping a component with no preview | Add the preview, run previews:refresh |
Casing and number marks are locale DATA
Turkish is a first-class language here, and two Dart primitives ignore the
locale. String.toUpperCase() maps i to I where Turkish needs İ, and
toStringAsFixed writes a full stop where Turkish writes a comma and groups
thousands with the stop. Both produced live defects: ÇALIŞAN IZLEYICILER on the
dashboard, %99.9 on an SLO target.
Use upperCase() and formatDecimal() from lib/app/support/formatters.dart
for anything a person reads. formatDecimal routes its whole part through
formatCount, so pass grouped: false where the caller does its own magnitude
handling; the metric formatter is the one case.
Wind's uppercase utility calls the locale-aware transform as of wind 1.5.1, so
the className is correct on its own again and is the default for new code. The
existing upperCase() wrappers stay: they are idempotent over a correct result
(İ.toUpperCase() is İ), and reverting fifteen files for a byte-identical
render is not worth a PR. Drop a wrapper only in a file you are opening anyway.
Reach for upperCase() directly on any path that is not a WText.
Two places to reach past Wind, and why
Wind's flex makes children greedy, which is correct for a row of equals and wrong in two shapes that recur in this app. Both are settled; do not re-derive them as bugs.
- A glyph beside text that must wrap uses plain Flutter
Row(crossAxisAlignment: CrossAxisAlignment.start)with anExpandedaround the text. Wind flex overflows here because the text never yields. - A row of buttons that must reflow uses
Wrapwithspacing/runSpacingrather than a flex row, so a narrow column drops the second button to its own line instead of overflowing.
The registry is generated
docs/component-registry.md is written by bin/sync-registry from lib/ui/components/, lib/ui/layouts/ and lib/preview/, and bin/check's registry job fails when it is out of date. Run the script after adding or changing a component; never edit the file. It was hand-maintained once and drifted into documenting magic_starter's library instead of this app's, which is worse than having no registry because the file is trusted.
Release Boundary
Preview files (*.preview.dart) and the generated _previews.g.dart are dev-only. They are excluded from release builds through the magic_devtools dev-package boundary and kDebugMode/kReleaseMode const-fold. Never import a preview file from production code.