-
Notifications
You must be signed in to change notification settings - Fork 1
DesignSystem
The design system behind the write-once components: a complete token/component specification (authored with Claude Design, preserved in-repo at docs/design/Photon-Design-System.dc.html — the source of truth) implemented as typed C# tokens in eQuantic.UI.Primitives and consumed by every realization target.
-
Colors — every color is a paired light/dark
ColorToken; components never hold raw colors. Interactive variants resolve five sub-tokens (Base,OnBase,Pressed— a real token, not an overlay —Subtle,OnSubtle). Disabled is not a color: it's a 38% opacity group. -
Type scale — role-driven (
Display…BodyM,Caption,Label) with Dynamic Type clamps per role, plus the dense end every desktop chrome lives in:TitleSmall(15/700 — the rung betweenLabel13 andTitle20),LabelSmall(11 — status rails, inspector read-outs, specimen captions) andOverline(10/800, tracked — the uppercase eyebrow over a group). A scale that stops at 12dp is a touch scale; a sidebar, a toolbar and a status bar all live below it, and without these rungs every one of them comes out a fifth too large. A style also carries its face:TypeStyle.Monoselects the monospaced one (the system's own — SF Mono on a Mac), which the measurer, the rasterizer and the raster cache all honour because they already key on the style. -
Line boxes follow the size —
TypeStyle.WithSize(size)scales the line height by the same ratio (CSS's unitlessline-height), andTypeStyle.OfSize(size, weight)derives one at the typographic 1.25×. Patching only the size leaves a bigger glyph in the old box, which is how a descender ends up outside its line. -
Spacing (
Space.S1–S16, 4dp base, gap-owned — no margin exists), radius (Radius.Xs–Full, engine-clamped), icon sizes (§07 whitelist 16/20/24/32 — arbitrary sizes throw), touch (≥48dp hit contract), elevation (one analytic shadow per node), motion (durations + curves + spring).
Density (Comfortable | Compact) is a property of the target, never of the call site: an app never asks for a smaller button on the Mac; the Mac asks for a denser app. It is Material's density and Apple's control size, and it is what keeps ONE tree honest on a phone and on a desktop.
Sizing.Height, PaddingX, LabelSize and HitTarget resolve by it (Small 32→26, Medium 40→32, Large 48→40, XLarge 56→48), and so does the selection ladder (SwitchWidth/SwitchHeight/SwitchThumb/SwitchTravel, SelectionBox, RadioDot). Under a pointer the hit target stops inflating — the §08 minimum is a finger's contract, and on a toolbar of 26dp buttons those invisible margins would overlap each other.
It reaches components through ComponentContext.Density, the same door the theme comes through, so no Build signature changed. Who decides is the target: the macOS shell says Compact; the web boot reads (pointer: fine).
The ladder lives in the token layer, always. Four components once kept a private copy of their own size table (
Avatar,IconButton,Switch,Checkbox/RadioGroup) — and a copy cannot follow anything: they stayed thumb-sized after the window went dense.Sizing.Avatar(size)and the selection rungs exist so a caller reads the same number the component draws.
- Token values are pinned; WCAG contrast is recomputed in tests for every claimed pair.
- Component metrics come from the spec tables (e.g. Button's size table: 32/40/48/56 heights) and are asserted on every axis: C# web realizer pins, native golden images (light + dark), transpiled-fixture execution in vitest.
- The style resolver rules (derived Outline/Ghost/Link variants, pressed-as-token-swap, focus double-ring) live in target-neutral C# (
ButtonStyles).
Client-side artifacts carrying design-system values are generated from the C# single source and byte-pinned in CI (a drift fails the build; env-var flows regenerate):
| Artifact | Generator | Regenerate with |
|---|---|---|
Normative stylesheet (custom properties, .eq-type-*, .eq-elevation-*, motion vars) |
PhotonCssGenerator |
(pure function — tested per value) |
design-system.generated.ts (tokens, theme object, Button size table) |
DesignSystemTsGenerator |
EQ_UPDATE_DESIGN_TS=1 |
icons.generated.ts (glyph path data) |
IconTsGenerator |
EQ_UPDATE_ICONS_TS=1 |
| Embedded transpiled component modules | live eqc output | EQ_UPDATE_TRANSPILED=1 |
The same rule powers hydration: SSR (C# realizer) and client lowering (TS) are held byte-identical by cross-pinned literals in both test suites.
A curated Icons enum (16 glyphs and growing) with the path data living once in the C# IconRegistry (24×24 single-path alpha masks): the web emits inline <svg fill="currentColor"> (the tint rides the color token exactly like text), the TS side consumes the generated module, and the future native glyph atlas rasterizes from the same registry. Outline ↔ filled are distinct glyphs; icons ignore Dynamic Type.
Authoring is 100% typed C# (BoxStyle, StyleDiff, tokens) — there is no CSS plane. Three laws govern the web realization:
- One semantics — the same style vocabulary drives dp values on Photon and CSS on web.
-
No expressiveness ceiling — Wrap, Grid, AspectRatio, AlignSelf, group Opacity, static Transform2D, Hover/Focus state diffs, window-size-class adaptivity (
AdaptiveNode), Sticky, ZIndex, both-axis scroll. -
No rewrite by construction — every regular declaration lowers to ONE deduplicated atomic class (
eq-+ FNV-1a hash ofprop:value); theme colors referencevar(--eq-color-*); SSR collects the page's rules into a single<style id="eq-atomic">the client adopts by identity (hydration = class equality, enforced end-to-end by the e2e identity test). ~118 rules / 4.8 KB covered the entire showroom.
Pseudo-states lower to pseudo-variant atomic rules (:hover/:focus-visible — zero JS); size classes to fixed media-gate classes. The C# StyleAtomizer and the TS style-atomizer.ts are byte-identical twins (cross-pinned fixture).
One interface (colors as light/dark ColorToken pairs, type roles, shape scale, elevation specs, disabled opacity), selected via AddUI(...).UseTheme(...) and bridged SSR→client.
Switching light/dark at run time is a SERVICE: IThemeController (Mode + Apply) is taken through a constructor like any capability, and the component never learns which target answered — the native runner repaints the window against the other palette, the browser flips one color-scheme declaration (the whole web palette is authored as light-dark() pairs, so the swap costs no re-render). ITextClipboard is offered the same way for a page's own Copy button. Two implementations ship: PhotonTheme (the default) and MaterialTheme (real Material 3 — MaterialTheme.Instance baseline or FromSeed(color) dynamic color via a CAM16/HCT port validated against Google's values). Shape is theme-driven; the same component tree rebrands with one line.