From 5fb169dc89e70551f29d02deb7247bb0db6ad7a6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:59:47 -0700 Subject: [PATCH 1/3] feat(v2): chat and the sidebar draw their keys and segment from the D223 primitives primitives.css now loads globally after v2.css. Chat's plus, Send, back, Invite and panel toggle, the panel head's keys, and the sidebar's new-pod and search keys carry v2p-b classes (ghost, primary, secondary; 32px icon, 28px small), and V2Segmented renders v2p-seg. Their bespoke looks are gone from v2.css, since a .v2-root button. rule would outrank the primitive; what stays is layout, open and disabled states, focus on the segment, and the phone hit areas. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpQmb6BbxDrAtw45bgN --- frontend/src/v2/V2App.tsx | 2 + .../v2/__tests__/v2-layout-invariants.test.ts | 40 +++-- frontend/src/v2/components/V2Composer.tsx | 4 +- frontend/src/v2/components/V2Inspector.tsx | 6 +- frontend/src/v2/components/V2PodsSidebar.tsx | 4 +- frontend/src/v2/components/V2Segmented.tsx | 4 +- frontend/src/v2/components/V2Thread.tsx | 6 +- frontend/src/v2/v2.css | 141 ++---------------- 8 files changed, 52 insertions(+), 155 deletions(-) diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index 9fef21f9a5..17e34c68d8 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -15,6 +15,8 @@ import '@fontsource/ibm-plex-sans/500.css'; import '@fontsource/ibm-plex-sans/600.css'; import '@fontsource/ibm-plex-mono/500.css'; import './v2.css'; +// D223 shared primitives (TASK-730): the keys and segment chat and the sidebar use. +import './primitives/primitives.css'; import { applyV2Theme, getV2Theme } from './utils/theme'; // TASK-145 — inside the shell, only the surfaces the composer path needs stay diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index ad93564f65..de0925de23 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -174,6 +174,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { const threadMessages = read('../components/V2ThreadMessages.tsx'); const messageRow = read('../components/V2MessageRow.tsx'); const composer = read('../components/V2Composer.tsx'); + const segmented = read('../components/V2Segmented.tsx'); + const primitives = read('../primitives/primitives.css'); const decisionCard = read('../components/V2DecisionCard.tsx'); const navRail = read('../components/V2NavRail.tsx'); const threadCard = read('../components/V2ThreadCard.tsx'); @@ -391,8 +393,9 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(search).toContain('height: 34px'); expect(podsSidebar).toContain(''); // The + is the artboard's 32px raised key, a glyph rather than a character. - expect(ruleBody(v2, '.v2-root button.v2-pods__new')).toContain('height: 32px'); - expect(ruleBody(v2, '.v2-root button.v2-pods__new')).toContain('width: 32px'); + // D223: the new-pod key is the primitives' 32px secondary icon key. + expect(podsSidebar).toContain('className="v2p-b v2p-b-s v2p-ib v2-pods__new"'); + expect(primitives).toMatch(/\.v2p-ib \{ width: 32px; height: 32px;/); expect(podsSidebar).toContain(''); expect(row).toContain('grid-template-columns: 24px minmax(0, 1fr) auto'); expect(row).toContain('min-height: 36px'); @@ -431,10 +434,12 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(v2).toContain('.v2-pods__rowwrap:hover .v2-pods__row-time,\n.v2-pods__rowwrap:has(:focus-visible) .v2-pods__row-time { visibility: hidden; }'); expect(ruleBody(v2, '.v2-pods__row-time')).toContain('min-width: 24px'); // The shared segment: a tinted track with the checked option raised. - expect(ruleBody(v2, '.v2-seg')).toContain('display: inline-flex'); - expect(ruleBody(v2, '.v2-seg')).toContain('flex: none'); - expect(ruleBody(v2, '.v2-root button.v2-seg__option')).toContain('white-space: nowrap'); - expect(selectorRuleBody(v2, '.v2-root button.v2-seg__option--on,')).toContain('background: var(--v2-surface)'); + // D223: the segment draws as the primitives' v2p-seg; v2.css keeps only its states. + expect(segmented).toContain("'v2p-seg v2-seg'"); + expect(segmented).toContain("' on v2-seg__option--on'"); + expect(ruleBody(primitives, '.v2p-seg')).toContain('display: inline-flex'); + expect(ruleBody(primitives, '.v2p-seg button.on')).toContain('background: var(--v2-surface)'); + expect(v2).not.toMatch(/(^|\n)\.v2-seg \{/); expect(ruleBody(v2, '.v2-root button.v2-seg__option:focus-visible')).toContain('box-shadow: var(--v2-focus-ring)'); expect(ruleBody(v2, '.v2-root button.v2-icon-button:focus-visible')).toContain('box-shadow: var(--v2-focus-ring)'); expect(v2).toContain('.v2-root .v2-pods__section--kind button.v2-pods__section-head'); @@ -594,10 +599,10 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(ruleBody(v2, '.v2-msg__gutter-time')).toContain('width: 36px'); // The typing line is not a message row; its faces stay small. expect(ruleBody(v2, '.v2-thread .v2-chat__typing-avatars .v2-avatar')).toContain('width: 20px'); - // The phone hides the panel's Hide key. It also carries .v2-icon-button, - // whose later display rule ties a one-class selector, so the hide rule - // must name both classes or it never applies. - expect(mediaBlockContaining(v2, '.v2-workspace-inspector__hide')).toMatch(/\.v2-root button\.v2-icon-button\.v2-workspace-inspector__hide \{ display: none; \}/); + // The phone hides the panel's Hide key; .v2-root button. outranks + // the primitive key's display. + expect(mediaBlockContaining(v2, '.v2-workspace-inspector__hide')).toMatch(/\.v2-root button\.v2-workspace-inspector__hide \{ display: none; \}/); + expect(workspaceInspector).toContain('className="v2p-b v2p-b-g v2p-ib v2p-b-sm v2-workspace-inspector__head-button v2-workspace-inspector__hide"'); expect(v2).toContain('.v2-msg--grouped'); }); @@ -1003,7 +1008,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // no border draws its edge; the plus is a ghost key inside it. expect(ruleBody(v2, '.v2-composer')).toContain('border: 0'); expect(ruleBody(v2, '.v2-composer')).toContain('box-shadow: var(--v2-shadow-pop'); - expect(ruleBody(v2, '.v2-root button.v2-composer__plus')).toContain('border: 1px solid transparent'); + expect(composer).toContain('className="v2p-b v2p-b-g v2p-ib v2-composer__plus"'); // Nothing in the column may escape the shared edge (2026-08-23: // mentions and threads sat off-grid while messages aligned). // Mentions: the wash bleed must equal the padding (the old -12px against @@ -1429,14 +1434,19 @@ describe('v2 layout invariants (CSS rule presence)', () => { test('the composer is one line with a 32px plus menu and a 32px ink Send that exists only with text (D215, TASK-734)', () => { expect(lastRuleBody(v2, '.v2-composer__row')).toContain('min-height: 52px'); expect(ruleBody(v2, '.v2-composer')).toContain('border-radius: var(--v2-r-14, 14px)'); - expect(ruleBody(v2, '.v2-root button.v2-composer__plus')).toContain('width: 32px'); - expect(ruleBody(v2, '.v2-root button.v2-composer__plus')).toContain('border-radius: var(--v2-r-8, 8px)'); + // D223: plus and Send are the primitives' 32px ghost and primary icon keys, + // which only draw because the shell loads primitives.css after v2.css. + expect(read('../V2App.tsx')).toMatch(/import '\.\/v2\.css';\n[^\n]*\nimport '\.\/primitives\/primitives\.css';/); + expect(composer).toContain('className="v2p-b v2p-b-g v2p-ib v2-composer__plus"'); + expect(ruleBody(primitives, '.v2p-ib')).toContain('border-radius: var(--v2-r-8)'); // The menu opens above the 32px plus, not over it. expect(ruleBody(v2, '.v2-composer__menu')).toContain('bottom: 40px'); expect(lastRuleBody(v2, '.v2-root .v2-composer__field > textarea')).toContain('max-height: 112px'); expect(lastRuleBody(v2, '.v2-root .v2-composer__field > textarea')).toContain('resize: none'); - expect(lastRuleBody(v2, '.v2-root button.v2-composer__send')).toContain('width: 32px'); - expect(lastRuleBody(v2, '.v2-root button.v2-composer__send')).toContain('background: linear-gradient(180deg, var(--v2-fill-a, var(--v2-ink)), var(--v2-fill-b, var(--v2-ink)))'); + expect(composer).toContain('className="v2p-b v2p-b-p v2p-ib v2-composer__send"'); + expect(ruleBody(primitives, '.v2p-b-p')).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); + // A bespoke look on Send would outrank the primitive (0,2,1 over 0,1,0). + expect(lastRuleBody(v2, '.v2-root button.v2-composer__send')).not.toContain('background'); expect(composer).toContain('{hasText && ('); expect(composer).toContain("role=\"menu\""); expect(composer).not.toContain('postsAs'); diff --git a/frontend/src/v2/components/V2Composer.tsx b/frontend/src/v2/components/V2Composer.tsx index 9589469702..6a002fa2f8 100644 --- a/frontend/src/v2/components/V2Composer.tsx +++ b/frontend/src/v2/components/V2Composer.tsx @@ -143,7 +143,7 @@ const V2Composer: React.FC = ({