unified-design-system · component spec

Components

One decision, one sentence of why, per component — grounded in real screens (diet-guard, dufs-cloud, screen-locker), not invented examples. Where a component isn't built yet, that's stated rather than faked.

Button 01 live

diet-guard/app/lib/screens/meal_builder_screen.dart

Radius: radius-sm, 8px. Same token as inputs and chips — one family of interactive elements, not a size picked for buttons alone.

Padding: 24px / 12px. Horizontal is exactly 2× vertical (rule 22) — never tuned per component.

Label: 16px, body. A button is read, not decorative chrome — never below the 16px floor.

Fill only, no shadow. Dark surfaces never carry a shadow — weight comes from the fill itself.

Secondary
Disabled

Fill vs. border-only is the only weight distinction — never two filled buttons on one screen, so Log meal (primary) and Add item (secondary) read as different weight at a glance.

✓ Fixed this session: label is on-fill (#211d1b, dark), not near-white — near-white measured ≈3.2:1 on this fill, below the 4.5:1 floor. Dark text passes 5.8–9.7:1 on all four semantic fills; it's a closed rule, not a per-color pick.
PropertyValueRule
Radius8pxshared with inputs/chips
Padding12px 24pxrule 22 — 2× ratio
Label16px on-fillrule 20 floor + on-fill
Depthfill / border onlyshadow policy
Disabledopacity 0.5no separate muted token

Card 02 live

diet-guard/app/lib/widgets/day_status_calendar.dart

✓
!

Radius: radius-md, 12px. One step up from a button's 8px — a card contains interactive elements, so it must read as a larger unit than they are.

Elevation: fill step, not shadow. surface-1 against the page background — dark surfaces never use a shadow for depth.

Nested radius: 6px. Rule 24 — inner cell radius = outer (12px) − the gap to its edge (≈6px), not a reused constant.

Status cells use on-fill. The ✓/! cells sit directly on success/danger — this is the one place in the app that already got the on-fill rule right.

The status colors sit at mid brightness, so ink gives strong on-palette contrast — this is the real code comment in day_status_calendar.dart, written before today's on-fill token existed to name the pattern.

Input 03 live

diet-guard/app/lib/widgets/macro_input_row.dart · screen-locker/_ui_widgets.py

Not yet themed. No InputDecorationTheme is set in theme.dart — every TextField renders M3's default indigo focus color, off-palette.

Tkinter side too. gatelock's tk.Entry sets bg/fg but no relief/bd — Tk's default sunken bevel border, not the flat line-dark outline.

⚠ Open finding, not fixed: the input shown above is the target spec (radius-sm, flat 1px border, no bevel) — the actual rendered app still falls back to each toolkit's own default. Queued with the pilot-code palette migration, same class of bug as the Chip finding below.
PropertyValueRule
Radius8pxshared with buttons/chips
Border1px line-darkflat, no bevel
Label12px captionchrome, not read content
Fill on focussurface-1 → surface-2fill step, not a shadow/glow

Chip 04 live

diet-guard/app/lib/widgets/slot_selector_row.dart

✓9am 🍎 Snack

Fill/border at 15%/40% alpha of the status color. Not a fixed chip palette — logged/due/upcoming reuse success/danger/muted, so a chip's meaning matches the calendar's.

Bug, still open: the Snack chip sets no backgroundColor/side at all, so it falls through to Flutter's own default — a literal pure-white #FFFFFF border, the one banned value in the whole system.

The hour chips (left) do this correctly: icon at 72% opacity of the status color (rule 28), selected border 2px vs. unselected 1px at 40% alpha. Snack just needs the same three properties applied — not a new pattern, a missed one.

Dialog 05 live

dufs-cloud/web/src/index.css · screen-locker/_manual_workout_dialog.py

Web (floating over content)
Delete 3 files?
Cancel Delete
Tkinter (the whole screen)
Log Manual Workout
SUBMIT

Same radius (radius-md), different depth technique, and that's correct, not inconsistent: the web dialog floats over other content (shadow allowed, per policy), the screen-locker dialog fills the entire lock screen — there's nothing behind it to cast a shadow onto, so it's fill/border only, same as every other dark surface.

PropertyValueRule
Radius12pxradius-md, one step above buttons
Web shadow0 4px 8px rgba(ink,.16)level 2 — bigger than a dropdown's level 1
Actionsright-aligned, 8px gapspace-2 on the spacing scale

Switch 06 spec only

no live usage yet — locking the values in before the first one gets built

Track = accent, thumb = on-fill. Same fill/on-fill pairing as a button — a switch is a tiny filled control, not a new color relationship to invent.

Fully rounded track. The one place in this system a pill shape is correct: an on/off track has no other elements to stay visually consistent with, unlike a button.

Off state: track becomes surface-2, thumb becomes muted — a fill-step change, matching how every other "inactive" state in this system works (never a separate grey palette).

Divider 07 live

diet-guard/app/lib/screens/settings_screen.dart · dufs-cloud/web/src/index.css

dufs-cloud (correct)
.topbar
diet-guard (open finding)
const Divider()
⚠ Open finding, not fixed: settings_screen.dart uses const Divider() with no dividerTheme set app-wide — same missing-theme pattern as Input above, this time landing on Material's default grey rather than line-dark.
PropertyValueRule
Weight1pxhairline — never a filled bar
Colorline-dark / line-lightsame border token as cards/inputs
Spacingspace-3 (16px) each sidenever touches adjacent text

Checkbox 08 live

dufs-cloud/web/src/index.css — .tile-select

Chip sits on a scrim, not a fill. rgba(ink,0.45) over a photo thumbnail — this is on-scrim territory, not on-fill, since the thumbnail behind it varies.

Open finding: the checkbox itself is an unstyled native <input type="checkbox"> — its checked-state color is the browser's default blue, off-palette. No custom checkbox styling exists yet.

The 8px-radius scrim chip behind it is deliberately correct (matches radius-sm, dark translucent per the scrim rule) — only the checkbox glyph inside it is unstyled.

Snack bar / toast 09 spec only

no live usage yet — no repo has a toast/snackbar today

Upload failed — connection lost Retry

Never danger-filled. A toast is transient chrome, not a status cell — surface-2 + danger-colored icon/text reads as urgent without becoming a full alert box.

One action max. Retry here, styled as text, not a button — a toast disappears on its own; a second filled button would compete with the page's real primary action.

Copy above is realistic (a dufs-cloud upload failure), not a placeholder — written to spec the tone (states what broke, offers the one useful next step) alongside the visual values.