Skip to content

DesignSystem

Edgar Mesquita edited this page Aug 8, 2026 · 4 revisions

Photon Design System

The design system behind the write-once components: a complete token/component specification (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.

Token layers (§01–§09)

  • 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 (DisplayBodyM, Caption, Label) with Dynamic Type clamps per role, plus the dense end every desktop chrome lives in: TitleSmall (15/700 — the rung between Label 13 and Title 20), LabelSmall (11 — status rails, inspector read-outs, specimen captions) and Overline (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.Mono selects 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 sizeTypeStyle.WithSize(size) scales the line height by the same ratio (CSS's unitless line-height), and TypeStyle.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.S1S16, 4dp base, gap-owned — no margin exists), radius (Radius.XsFull, 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 — the same button, 32dp under a thumb and 26dp under a mouse

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. A component never keeps a private copy of its own size table — a copy cannot follow the density the target asks for. Sizing.Avatar(size) and the selection rungs exist so a caller reads the same number the component draws.

Spec fidelity is tested, not aspired to

  • 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).

The "generated, never hand-written" rule

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.

Icons pipeline (spec A10)

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.

Styling without CSS — the atomic engine

Authoring is 100% typed C# (BoxStyle, StyleDiff, tokens) — there is no CSS plane. Three laws govern the web realization:

  1. One semantics — the same style vocabulary drives dp values on Photon and CSS on web.
  2. 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.
  3. No rewrite by construction — every regular declaration lowers to ONE deduplicated atomic class (eq- + FNV-1a hash of prop:value); theme colors reference var(--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).

Theming = providing an IAppTheme

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.

Clone this wiki locally