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.
main.html and login.html link, in this order:
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.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.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 user's saved appearance lives in TWO places with ONE source of truth:
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."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.
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)..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).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.
| 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.
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:
--spry-bg-alt bg, --spry-fg-muted fg): draft, paused,
cancelled, offline, reference, command, created, shell-reset,
mode-implement, and their goal-* twins.--spry-warning-soft/--spry-warning): queued,
enqueued.--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.--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.--spry-success-soft/--spry-success): done, idle,
active, complete, completed, progress, mode-test, chat-idle,
suggestion-approved, and the answered family.--spry-danger-soft/--spry-danger): failed and every
rejected/failed goal transition.--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.)--spry-bg-inset/--spry-fg-muted): comment,
tool.--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.
<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..mt-3, .p-2, .gap-*
via .cluster/.stack), not bespoke margins. If you write
margin: … in app.css, you need a reason comment.--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..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.<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.<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.<details>/<summary> (Spry styles them;
group siblings in .expander-group to fuse borders).<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..table-wrap; dense tables add
.table-compact, row-hover .table-hover.src/pages/login.html).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).