spry-conversion.md 11 KB

Spry conversion conventions (goal 41, binding for T3–T10)

ClankerSaddle runs on the Spry CSS framework that ships inside libspry. This document is the binding contract for every page task in the UI/UX goal: what Spry provides, what app.css owns, how classes map from the old stylesheet, and the rules for new markup.

How the framework is loaded

main.html and login.html link, in this order:

  1. spry.css — core + default theme (light palette, dark palette on prefers-color-scheme: dark). This IS the product default; no data-theme is set anywhere by default.
  2. spry-theme-{airy,dwarf,formal,glassy,h2o,living-sky,modern}.css — the seven optional themes, loaded but INERT until <html data-theme="slug"> is set. T2 wires the account-settings picker that sets that attribute; until then they cost a few KB of cache and do nothing.
  3. app.css — the app component layer, always LAST so it wins ties.

Color mode is data-mode="auto" on <html> (spry's default behavior — omitting the attribute is also auto). Never hard-code a mode.

The theme slot contract (T2 — the account-settings picker)

The user's saved appearance lives in TWO places with ONE source of truth:

  • Server side: the user_preferences table (M0011) stores theme ("" = the core default, or one of the seven slugs) and mode (auto|light|dark) per spry_users.id — see Services.UserPreferenceService. This is the durable record the login seeds from.
  • Client side: the DEVICE-scope Statum slot "ui" (public data {theme, mode}) — written by SetThemeAction on save and re-seeded by LoginAction at login. DEVICE scope means the browser caches it in localStorage, so a returning visit paints the chosen theme before any server round-trip (no FOUC beyond the very first visit, which sees the Spry default — itself the default).

app.js's applyUiSlot hook reads the slot on statum init and after every render cycle and does the only client-side work there is: data-theme="slug" on <html> when a theme is set (attribute REMOVED for the core default — no data-theme="core"), and data-mode="light|dark" when forced (always present; auto is the default value). No other JS, stylesheet swaps or reloads — the seven theme stylesheets are always linked and inert until data-theme scopes them.

Per-account on a per-device slot: two humans sharing a browser profile each get their own choice once they log in; a single user on two devices picks per device (the DEVICE slot is written on every save and every login of that user, so the LAST login on a device wins). Deleting the slot client-side (statum.clear('ui')) reverts to the Spry default until the next login.

Where styles live — the layering contract

  • spry.css owns every primitive: typography, links, focus rings, buttons (button, .btn, .btn-primary/danger/outline/ghost/link, .btn-sm/lg/block, .btn-group), forms (input/select/textarea element styles, .field, .label, .hint, .is-invalid/valid, .choice(s)(-inline), .switch), tables (table, .table-wrap, .table-striped/hover/compact/borderless), cards (.card, -flat/-header/-body/-footer/-title/-subtitle), alerts (.alert
    • -danger/success/warning/info), badges (.badge + -danger/success/warning/info/muted), progress/meter/spinners, dialogs (<dialog>, .dialog-sm/lg/titlebar/title/close/body/footer, [popover], .popover-menu, .anchor/.popover-anchor), expanders (details/summary, .expander-group), hero/avatar/otp/ drawer/pagination, topbar/nav/menu/submenu/breadcrumb/tabs, layout utilities (.container(-sm/-lg), .stack, .cluster, .center, .with-sidebar, .block/inline/hidden/visually-hidden, width/ position/overflow utilities), spacing utilities (.{m,p}{t,r,b,l,x,y}-{0..5}, .mx-auto), and text utilities (.text-{xs..2xl}, .text-muted/accent/danger/success/warning/info, .fw-*, .mono, .num, .truncate, .clamp-2/3, .measure, .lead, .small).
  • app.css owns only ClankerSaddle components: status/event badges (the .badge-* family below), the goal identity, comments and footnote treatment, blocks + plan review, the event log, advisor chat, the terminal, toasts, the quote popover, the login shell, .cards grid, .grid-2, and chrome shims (.brand-emblem, .nav-auth, .nav-user, .site-footer).
  • Never restyle a Spry primitive from app.css and never re-declare Spry's reset/typography. If a page needs something Spry has, USE the Spry class; only add to app.css when the component is genuinely app-specific.

app.css append-only discipline

app.css is organised in sections with ==== header ==== comments. Within a section, new rules are APPENDED under the section header with a /* comment */ naming the component and the reason (which page, which plan/goal). Never edit another task's rules in place — merge conflicts in this file are append-per-section and resolved trivially. Sections may be added at the END of the file.

Class mapping (old app.css → Spry)

old class use instead
body 64rem column .container on <main> (72rem, fluid padding)
.form-row .field (label + input inside)
button.primary .btn-primary
button.approve .btn-primary
button.reject .btn-danger
form.inline .cluster (wrap the fields/buttons)
label.inline (checkbox row) .choice
.muted .text-muted
.error (text color) .text-danger
details custom bare details/summary (Spry styles them)
.card .card (Spry) + .card-body etc. as needed
header custom chrome .topbar + .topbar-brand + ul.nav/.has-submenu/ul.submenu
.nav-menu/.nav-dropdown deleted — see topbar row above
tables bare <table> (+ .table-hover/.table-compact where dense) inside .table-wrap when wide

Kept as app components (now tokenised): .comment, .footnote, .block-card, .plan-review, .plan-popover, .review-banner, .goal-*, .log-*, .event-*, .term-*, .terminal, .chat-*, .toast*, .quote-*, .ssh-key, .badge-* status family, .cards, .grid-2, .status-head, .status-corner, .waiting-drafts, .rule-card, .chip-remove, .yeehaw-*.

App tokens: --goal: var(--spry-accent) and --goal-surface: var(--spry-accent-soft) (goal identity derives from the theme accent; goal pages read as themselves through layout and badges, not a competing hue). All other old app tokens (--ink, --line, --surface, --accent, --success, …) are DELETED — use the --spry-* equivalents.

The badge-colour contract

Every status/health/event badge consumes a semantic Spry variable — NO hard-coded hex anywhere. A .badge-<status> rule in app.css maps to exactly one of:

  • muted (--spry-bg-alt bg, --spry-fg-muted fg): draft, paused, cancelled, offline, reference, command, created, shell-reset, mode-implement, and their goal-* twins.
  • queued (--spry-warning-soft/--spry-warning): queued, enqueued.
  • info (--spry-info-soft/--spry-info): assigned, planning, started, resumed, dispatch, query, question, user-comment, co-developed, mode-investigate, chat-awaiting_answer, kind-task, state_change.
  • warning (--spry-warning-soft/--spry-warning): working, blocked, block, suspended, review_requested, review, completion_review, mode-review, busy, chat-running, suggestion-pending, yeehaw, and the block-raised family.
  • success (--spry-success-soft/--spry-success): done, idle, active, complete, completed, progress, mode-test, chat-idle, suggestion-approved, and the answered family.
  • danger (--spry-danger-soft/--spry-danger): failed and every rejected/failed goal transition.
  • accent (--spry-accent-soft/--spry-accent): plan, plan_review, rfc, mode-orchestrate, block-plan, admin, capability, kind-goal, goal-plan-submitted. (The old violet plan token retired: plans and goals now ride the theme accent.)
  • neutral inset (--spry-bg-inset/--spry-fg-muted): comment, tool.
  • Health badges are FILLED (strong var as bg, --spry-accent-fg fg) so they read at a glance on the saddles page.

New statuses in page tasks: add ONE line to the matching group in the badge section of app.css, never a new hex.

Markup rules for page tasks (T3–T10)

  1. Page shell: every page's content sits directly under <main class="container">; page-level stacks use .stack or .stack-like sections with spacing utilities. Detail pages may adopt .with-sidebar from T5/T6 for side navigation.
  2. Spacing comes from Spry utilities (.mt-3, .p-2, .gap-* via .cluster/.stack), not bespoke margins. If you write margin: … in app.css, you need a reason comment.
  3. Every colour via --spry-* (or an app token derived from one). Dark mode must work with zero extra rules — if you find yourself writing @media (prefers-color-scheme), stop: you hard-coded something.
  4. Buttons: the element default is already Spry-styled; add .btn-primary for the one primary action per view, .btn-danger for destructive, .btn-ghost for quiet/inline actions (the old button.inline), .btn-sm inside dense rows/tables.
  5. Forms: <div class="field"><label…>…</label><input…> <p class="hint">…</p></div>. Checkboxes/radios use .choice / .choices(-inline); toggles use .switch. Wide editors (textareas) live in .fields capped by .form-rows .field { max-width: 34rem } — group a page's edit fields in .form-rows.
  6. History folds / actions surfaces: per the goal plan, the collapsed-history expander pattern and "actions surface" reshaping (modals via native <dialog> with .dialog-titlebar/-title/ -close/-body/-footer) arrive WITH the page tasks (T3–T9), not in this foundation. When you add one: <dialog class="…" id="…"> with a .dialog-titlebar carrying <p class="dialog-title"> and a .dialog-close button, body in .dialog-body, actions right- aligned in .dialog-footer.
  7. Expanders: plain <details>/<summary> (Spry styles them; group siblings in .expander-group to fuse borders).
  8. Badges: <span class="badge badge-<status>"> per the contract above; event kinds on the log/narrative use the same semantic groups via .event-kind-* in app.css.
  9. Wide tables sit in .table-wrap; dense tables add .table-compact, row-hover .table-hover.
  10. Login is the reference for form markup conventions (src/pages/login.html).

The breadcrumb partial

src/pages/breadcrumb.html is a pstm-fragment partial (the progress_feed pattern) that renders a Spry .breadcrumb from a crumbs array of {href?, label} items — the last crumb carries aria-current="page". Include it as:

<pstm-fragment name="breadcrumb" crumbs="page.crumbs"/>

Adoption by detail pages happens in T5/T6/T9 (it needs each page's entrypoint to expose a crumbs vector — do not fake it client-side).