Skip to content

feat: type scale, status tokens and one KPI card anatomy (#691 phase 1) - #693

Merged
hyoshi merged 2 commits into
mainfrom
feat/691-type-scale
Aug 22, 2026
Merged

feat: type scale, status tokens and one KPI card anatomy (#691 phase 1)#693
hyoshi merged 2 commits into
mainfrom
feat/691-type-scale

Conversation

@hyoshi

@hyoshi hyoshi commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What

Phase 1 of #691: type scale, semantic status colours, and one KPI card anatomy. A restyle — no layout structure, DOM hierarchy or wording changed. Deltas and sparklines are phase 4 and wait on the daily-history schema.

Operator feedback was "nothing stands out, everything is too small". Measured, that was structural: 107 of 116 font-size declarations were ≤15px, the headline figures were 15px over 10px labels, and eleven font-weight values crowded the 500–700 band.

1. Type scale

Six steps, three weights, stated once on :root:

token value used for
--type-title 20 / 700 page title
--type-kpi 30 / 700 a figure an operator is meant to read
--type-figure 20 / 700 the same figure in a 230px client-card track
--type-heading 16 / 600 section heading
--type-card-title 14 / 600 card title
--type-body 14 / 400 body
--type-caption 12 / 400 caption

105 literal font-size declarations now reference the scale — none are left in the file:

was count
10, 10.5, 11, 11.5, 12, 12.5px --type-caption-size 62
13, 13.5, 14, 15px --type-body-size 38
16px --type-heading-size 2
18, 21, 22px --type-title-size 3

Weights collapse to 400 / 600 / 700: 500 ×2, 520 ×3, 540 ×1, 550 ×3, 560 ×2, 620 ×2, 640 ×5, 650 ×2, 660 ×4 all become 600; the seven 700s stay.

The client-card figure is 20px, not 30px — it sits in a 230px grid track four across, and the mockup draws the two steps differently (.client-figs .val 20/700 vs .kpi .value 30/700). Flagging because the brief said 30px for reports-client-kpi-value; I followed the mockup as the stated source of truth.

Font stack leads with "Inter", "Noto Sans JP" then the system stack. No @import, no <link>, no CDN — offline behaviour is unchanged; the named faces are used only if the machine already has them.

Monospace off figures: removed from 13 rules, replaced by font-variant-numeric: tabular-nums on the body so digits still align. Kept on the 3 rules that really are code — .dashboard-env-name, .dashboard-env-value, .connector-url-row code. (The brief said 5 monospace sites; there were 18, of which 3 are legitimately code.)

2. Status tokens, and red narrowed to act-now

--status-alert / --status-watch / --status-ok with -bg and -line variants, in both themes. Which state a thing is in is still decided by reports_triage.js and carried as .is-attention / .is-watch / .is-ok; the CSS only says what those look like.

Red audit — every previously-red rule, classified:

rule was now why
.reports-triage, .reports-client-card.is-conflicted, .reports-client-card-mark, .report-card-conflict, .report-card-stale, .report-card.is-conflicted, .reports-client-card-fresh.is-stale, .report-card-fresh.is-stale, .report-chip.is-danger, .reports-client-card-item.is-triaged red --status-alert withholds a figure while it stands — act now
.nav-badge-update, .about-update-outdated red --status-watch an update available is something to look at
.landing-legacy-notice, .report-card-not-collected, .report-chip.is-warn warn tint --status-watch-* advisory
.mark-no red --muted "not configured" is a state, not a problem — most installs leave most of these off
.wizard-step-pill.not-done red tint --surface-2 / --muted progress, not failure
.btn-danger (+ hover, focus), .landing-legacy-notice button, .reports-client-archive:hover red kept --danger a destructive action, which is a different convention

Two palettes now, deliberately apart: --danger = destructive action, --status-alert = act-now state.

Colour never carries meaning alone — and did not need fixing to keep that property. Verified rather than assumed, at each site: the health badge sets textContent from dashboard.reports_health_<state>; the filter dot is followed by a label and a count; the triage dot is followed by a text tag chip; statusMark emits a ✓/✗ glyph; the card's left rule is paired with the status badge in its head. No decoration added — there was no colour-only signal to fix.

3. One card anatomy

Caption → figure, 4px apart, on all three KPI families (.reports-client-kpi, .reports-kpi, .report-card-headline — previously 1px, 3px and 2px gaps). clientKpiCell built its cell figure-first, so the two families on the index read in opposite orders; two appendChild calls swap. Same nodes, same parent, same class names.

4. Dark theme

Both :root and the prefers-color-scheme: dark block carry the full status set. This also fixes a dark-theme bug: the amber watch badges hard-coded #7a5a12 as a foreground — a light-only value, unreadable on a dark surface. Now a token. Swept the file for other hard-coded hex outside the token blocks: the 28 remaining are #fff foregrounds on accent fills, the toast pair, and provider brand colours (Google #4285f4, Meta #1877f2, …), none of them status colours.

Test changes: one, and it is a coverage fix

tests/js/reports_index_height.test.js computes a height estimate by reading box metrics out of app.css. Several are now stated as tokens one level up, which its px() helper could not parse — and the failure mode was the bad one: ten of its assertions stopped running rather than failing (367 → 357 collected). px() and line() now resolve a single var(--token) against :root, which is what "read the metric out of the stylesheet" means once the stylesheet has tokens. One level only, deliberately.

No font-size, weight or colour pin needed updating — the sweep found none beyond this. Its height budgets (TOP_BUDGET 900, TOTAL_BUDGET 2200) are unchanged and still pass at the larger type, which is the meaningful check that the page did not blow up.

Verification

  • node --test tests/js/*.test.jstests 367, pass 367, fail 0 — same count as origin/main, so no test silently dropped.
  • pytest tests/ -q14 failed, 9844 passed, 8 skipped in 409.12s — the same 14 pre-existing failures as origin/main, name for name. Zero new failures.
  • Web-asset group → 371 passed.
  • black --check mureo/ tests/ → 742 files unchanged. ruff check mureo/ tests/ → All checks passed. node --check on the touched JS → clean.
  • Residual checks: 0 literal font-size values left, 0 occurrences of #7a5a12, --font-mono on exactly 3 rules.

Capture verification is not done here — per the process, the rendered screenshots are taken and reviewed before merge.

Refs #691

Phase 1 of #691. Operator feedback on the reports screens was that nothing
stands out and everything is too small. Measured, the complaint was
structural rather than a matter of taste: 107 of 116 `font-size` declarations
in app.css were 15px or under, the headline figures were 15px sitting over
10px labels, and eleven distinct `font-weight` values crowded the 500-700
band. Nothing on the page had more emphasis than anything else because
nothing could.

A type scale
------------
Six steps and three weights, stated once as custom properties on :root and
referenced everywhere:

  --type-title    20/700   the page title
  --type-kpi      30/700   a figure an operator is meant to READ
  --type-figure   20/700   the same figure in a 230px client-card track
  --type-heading  16/600   a section heading
  --type-card-title 14/600
  --type-body     14/400
  --type-caption  12/400

All 105 remaining literal `font-size` declarations now reference it — the
file has none left. The eleven weights collapse to 400 / 600 / 700: a 640
next to a 660 is not a distinction anybody can see.

Figures are proportional now, not monospace. The typewriter texture came off
thirteen rules and `font-variant-numeric: tabular-nums` moved to the body, so
digits still line up in a column. The three rules that really are code — the
env-var name and value, the connector URL — keep their monospace.

Status tokens, and red narrowed to act-now
------------------------------------------
--status-alert / --status-watch / --status-ok, each with a background and a
border variant, in both themes. Which state a thing is in is still decided by
reports_triage.js and carried in the markup as .is-attention / .is-watch /
.is-ok; this only says what those look like.

Red was doing too many jobs. An update being available and an install being
out of date are things to LOOK at, so they are amber now. "Not configured"
and "wizard step not done" are states rather than problems — most installs
leave most of those off on purpose — so they are neutral. What is left red is
what withholds a figure or costs money while it stands.

`--danger` survives, narrowed to what it always meant on a button: a
destructive action (Clear All, Remove, Archive). Two palettes, kept apart.

Colour never carries meaning alone, and did not need fixing to keep that
property: every element these paint already ships a word or a glyph beside it
(the health badge's text, the filter chip's label, the triage row's tag, the
✓/✗ mark). Verified rather than assumed — no decoration was added.

One card anatomy
----------------
Caption, then figure, 4px apart, on all three KPI families. The client card
built its cells figure-first, so the two families on the index read in
opposite orders and a reader scanning a column had to work out which caption
belonged to which number. Same nodes, same parent, same classes — only the
order two appendChild calls run in.

Also a dark-theme bug
---------------------
The amber "watch" badges hard-coded `#7a5a12` as their foreground, a
light-only value that was unreadable on a dark surface. It is a token now.

Scope
-----
A restyle: no layout structure, no DOM hierarchy, no wording changed. Deltas
and sparklines are phase 4 and wait on the daily-history schema.

Test changes
------------
One, and it is a coverage fix rather than a pin update: the height model in
tests/js/reports_index_height.test.js reads box metrics straight out of
app.css, and several are now stated as tokens one level up. Its `px()` helper
resolves a single `var()` so it keeps reading the stylesheet rather than a
literal duplicated into the test. Without it ten of its assertions did not
fail — they silently stopped running.

Refs #691
@hyoshi
hyoshi merged commit 4c9a1c5 into main Aug 22, 2026
13 checks passed
@hyoshi
hyoshi deleted the feat/691-type-scale branch August 22, 2026 03:59
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