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 @@ +
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 @@+ The scene and its review notes go with it. This cannot be undone. +
++ One overlay primitive, two sizes and no third: 420px by default, 640px for a list to scan + or a passage to read. The backdrop is drawn from the page background, so each theme dims + in its own key. The head carries a title or a search field, the close is an icon-btn sm, + and the primary action sits last in the footer. Esc closes, focus is trapped in the panel + and returned to the opener, and a backdrop click closes only when nothing is unsaved: + that behaviour is the component's, not the CSS's. +
+ + + +