Skip to content

fix(ui): one stylesheet vocabulary, layered CSS, and an executable design-system guard - #572

Merged
smunini merged 9 commits into
mainfrom
fix/543-stylesheet-unification
Aug 19, 2026
Merged

fix(ui): one stylesheet vocabulary, layered CSS, and an executable design-system guard#572
smunini merged 9 commits into
mainfrom
fix/543-stylesheet-unification

Conversation

@angela-helios

Copy link
Copy Markdown
Contributor

Closes #543. Five commits, one per phase, in the order the issue lays out.

1. Collapse the duplicate vocabularies

.btn, .data-table, .page-head__title win; bulk-import.html, bulk-import-detail.html, tenants.html (and the dashboard's stray .page-title) migrate onto them; .button / .button--primary / .page-title are deleted; the dead class="table" becomes the .table-wrap > .data-table structure the other tables use, with .data-table__empty rows. The duplicate .data-table and .notice definitions are merged into single blocks that preserve today's computed cascade-merge exactly.

A layer-aware duplicate sweep then found four more pairs the issue didn't list — .pane, .addbox__actions, .tag, and the .tabs / .tab / .tab:hover trio (history's underline tabs and the compartment viewer's pill tabs shared names). Same treatment: literal declaration-order merges, zero visual change, one definition each.

2. Layer the stylesheet

@layer tokens, base, components, pages declared once; every rule wrapped in place (no rule moved, so the diff is reviewable). Conflict resolution is now by layer — a page rule outranks a component rule deliberately, never by accident of specificity or file position.

3. Make it executable — design-system.spec.ts

  • No undefined classes: every class on every page must match a rule in app.css or a dot-selector in shipped script (JS hooks like .json-line--foldable count; createElement("table") does not — a dead class="table" still fails).
  • Canonical <h1>: exactly one per page, always .page-head__title.
  • One primary button: computed height/radius/background/color must be identical across all pages.
  • No duplicate selectors: postcss walk; @layer does not license a duplicate, a different @media block does.

The guard's first run earned its keep: 18 dead classes (removed from templates), .toolbar__title/__count used-but-never-defined (Import section headers rendered as browser-default h2 — now the .card-head h3 scale), and one real primary-button divergence (.nl-input .btn--primary at 44px — metric overrides dropped).

4. Close the opt-out

The route lists no-cdn.spec.ts and a11y.spec.ts each carried privately are now one export in e2e/pages/routes.ts. /ui/bulk-import joins the list (it had escaped every guard); the detail page has no static URL, so the guards seed a submission and cover it too. A new page added there is covered by axe (both themes), no-CDN, and the design-system guard at once.

5. Write it down + make the right path easy

Component-vocabulary table and the layering contract in crates/ui/README.md, a condensed version in the work-with-ui skill, a "no second vocabulary" rule in the must-NOT list, and templates/pages/_scaffold.html as the copy-me starting point (unreferenced, so Askama doesn't compile it).

Verification

  • cargo test -p helios-ui green at every phase.
  • Full Playwright suite locally: 123 passed — including the new guard, a11y light+dark over the extended route list, nojs, and both auth legs.
  • Every merge in phases 1–2 preserves the current computed styles by construction (declaration-order concatenation of what already cascade-merged). The only intended visual changes are the ones the issue asks for: Import/Tenants pages on the canonical title/button/table treatment, Import section headers at card-head scale, and the NL Ask button at standard primary metrics.

…lasses

The Import and Tenants pages carried a second spelling for every primitive:
.button vs .btn, .page-title vs .page-head__title, and a class="table" that
matched no rule at all. .data-table and .notice were each defined twice, so
what rendered was the cascade-merge of two authors' blocks.

Canonical spellings win (.btn, .data-table, .page-head__title). The duplicate
.data-table / .notice definitions are merged into single blocks that preserve
today's computed result exactly, the retired classes are deleted, and the
bulk-import tables gain the .table-wrap / .data-table structure the other
tables already use.
@layer tokens, base, components, pages — declared once at the top, then each
existing block wrapped in place so the diff stays reviewable. Rules keep their
file positions; what changes is conflict resolution: a page rule now outranks
a component rule regardless of specificity or source order, and the shared
vocabulary (button, card, table, field, notice, pill, menu, page-head,
toolbar) is separated from page-specific styling by layer rather than by
convention.
Beyond the .data-table / .notice pairs the issue named, a duplicate-selector
sweep found four more: .pane, .addbox__actions, .tag, and the .tabs / .tab /
.tab:hover trio (history's underline tabs and the compartment viewer's pill
tabs shared class names). Each pair rendered as its cascade-merge already, so
every merge concatenates the two declaration blocks in source order — the
computed result is unchanged, there is just one definition to find now.
… guards

design-system.spec.ts makes the vocabulary executable: every class used on
every page must match a rule in app.css (this is what would have caught
class="table"), every <h1> must be the canonical .page-head__title, primary
buttons must resolve to identical computed metrics on every page, and no
selector may be defined twice (postcss walk, layer-aware: @layer does not
license a duplicate, @media does).

The route lists no-cdn.spec.ts and a11y.spec.ts each carried privately are now
one export in pages/routes.ts — a new page added there is covered by every
cross-page guard at once, instead of opted out of each by default. The
bulk-import pages join the list; the detail page has no static URL, so the
guards seed a submission and cover it too.
…d it

A vocabulary table and the layering contract in crates/ui/README.md, a
condensed version in the work-with-ui skill, a 'no second vocabulary' rule in
the must-NOT list, and templates/pages/_scaffold.html as the copy-me starting
point — so the default path for a new page is the canonical one, for humans
and agents alike.
…imary button

The design-system guard's first run over every page found the tail the issue
predicted: 18 classes matching no rule (chart-legend__type, nav-panel__name,
builder-section, query-results__open, modal__pane, the history/editor page
wrappers, jt, col-created, backlink, mono, and friends) — all removed from the
templates; .json-line--foldable stays because shipped script selects it, which
the guard now recognizes (dot-classes inside JS string literals count as
load-bearing, so a dead class="table" still fails).

.toolbar__title / .toolbar__count were used but never defined — the Import
section headers rendered as browser-default h2. They now match .card-head h3.
And the one real primary-button divergence, .nl-input .btn--primary at 44px,
drops its metric overrides: every primary button on every page now resolves
identically, which the guard holds from here on.
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Main brought Bulk Export (#538), the dashboard job cards (#565), and the menu
close behaviors (#566). The vocabulary work carries over both export pages,
which predate #543: canonical title/button classes, the job-status chips move
onto the .tag status-pill family (main's new .chip block collided with the
facet-chip component — exactly the duplicate this branch exists to kill), the
typegrid joins the pages layer, the job-card additions join the components
layer, and both export routes join the shared guard list.

Also makes the design-system guard read app.css and the shipped scripts over
HTTP from the server under test instead of the source tree: the CI runner
drives a packaged binary with no checkout alongside it (the ENOENT on every
merge-ref run), and in HFS_E2E_BASE_URL mode the running server is the only
truth worth checking anyway.
… backlink

The guard's script scan read only the landing page's script tags, but pages
ship their scripts individually — editor.js never loads on /ui, so its
.json-line--foldable hook read as undefined. The sweep now collects every
script each route actually ships and legitimizes across the union. And the
export pages' vestigial .backlink goes the way bulk-import's did.
@smunini
smunini merged commit 8467a99 into main Aug 19, 2026
19 checks passed
@smunini
smunini deleted the fix/543-stylesheet-unification branch August 19, 2026 17:34
angela-helios added a commit that referenced this pull request Aug 19, 2026
…ubscriptions-ui

Union merge: both export routes join the guard list alongside the
subscriptions page, the locale catalogs keep both new key blocks, and the
.tag status-pill family carries the subscription states next to the export
job states — one vocabulary, as #572 intends.
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.

UI: unify the stylesheet approach so pages can't diverge (Import vs Resources style drift)

2 participants