Skip to content

Widget visual styling: ADR-0006 + icon {source, name} schema - #36

Merged
jonocodes merged 13 commits into
mainfrom
widget-visual-styling-adr
Jul 20, 2026
Merged

jonocodes merged 13 commits into
mainfrom
widget-visual-styling-adr

Conversation

@jonocodes

Copy link
Copy Markdown
Owner

Implements the design decisions from #21 (grilled) and records them in ADR-0006.

What's here

  • ADR-0006 — widget visual styling: presentation relays opaquely through the daemon (amends ADR-0003), color is raw-hex background-only, icon is a {source, name} dispatch resolved against a client-owned renderer registry, one fixed style, per-kind look is hard-coded client CSS, uploads deferred.
  • Icon schema migration (structure only): icon changes from a bare string to {source, name} on both the daemon (new Icon pydantic model) and client (protocol.ts). All 6 shipped layouts migrated. Daemon validates non-empty strings and relays opaquely — it does not enumerate valid sources.

Follow-up (tracked in #21, not in this PR yet)

  • Real glyph rendering (bundle Lucide + Simple Icons, wire the source dispatch, unknown-source placeholder) — being added on this branch next.
  • Reconcile icon names against Lucide's actual vocabulary (close → x, refresh → refresh-cw, etc.).
  • Inter font bundling; global show-labels toggle.

Also split off #35 (extensible / user-defined control kinds) from the Q4 discussion.

Verification

  • 151 daemon tests pass (server suite loads the real layouts/ files through the new schema).
  • Client typechecks clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm

jonocodes and others added 13 commits July 19, 2026 11:26
Records the design decisions from the #21 grilling: presentation
(color, icon) relays opaquely through the daemon (amending ADR-0003),
color is raw-hex background-only, icon becomes a {source, name} dispatch
resolved against a client-owned renderer registry, the client bundles
Lucide + Simple Icons, one fixed style (no light/dark), per-kind look is
hard-coded client CSS with color button-only, and image/font-set uploads
are deferred. Extensible custom control kinds split off to #35.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
The widget `icon` field changes from a bare string to a {source, name}
object: `source` selects a client-side icon renderer, `name` is the glyph
within it. The daemon validates both are non-empty strings and relays them
opaquely — it does not enumerate valid sources (client owns that registry).

- daemon: new `Icon` pydantic model; `Widget.icon: Icon | None`
- client: `Icon` type in protocol.ts; `Widget.icon` updated
- client: ButtonGrid renders `icon.name` as text for now — the dispatch to
  real Lucide / Simple Icons glyphs lands with the renderer work in #21
- JogHandle drops its unused `icon` field (jogstrips ignore icons)
- all shipped layouts migrated to the {source: lucide, name: ...} form
- README documents the new icon shape

Structure-only step: no icon libraries bundled yet. Icon names are carried
over verbatim and will be reconciled against Lucide's actual vocabulary
when the renderer is added. 151 daemon tests pass; client typechecks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Wires the {source, name} icon dispatch to actual glyphs via a new
client/src/Icon.tsx renderer:

- source: lucide -> Lucide component, resolved kebab-case -> PascalCase.
  Bundled whole (~198 KB gz) into the main chunk; renders synchronously.
- source: simple-icons -> brand path from the Simple Icons set, imported
  lazily as a single on-demand chunk (~2.1 MB gz) the first time a layout
  references a logo. Never fetched unless a brand icon is actually used.
- unknown source / unresolved name -> visible dashed placeholder.

Also:
- ButtonGrid renders the glyph above the label (icon-first reads better).
- Jogstrip "||" placeholder -> Lucide chevrons-up-down (the Q4 mark).
- style.css sizes the svg glyphs; drops the ::before "||" pseudo-element.
- Reconcile layout icon names to Lucide's real vocabulary: close->x,
  columns->columns-2, media-play->play, paste->clipboard-paste,
  refresh->refresh-cw, rows->rows-2, window->app-window. All 17 names now
  resolve to real components (verified programmatically).
- ADR-0006 corrected: the "~0.5 MB bundle both whole" estimate was wrong;
  records the Lucide-whole / Simple-Icons-lazy split and real sizes.

Client typechecks and builds; 151 daemon tests pass. Per ADR-0003, React
visuals are not covered by the test suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Demo mode: a ``?demo=<name>`` URL param (firefox / default / showcase)
renders a fixture ServerLayout with the WebSocket disabled, so the client
can be viewed, screenshotted, or design-iterated without a running daemon.
The showcase fixture exercises both renderers and the edge cases in one
view (Lucide glyphs, per-button colour, lazily-loaded Simple Icons brand
logos, a no-icon button, and an intentionally-unknown icon placeholder).

Build: split the icon sets into named chunks — ``lucide`` (eager, ~148 KB
gz) and ``simple-icons`` (lazy, ~2.1 MB gz, fetched only on brand-logo
use) — and lift chunkSizeWarningLimit above them so the size warning stays
meaningful signal instead of a permanent false alarm. App chunk is ~50 KB
gz; initial load (app + lucide) ~198 KB gz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Icon sizing: the .icon svg selector never matched because Lucide/Simple
Icons render the <svg> *with* the .icon class (not a child), so Lucide fell
back to its intrinsic 24px while Simple Icons svgs (viewBox, no width/height)
ballooned to fill the cell. Size the .icon element directly so every source
renders in one consistent box.

ESLint: flat config (typescript-eslint + react-hooks + react-refresh),
`npm run lint` script. Two experimental react-hooks rules fired:
- react-hooks/refs on JogStrip/Trackpad's deliberate sync-ref-during-render
  idiom (latest prop in a stable pointer callback) — disabled that one rule
  rather than rewrite working input logic; kept rules-of-hooks/exhaustive-deps.
- react-hooks/set-state-in-effect on Icon.tsx's SimpleBrandIcon — genuinely
  cleaned up: resolution is now keyed by name and set only in the async
  callback, no synchronous setState reset in the effect.

lint + tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Two backend-free design surfaces (issue #21 "Both"):

Responsive gallery — `npm run dev` then /gallery.html renders the real
client in phone / large-phone / 7" / 10"-tablet iframes at once, with
layout + orientation selectors, for checking a layout across screen sizes.
Dev-only entry (root gallery.html), not in the production build. Reuses the
existing demo fixtures via a new DEMO_LAYOUTS export.

Ladle — `npm run ladle` serves ButtonGrid / Icon / JogStrip stories in
isolation with width/theme controls. Vite-native, Storybook-compatible CSF.
.ladle/components.tsx wraps stories in the app's dark surface + CSS. Stories
in src/*.stories.tsx.

Also: README documents demo mode, gallery, Ladle, and lint under a new
"Design tooling" section.

tsc + lint clean; production build verified unchanged (dev files excluded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Text on a button is now opt-in: a widget with a `label` shows it, one
without is icon-only. The id remains only as a last-resort fallback when a
widget has neither label nor icon, so it isn't a blank unidentifiable
button. Replaces the previous `label ?? id` which always showed text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Inter: self-host Inter (woff2, weights 400/600/700 — the ones the UI uses)
via @fontsource, imported by every entry point (app, gallery, Ladle) and set
as the primary family ahead of the system stack, for a consistent typeface
across devices. Browsers fetch only the ~5 KB latin subset they need.

Ladle device sizes: ButtonGrid gains an optional `orientation` prop that
overrides the window-based auto-detect (the live app leaves it unset). New
Surface stories render the grid in fixed device-dimension frames
(phone/tablet, landscape + portrait) so a fixed-size parent simulates a
resolution — and, via the override, the portrait transpose — right in Ladle.

tsc + lint clean; Inter confirmed loaded (document.fonts.check); production
build emits the woff2 subsets and excludes the dev-only stories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
A client-side "Content size" multiplier scales grid content (button icon +
label, in-grid jogstrip) on top of the responsive base, so the user can
dial readability per device.

- settings-store.ts: content-scale setting (min 0.75, max 2.5, step 0.1,
  default 1.0) with clampContentScale and the same URL>localStorage>default
  read precedence as the other sliders; key `deckd.contentScale`, param
  `contentScale`.
- App.tsx: set `--content-scale` on the `.surface` layout area only, so grid
  content scales while the persistent chrome (sibling jogstrip, bottom bar)
  stays fixed.
- style.css: grid-content sizes become `calc(base * var(--content-scale))`;
  `:root` defaults the var to 1 (keeps chrome and Ladle stories at base);
  cells get `overflow: hidden` so scaled content clips cleanly.
- Settings.tsx: "Content size" slider under Display.

Default 1.0 reproduces the current look. Verified: content scales, chrome
stays fixed, slider works. tsc + lint clean; 151 daemon tests pass. Design
and acceptance criteria in #37; per ADR-0003 React visuals aren't unit-tested
(verified via the demo/gallery instead).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
Shrink the button label base from clamp(13px,2.4vw,18px) to
clamp(11px,2vw,15px) so the icon reads as the primary element with the
label as a compact caption. The content-size multiplier (#37) still stacks
on top, so anyone wanting larger text can dial it up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
- Reduce button label base again to clamp(9px,1.5vw,12px) — the label is a
  small caption under the icon; the content-size multiplier still scales it.
- The chrome jogstrip "scroll" hint now reads vertically (writing-mode:
  vertical-rl), fitting the tall narrow strip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
…ize docs

- Reframe from "Linux desktop" to cross-platform: note deckd runs on GNOME
  (Wayland), KDE Plasma (Wayland), X11, and macOS (each with its own focus
  watcher / input backend). The per-platform setup sections already existed;
  the top-line framing was the gap.
- Add a hero screenshot (docs/screenshot.png) — a Firefox layout with icons,
  per-button colour, and the persistent scroll strip.
- Document the visual system in "What works today" (bundled Lucide + Simple
  Icons via icon: {source, name}, per-button colour) and add the Content
  size slider (0.75x-2.5x) to Client tuning + the contentScale URL param.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx5giKP22zuCBmQE2YcPPm
@jonocodes
jonocodes merged commit add526f into main Jul 20, 2026
2 checks passed
@jonocodes
jonocodes deleted the widget-visual-styling-adr branch August 4, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant