fix(ui): one stylesheet vocabulary, layered CSS, and an executable design-system guard - #572
Merged
Merged
Conversation
…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 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
approved these changes
Aug 19, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #543. Five commits, one per phase, in the order the issue lays out.
1. Collapse the duplicate vocabularies
.btn,.data-table,.page-head__titlewin;bulk-import.html,bulk-import-detail.html,tenants.html(and the dashboard's stray.page-title) migrate onto them;.button/.button--primary/.page-titleare deleted; the deadclass="table"becomes the.table-wrap>.data-tablestructure the other tables use, with.data-table__emptyrows. The duplicate.data-tableand.noticedefinitions 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:hovertrio (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, pagesdeclared 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.tsapp.cssor a dot-selector in shipped script (JS hooks like.json-line--foldablecount;createElement("table")does not — a deadclass="table"still fails).<h1>: exactly one per page, always.page-head__title.@layerdoes not license a duplicate, a different@mediablock does.The guard's first run earned its keep: 18 dead classes (removed from templates),
.toolbar__title/__countused-but-never-defined (Import section headers rendered as browser-defaulth2— now the.card-head h3scale), and one real primary-button divergence (.nl-input .btn--primaryat 44px — metric overrides dropped).4. Close the opt-out
The route lists
no-cdn.spec.tsanda11y.spec.tseach carried privately are now one export ine2e/pages/routes.ts./ui/bulk-importjoins 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 thework-with-uiskill, a "no second vocabulary" rule in the must-NOT list, andtemplates/pages/_scaffold.htmlas the copy-me starting point (unreferenced, so Askama doesn't compile it).Verification
cargo test -p helios-uigreen at every phase.nojs, and both auth legs.