From 5c52d37121d958fcedc90858a65721e9317571e6 Mon Sep 17 00:00:00 2001 From: Mads Taanquist Date: Wed, 29 Jul 2026 18:29:53 +0200 Subject: [PATCH 1/2] Consolidate the UI onto one set of primitives Adds primitives.css, loaded last, owning the modal, the empty state, the chip remove affordance, and the shared focus ring. Completes .btn (states plus btn-accept), gives .icon-btn a 28px .sm and a .danger tint, gives .seg a .seg-count, and adds the type ramp, --danger-contrast, and a per-theme --select-caret. Migrates the scoped duplicates onto them: the rv-btn family, rv-quick-btn, rb-btn, mini-btn, tool-btn, send-btn and pop-open become .btn/.icon-btn; rtabs, rv-filters, rv-mtabs and revision-filter-chip become .seg; the three hand-rolled modals share one panel; twelve empty-state spellings become .empty-state. Deletes the superseded CSS and the drift sheet's dead blocks. Behaviour is unchanged; e2e selectors follow the renames. Co-Authored-By: Claude Opus 5 --- TODO.md | 19 + e2e/account.spec.ts | 16 +- e2e/core-flow.spec.ts | 7 +- e2e/entity-card.spec.ts | 4 +- e2e/find-and-search.spec.ts | 4 +- e2e/review-entity-card.spec.ts | 2 +- e2e/review.spec.ts | 2 +- e2e/scene-board.spec.ts | 2 +- e2e/scene-title.spec.ts | 4 +- e2e/universe-settings.spec.ts | 2 +- scratch/design-kit/README.md | 2 +- scratch/design-kit/buttons.html | 44 +- scratch/design-kit/empty-states.html | 34 +- scratch/design-kit/modal.html | 100 ++++ scratch/design-kit/seg.html | 9 +- scratch/system-design/design-system.md | 201 +++++-- src/lib/components/AssistantPanel.svelte | 22 +- src/lib/components/CommandPalette.svelte | 127 ++--- src/lib/components/EntityQuickCard.svelte | 4 +- src/lib/components/EntityRelationships.svelte | 2 +- src/lib/components/HelpModal.svelte | 82 +-- src/lib/components/NotesSidebar.svelte | 13 +- src/lib/components/NotificationBell.svelte | 11 +- src/lib/components/PlanSidebar.svelte | 4 +- src/lib/components/RelationshipWeb.svelte | 19 +- src/lib/components/ReviewCommentCard.svelte | 24 +- src/lib/components/ReviewModal.svelte | 135 ++--- src/lib/components/ReviewPanel.svelte | 26 +- src/lib/components/ReviewSceneHead.svelte | 4 +- .../components/ReviewSuggestionCard.svelte | 11 +- src/lib/components/ReviewSurface.svelte | 6 +- src/lib/components/ReviewWorkspace.svelte | 20 +- src/lib/components/RevisionHistory.svelte | 20 +- src/lib/components/RevisionPreview.svelte | 18 +- src/lib/components/SceneBoard.svelte | 9 +- src/lib/components/StoryBoard.svelte | 9 +- src/lib/components/StoryOutline.svelte | 12 +- src/lib/components/StoryPreview.svelte | 6 +- src/lib/editor-mentions.ts | 4 +- src/lib/styles/admin.css | 43 +- src/lib/styles/editor.css | 13 +- src/lib/styles/menus.css | 9 +- src/lib/styles/pages.css | 195 +++---- src/lib/styles/primitives.css | 232 ++++++++ src/lib/styles/review.css | 304 +--------- src/lib/styles/theme.css | 536 +++--------------- src/lib/styles/tokens.css | 20 + src/routes/+layout.svelte | 2 + src/routes/stories/[id]/+page.svelte | 32 +- src/routes/stories/[id]/notes/+page.svelte | 10 +- src/routes/stories/[id]/plan/+page.svelte | 34 +- .../universes/[id]/[[section]]/+page.svelte | 36 +- .../universes/[id]/insights/+page.svelte | 20 +- src/routes/universes/[id]/notes/+page.svelte | 10 +- src/routes/universes/[id]/plan/+page.svelte | 44 +- 55 files changed, 1105 insertions(+), 1475 deletions(-) create mode 100644 scratch/design-kit/modal.html create mode 100644 src/lib/styles/primitives.css diff --git a/TODO.md b/TODO.md index 7b242fd..34fa0d1 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,25 @@ themes, DesignSync-ready), run the briefs in Claude Design, port the results, then the code-side consolidation refactor the sheet's backlog section lists. +Primitives consolidation (2026-07-29, branch +`feat/primitives-consolidation`). The fourth brief's result, ported. +New `src/lib/styles/primitives.css`, loaded last, owns the families that +had no single owner: the modal, the empty state, the chip remove +affordance, and the shared focus ring. `.btn` is now the complete family +(states plus `.btn-accept`), `.icon-btn` gains `.sm` and `.danger`, +`.seg` gains `.seg-count`, and `tokens.css` gains the seven-step type +ramp, `--danger-contrast`, and a per-theme `--select-caret`. Migrated +across the app: the `.rv-btn` family, `.rv-quick-btn`, `.rb-btn`, +`.mini-btn`, `.tool-btn`, `.send-btn` and `.pop-open` onto `.btn`/ +`.icon-btn`; `.rtabs`, `.rv-filters`, `.rv-mtabs` and +`.revision-filter-chip` onto `.seg`; the three hand-rolled modals onto +one primitive; twelve empty-state spellings onto `.empty-state`. About +1,400 lines of CSS deleted, including the drift sheet's dead blocks +(`.ctx-menu`, `.settings-nav`, `.note-card`, `.icon-button`, `.kbd`). +Remaining from the sheet's backlog: the duplicated component blocks +(assistant chat, `.insp-*`, kanban), the residual bare `#fff`, and focus +reachability on the sidebar rows. + Dev environment automation (2026-07-28, branch `feat/dev-env-automation`). Setting the project up on a fresh macOS machine turned up three things CI never sees, all fixed here. diff --git a/e2e/account.spec.ts b/e2e/account.spec.ts index 4ed3df2..0c65027 100644 --- a/e2e/account.spec.ts +++ b/e2e/account.spec.ts @@ -173,7 +173,7 @@ test('assistant tab: gated by the account switch and muted per story', async ({ // With the account on, the Assistant tab shows; opening it reveals the chat. // The click retries: right after the create-story navigation the page may // not be hydrated yet, and a pre-hydration click goes nowhere. - const tab = page.locator('.rtab', { hasText: 'Assistant' }); + const tab = page.locator('.right-head .seg-btn', { hasText: 'Assistant' }); await expect(tab).toBeVisible(); await expect(async () => { await tab.click(); @@ -237,10 +237,10 @@ test('assistant tab: gated by the account switch and muted per story', async ({ // The command palette carries the Assistant's quick actions while it is on. await page.keyboard.press('ControlOrMeta+k'); - await expect(page.locator('.palette')).toBeVisible(); - await expect(page.locator('.palette-item', { hasText: 'Catch me up' })).toBeVisible(); + await expect(page.locator('.modal-panel')).toBeVisible(); + await expect(page.locator('.modal-panel .menu-item', { hasText: 'Catch me up' })).toBeVisible(); await expect( - page.locator('.palette-item', { hasText: 'Review with the Assistant' }) + page.locator('.modal-panel .menu-item', { hasText: 'Review with the Assistant' }) ).toBeVisible(); await page.keyboard.press('Escape'); @@ -257,7 +257,7 @@ test('assistant tab: gated by the account switch and muted per story', async ({ await expect(status).toHaveText('Assistant off'); await gotoReady(page, storyUrl); await expect(page.locator('.story-title')).toHaveText('Gatekeeper'); - await expect(page.locator('.rtab', { hasText: 'Assistant' })).toHaveCount(0); + await expect(page.locator('.right-head .seg-btn', { hasText: 'Assistant' })).toHaveCount(0); // And the menus carry no Assistant entries while it is off. await expect(page.locator('.cm-content')).toBeVisible(); @@ -277,7 +277,7 @@ test('assistant tab: gated by the account switch and muted per story', async ({ // And the palette drops the Assistant commands. await page.keyboard.press('ControlOrMeta+k'); - await expect(page.locator('.palette')).toBeVisible(); - await expect(page.locator('.palette-item', { hasText: 'Focus mode' })).toBeVisible(); - await expect(page.locator('.palette-item', { hasText: 'Catch me up' })).toHaveCount(0); + await expect(page.locator('.modal-panel')).toBeVisible(); + await expect(page.locator('.modal-panel .menu-item', { hasText: 'Focus mode' })).toBeVisible(); + await expect(page.locator('.modal-panel .menu-item', { hasText: 'Catch me up' })).toHaveCount(0); }); diff --git a/e2e/core-flow.spec.ts b/e2e/core-flow.spec.ts index 4ba4916..75dee02 100644 --- a/e2e/core-flow.spec.ts +++ b/e2e/core-flow.spec.ts @@ -266,7 +266,10 @@ test('sign in, create a universe and a story, and open it', async ({ page, brows // Alice joined the Factions category above; the kind line carries it. await expect(page.locator('.entity-card .pop-role')).toHaveText('Character ยท Factions'); await expect(page.locator('.entity-card .pop-summary')).toHaveText('A toll-road smuggler.'); - await expect(page.locator('.entity-card .pop-open')).toHaveAttribute('href', /\/plan\?entity=/); + await expect(page.locator('.entity-card .btn-primary')).toHaveAttribute( + 'href', + /\/plan\?entity=/ + ); await fenwickSave; // The worker indexes the mention asynchronously; once it has, the scene's @@ -609,7 +612,7 @@ test('sign in, create a universe and a story, and open it', async ({ page, brows const relDelete = page.waitForResponse( (r) => r.url().includes('/api/relationships/') && r.request().method() === 'DELETE' && r.ok() ); - await page.locator('.rel-remove').click(); + await page.locator('.rel-row .icon-btn').click(); await relDelete; await expect(page.locator('.rel-row')).toHaveCount(0); diff --git a/e2e/entity-card.spec.ts b/e2e/entity-card.spec.ts index 71a9f0c..4822125 100644 --- a/e2e/entity-card.spec.ts +++ b/e2e/entity-card.spec.ts @@ -48,10 +48,10 @@ test('entity card: open from a mention, then back to the tabs', async ({ page }) await expect(card.locator('.insp-name')).toHaveText('Veylan'); await expect(card.locator('.insp-open')).toBeVisible(); // The tabs are replaced while the card is open. - await expect(page.locator('.rtabs')).toHaveCount(0); + await expect(page.locator('.right-head .seg')).toHaveCount(0); // Back returns to the tabbed panel. await card.locator('.back-btn').click(); await expect(card).toHaveCount(0); - await expect(page.locator('.rtabs')).toBeVisible(); + await expect(page.locator('.right-head .seg')).toBeVisible(); }); diff --git a/e2e/find-and-search.spec.ts b/e2e/find-and-search.spec.ts index 208d2c3..45aaea0 100644 --- a/e2e/find-and-search.spec.ts +++ b/e2e/find-and-search.spec.ts @@ -55,7 +55,7 @@ test('find in the editor and search the prose from the palette', async ({ page } // The palette finds the prose and lands on the scene. await page.keyboard.press('ControlOrMeta+k'); await page.getByPlaceholder('Search, or type a command...').fill(`pin${stamp}`); - const passage = page.locator('.palette-item', { hasText: 'In the text' }); + const passage = page.locator('.modal-panel .menu-item', { hasText: 'In the text' }); await expect(passage).toHaveCount(1); await expect(passage).toContainText(`pin${stamp} near the well`); await passage.click(); @@ -76,7 +76,7 @@ test('find in the editor and search the prose from the palette', async ({ page } await page.keyboard.press('ControlOrMeta+k'); await page.getByPlaceholder('Search, or type a command...').fill(`pin${stamp}`); - const jump = page.locator('.palette-item', { hasText: 'In the text' }); + const jump = page.locator('.modal-panel .menu-item', { hasText: 'In the text' }); await expect(jump).toHaveCount(1); await jump.click(); await expect(page.locator('.cm-content')).toContainText(`pin${stamp}`); diff --git a/e2e/review-entity-card.spec.ts b/e2e/review-entity-card.spec.ts index e47b7ac..81aab4b 100644 --- a/e2e/review-entity-card.spec.ts +++ b/e2e/review-entity-card.spec.ts @@ -44,7 +44,7 @@ test('review: hovering an entity mention opens its quick card', async ({ page }) const card = page.locator('.entity-card'); await expect(card).toBeVisible(); await expect(card.locator('.pop-name')).toHaveText('Veylan'); - await expect(card.locator('.pop-open')).toBeVisible(); + await expect(card.locator('.btn-primary')).toBeVisible(); // Moving the pointer away dismisses it. await page.mouse.move(2, 2); diff --git a/e2e/review.spec.ts b/e2e/review.spec.ts index 108b636..a3a31db 100644 --- a/e2e/review.spec.ts +++ b/e2e/review.spec.ts @@ -134,7 +134,7 @@ test('guest review: invite, comment as a guest, reply and resolve as the author' // Resolve the thread, then the Done filter shows both outcomes. await page.getByRole('button', { name: 'Resolve comment' }).click(); - await page.locator('.rv-filter', { hasText: 'Done' }).click(); + await page.locator('.rv-panel-head .seg-btn', { hasText: 'Done' }).click(); await expect(page.locator('.rv-status.resolved')).toBeVisible(); await expect(page.locator('.rv-status.accepted')).toBeVisible(); diff --git a/e2e/scene-board.spec.ts b/e2e/scene-board.spec.ts index ba3c20e..7209e14 100644 --- a/e2e/scene-board.spec.ts +++ b/e2e/scene-board.spec.ts @@ -77,7 +77,7 @@ test('scene board: a card moves along the status ladder and stays there', async await expect(page.locator('.ent-row')).toHaveCount(1); await expect(page.locator('.ent-row .name')).toHaveText('Ferry'); await page.getByLabel('Filter characters, places, lore...').fill('zzz-nobody'); - await expect(page.locator('.search-empty')).toBeVisible(); + await expect(page.locator('.empty-state-text', { hasText: 'Nothing matches.' })).toBeVisible(); await page.getByRole('button', { name: 'Clear' }).click(); // The universe plan shows the story board: this story sits in the lane diff --git a/e2e/scene-title.spec.ts b/e2e/scene-title.spec.ts index b5adc0c..cfb4fbd 100644 --- a/e2e/scene-title.spec.ts +++ b/e2e/scene-title.spec.ts @@ -72,7 +72,9 @@ test('opening a story resumes the last-edited scene', async ({ page }) => { await expect(page.locator('.scene-row')).toHaveCount(1); await expect(page.locator('.scene-row .scene-name')).toHaveText('Second thoughts'); await page.getByLabel('Filter chapters and scenes...').fill('zzz-no-such-scene'); - await expect(page.locator('.search-empty')).toBeVisible(); + await expect( + page.locator('.empty-state-text', { hasText: 'No chapters or scenes match.' }) + ).toBeVisible(); await page.getByRole('button', { name: 'Clear' }).click(); await expect(page.locator('.scene-row')).toHaveCount(2); }); diff --git a/e2e/universe-settings.spec.ts b/e2e/universe-settings.spec.ts index ddc8de3..6d8980b 100644 --- a/e2e/universe-settings.spec.ts +++ b/e2e/universe-settings.spec.ts @@ -61,7 +61,7 @@ test('universe settings: contents, categories, history, export, and the trash', await page.getByRole('link', { name: 'History' }).click(); const entry = page.locator('.revision-entry', { hasText: 'Histor' }).first(); await expect(entry).toBeVisible(); - await expect(entry.locator('.revision-source-kind')).toHaveText('Character'); + await expect(entry.locator('.pill')).toHaveText('Character'); await page.getByRole('button', { name: 'Checkpoints only' }).click(); await expect(page.locator('.revision-entry', { hasText: 'Histor' })).toHaveCount(0); await page.getByRole('button', { name: 'All', exact: true }).click(); diff --git a/scratch/design-kit/README.md b/scratch/design-kit/README.md index 9ca42f6..ab869a6 100644 --- a/scratch/design-kit/README.md +++ b/scratch/design-kit/README.md @@ -54,4 +54,4 @@ repo to project; never edit the system inside the project. Foundations: `colors.html`, `type.html`, `spacing.html`. Components: `buttons.html`, `forms.html`, `menus.html`, `badges.html`, -`seg.html`, `cards.html`, `empty-states.html`. +`seg.html`, `cards.html`, `empty-states.html`, `modal.html`. diff --git a/scratch/design-kit/buttons.html b/scratch/design-kit/buttons.html index f12f3ab..8c8678f 100644 --- a/scratch/design-kit/buttons.html +++ b/scratch/design-kit/buttons.html @@ -17,10 +17,12 @@ +
+
@@ -49,11 +51,49 @@ + + +

The only general button system. One primary action per view; ghost for quiet actions - inside panels; danger only for destructive actions. Icon-only buttons use icon-btn with an - aria-label. Do not invent a new button skin. + inside panels; danger only for destructive actions; accept for the affirmative decision. + Icon-only buttons use icon-btn with an aria-label, at 32px in chrome and 28px (icon-btn + sm) inside cards, panel heads and modal heads. Disabled buttons drop to 0.45 opacity and + do not light up on hover. Do not invent a new button skin.

diff --git a/scratch/design-kit/empty-states.html b/scratch/design-kit/empty-states.html index 512ad17..eec5c16 100644 --- a/scratch/design-kit/empty-states.html +++ b/scratch/design-kit/empty-states.html @@ -6,6 +6,8 @@ Empty states + + + + +
+ + + + diff --git a/scratch/design-kit/seg.html b/scratch/design-kit/seg.html index 2c58f8d..0326e99 100644 --- a/scratch/design-kit/seg.html +++ b/scratch/design-kit/seg.html @@ -28,10 +28,17 @@ +
With counts (seg-count)
+
+ + +

The exclusive-choice strip: modes, tab pairs, filters. Items are links when they navigate and buttons when they select in place; an unavailable item is disabled, not hidden - mid-session. The review workspace's rtabs duplicate is marked for consolidation onto this. + mid-session. A seg-count rides inside a segment when the choice has a tally behind it; it + is not a second badge family and only ever appears inside a seg-btn. Multi-select filters + are chips, not a seg.

diff --git a/scratch/system-design/design-system.md b/scratch/system-design/design-system.md index de4ac9d..8e59795 100644 --- a/scratch/system-design/design-system.md +++ b/scratch/system-design/design-system.md @@ -15,9 +15,10 @@ Companion documents: `design.md` (product intent and page semantics), Use tokens for every colour, radius, and font. Wrap every page in one of the four shells. Build controls from the canonical primitives below (`.btn`, `.icon-btn`, `.field`/`.input`, `.popover`/`.menu-item`, `.seg`, `.badge`, -`.chip`, `.pill`, `.empty`, `Icon.svelte`). Check all three themes. Do not -invent a new button skin, menu system, modal, or empty state; the codebase -already has too many, and the list of offenders is at the end of this sheet. +`.chip`, `.pill`, `.modal-panel`, `.empty-state`, `Icon.svelte`). Check all +three themes. Do not invent a new button skin, menu system, modal, or empty +state; the consolidation that removed the previous crop is described at the +end of this sheet. ## Tokens @@ -34,6 +35,17 @@ Fonts: content face is `--font-content`, which the user can switch. - `--font-mono` (JetBrains Mono) for code and identifiers. +Type ramp, seven steps covering every chrome and prose size. Use a step +rather than a raw pixel value; the serif display sizes stay per-surface +decisions, because a story title is not a card title. + +- `--text-micro` (11px) - segment counts, uppercase eyebrows. +- `--text-meta` (11.5px) - field hints, timestamps. +- `--text-sm` (12.5px) - small buttons, labels, secondary rows. +- `--text-base` (13.5px) - the default chrome size. +- `--text-lg` (15px) - modal titles, search fields. +- `--text-prose-sm` (14.5px) / `--text-prose` (19px) - reading surfaces. + Surfaces, from back to front: - `--bg` - the page background. @@ -53,6 +65,14 @@ Accent family: `--accent` (fills), `--accent-contrast` (text on accent fills; use this, never `#fff`), `--accent-soft` (selection, focus glow, soft fills), `--accent-line` (focused borders). +Assets and contrast: + +- `--danger-contrast` - text on a `--danger` fill (`.btn-danger`). The + danger twin of `--accent-contrast`; never write `#fff` on danger. +- `--select-caret` - the `.select` caret, declared per theme. A data URL + cannot read a custom property, so each theme carries its own faint stroke + colour in the asset. `.select` is the only consumer. + Meaning colours: - Status: `--status-outline`, `--status-draft`, `--status-revised`, @@ -113,19 +133,35 @@ The rule for all of them: if the control you need is close to one of these, use or extend the primitive; if it is genuinely new, add it to the shared CSS with a considered name and add it to this sheet. +Forced-state hooks: every state rule in the shared CSS also matches +`.is-hover`, `.is-active`, or `.is-focus` beside the real pseudo-state. +These exist for one reason only, so the primitives sheet can render a full +state matrix from the shipped declarations instead of restating them, which +is how the drift started. Product Svelte code must never use them; if a +component needs to look hovered, that is a real state and needs a real +class. One pre-existing exception is grandfathered: `EditorToolbar` and +`ViewMenu` put `.is-active` on `.md-tool`, which predates the hooks. It +does not collide, because every hook selector is compound and needs the +primitive's own class too, but do not copy the pattern. + ### Buttons - `.btn` with `.btn-primary`, `.btn-secondary`, `.btn-ghost`, `.btn-danger`, - size modifier `.btn-sm`. Defined in `pages.css`. This is the only general - button system. -- `.icon-btn` for icon-only buttons (top bars, panel headers). Defined in - `theme.css`; also styles `a.icon-btn`. + `.btn-accept`, size modifier `.btn-sm`. Defined in `pages.css`. This is + the only general button system. +- `.btn-accept` is the affirmative decision (accept a suggestion, approve a + pass), built on `--status-final`. It is a variant, not a second family. +- Every variant carries `:hover:not(:disabled)`, `:active`, + `:focus-visible`, and `:disabled` at 0.45 opacity. Disabled buttons do + not light up on hover. +- `.icon-btn` for icon-only buttons, 32px in chrome (top bars, panel + headers). `.icon-btn.sm` is 28px, for inside cards, panel headers and + modal headers where a 32px target would crowd the row. `.icon-btn.danger` + tints the hover for a destructive action. Defined in `theme.css`; also + styles `a.icon-btn`. There are two icon sizes and no third. - Scoped exceptions that stay: `.md-tool` (editor toolbar), `.seg-btn` - (segmented strips), `.rv-btn` family (review surfaces, pending - consolidation). -- Everything else (`.tool-btn`, `.mini-btn`, `.rb-btn`, `.send-btn`, - `.card-add`, `.outline-add`, and the rest of the 20+ skins) is legacy. - Do not use them in new code and do not create new ones. + (segmented strips). +- An icon-only button always carries an `aria-label`. ### Forms @@ -150,10 +186,24 @@ CSS with a considered name and add it to this sheet. ### Modals -There is no shared modal primitive yet; three hand-rolled ones exist. Until -one is extracted, model a new modal on `ReviewModal.svelte`: token-based -backdrop (`color-mix` over `--bg-canvas`), panel on `--bg-elevated`, footer -buttons using `.btn`. Do not copy `HelpModal`'s hard-coded black backdrop. +`.modal-backdrop` wrapping a `.modal-panel`, defined in `primitives.css`. +Two sizes and no third: 420px by default, 640px with `.modal-lg` for a list +to scan or a passage to read. The backdrop is derived from `--bg`, so all +three themes dim in their own key; there is no backdrop token and none is +needed. + +Structure: `.modal-head` (with `.modal-head-main`, `.modal-title`, +`.modal-sub`, `.modal-kind`), `.modal-body` (add `.rows` when it holds +`.menu-item` rows rather than prose), `.modal-foot` (with +`.modal-foot-note` on the left for a hint or shortcut legend). The close +control is an `.icon-btn.sm`; the primary action sits last in the footer. +A head can carry a `.modal-search` field instead of a title, with +`.modal-head.searching`. + +The behaviour is the component's, not the CSS's: Esc closes, focus is +trapped in the panel and returned to the opener, and a backdrop click +closes only when nothing is unsaved. `CommandPalette`, `ReviewModal` and +`HelpModal` are the three reference uses. ### Badges, chips, pills @@ -162,22 +212,27 @@ Three words, three meanings; keep them straight: - `.badge` - an entity identity mark (image or initial), sized `dot`, `sm`, `lg`, rendered by `EntityBadge.svelte`. - `.chip` - an interactive token: tags, removable filters, add-affordances - (`.muted`, `.dashed`, `.link`). + (`.muted`, `.dashed`, `.link`). `.chip-x` is the remove affordance inside + a removable chip; it only ever appears inside a `.chip`. A chip whose + removal needs confirming is not a chip, it is a row with a danger action. - `.pill` - a small static status label (`.pill-accent`). -The dozen other spellings (`.role-tag`, `.nav-badge`, `.rv-type-pill`, -`.tfa-badge`, ...) are scoped legacy; do not add new ones. +A thing that is both a label and a control is a chip, not a pill. + +The remaining scoped spellings (`.role-tag`, `.nav-badge`, `.tfa-badge`, +...) are legacy; do not add new ones. ### Segmented strips and filters -- `.seg` container with `.seg-btn` items (`.active`, `:disabled`) - the - mode strip and any exclusive-choice strip. `ModeSwitcher.svelte` is the - reference use. -- `.rtabs`/`.rtab` in the review workspace is a duplicate of `.seg` and is - marked for consolidation. Known bug to not replicate: `.seg-btn.active` - has a light-theme shadow soften; `.rtab.active` lacks it. -- Filter pill rows also exist twice (`.rv-filters` and `.revision-filters`); - prefer extending `.seg` semantics rather than adding a third. +- `.seg` container with `.seg-btn` items (`.active`, `:disabled`, + `:focus-visible`) - the mode strip, the right-panel tabs, the review + tabs, and any exclusive choice of two to four peers. `.seg.full` stretches + the items to fill the row. `ModeSwitcher.svelte` is the reference use. +- `.seg-count` is a tally riding inside a `.seg-btn` (open notes, filtered + results) at `--text-micro`. It is not a second badge family and appears + nowhere else. +- The line: an exclusive choice is a `.seg`; a multi-select filter row is + chips. ### Lists, rows, tables @@ -198,9 +253,12 @@ new settings content composes `.admin-card`. ### Empty states -`.empty` (centred, `--text-faint`, 13px) is the primitive. The other eleven -spellings are legacy. An empty state is one short sentence telling the user -what to do, per the writing rules. +`.empty-state` is the primitive, defined in `primitives.css`: a centred +column at `--text-faint`, with an optional `.empty-state-icon`, a required +`.empty-state-text`, and an optional `.empty-state-action`. Add `.tight` +inside a right-panel card or a sidebar list, where the full padding would +push the panel into a scroll. An empty state is one short sentence telling +the user what to do, per the writing rules. ### Icons @@ -234,9 +292,9 @@ From CLAUDE.md, repeated here because every screen touches it: ## CSS file map Load order in `src/routes/+layout.svelte`: `tokens.css`, `theme.css`, -`pages.css`, `admin.css`, `editor.css`, `review.css`, `menus.css`. All -global and unscoped, so a later file silently wins a specificity tie; check -for an existing definition before adding a class. +`pages.css`, `admin.css`, `editor.css`, `review.css`, `menus.css`, +`primitives.css`. All global and unscoped, so a later file silently wins a +specificity tie; check for an existing definition before adding a class. - `tokens.css` - tokens and base element rules only. No components. - `theme.css` - the workspace shell and its components (outline rows, @@ -247,11 +305,14 @@ for an existing definition before adding a class. cards, autocomplete popup). - `review.css` - review surfaces. - `menus.css` - the shared popover/menu skin. +- `primitives.css` - loaded last, so it wins over any screen-local skin. + Holds the families that had no single owner before: the modal, the empty + state, the chip remove affordance, and the shared focus ring. Component ` diff --git a/src/lib/components/EntityQuickCard.svelte b/src/lib/components/EntityQuickCard.svelte index 2eb1a84..8b7b700 100644 --- a/src/lib/components/EntityQuickCard.svelte +++ b/src/lib/components/EntityQuickCard.svelte @@ -46,7 +46,7 @@ {#if entity.related && entity.related.length > 0} diff --git a/src/lib/components/EntityRelationships.svelte b/src/lib/components/EntityRelationships.svelte index b954da9..51958cf 100644 --- a/src/lib/components/EntityRelationships.svelte +++ b/src/lib/components/EntityRelationships.svelte @@ -129,7 +129,7 @@ {relationship.notesMd} {/if} -
{@html renderMarkdown(article.body)}
-