diff --git a/DESIGN.md b/DESIGN.md index 7eaceec028..30ef7a682a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -126,11 +126,12 @@ Nothing interactive is square. One ladder, assigned monotonically by box height: | Radius | Maka tier | Astryx tier | Assign to | |---|---|---|---| | 6px | control | inner | chips, keycaps, nested inlays, and product-drawn compact controls | -| 10px | card | element | cards, rows-as-cards, list containers, chat bubbles; Astryx `Button`, `Input`, `SegmentedControl` | +| 10px | card | element | cards, rows-as-cards, list containers; Astryx `Button`, `Input`, `SegmentedControl` | | 12px | container | container | modals, panels, portal surfaces; Astryx `Card`, `Dialog`, `DropdownMenu` | +| 28px | chat | chat | the conversation surface as one shape: user bubble, assistant bubble, composer dock. Sourced from Astryx's `--radius-chat` by `ChatMessageBubble` and `ChatComposer` themselves; product CSS never restates it | | full | pill (999px) | full (9999px) | badges, pills, circular controls | -- **The Two-Name Rule.** These are one ladder under two vocabularies, and the names never line up: Maka's `control` is Astryx's `inner`, Maka's `card` is Astryx's `element`, Maka's `modal` is Astryx's `container`. Resolve a tier from the box, never from the token name that sounds right. The paired values agree *today* but are independent literals, not aliases — an Astryx upgrade can move one side silently, so a mismatch is a real failure mode rather than an impossibility. Astryx's `--radius-page` (28px) has no Maka tier and no product consumer; anything reaching for a page-level radius is inventing a rung. +- **The Two-Name Rule.** These are one ladder under two vocabularies, and the names never line up: Maka's `control` is Astryx's `inner`, Maka's `card` is Astryx's `element`, Maka's `modal` is Astryx's `container`. Resolve a tier from the box, never from the token name that sounds right. The paired values agree *today* but are independent literals, not aliases — an Astryx upgrade can move one side silently, so a mismatch is a real failure mode rather than an impossibility. The chat rung is the one tier the product does not assign: `ChatMessageBubble` and `ChatComposer` both resolve `--radius-chat` on their own, which is why the bubble and the dock round together, and why setting a bubble radius or a non-default `density` in product code silently breaks the pair. Astryx's `--radius-page` carries the same 28px literal but is a different token with no Maka tier and no product consumer; reaching for it to match the chat surface is inventing a rung. - **The Full-Bleed Rule.** `border-radius: 0` is legal only on true full-bleed rows — an element flush with its container on both sides. Radius and gap move together: if it has breathing room, it has corners. - **Proportional marks.** Product-drawn icon plates use ratio-owned radius (~25–27% of the box edge), recorded in prose because Stitch accepts only absolute units. diff --git a/apps/desktop/e2e/session-workbar.spec.ts b/apps/desktop/e2e/session-workbar.spec.ts index 6f754ba4fb..e5d69d2fb6 100644 --- a/apps/desktop/e2e/session-workbar.spec.ts +++ b/apps/desktop/e2e/session-workbar.spec.ts @@ -70,6 +70,32 @@ test('narrow right workbar keeps launcher shortcuts and side-chat send button in const composerCard = companion.locator('.maka-composer-astryx'); // Keep ChatComposer's inner elevation visible. await expect(composerCard).toHaveCSS('overflow', 'visible'); + + // #3452: Side Chat is a branch of the main conversation, not a second + // conversation surface, so its dock rounds like the main dock. Both resolve + // Astryx's `--radius-chat`, the same token `ChatMessageBubble` defaults to — + // a side-only `--_chat-composer-radius` split the bubble from the dock the + // moment the bubbles moved back to that default. Compared against the main + // composer rather than a literal so an upstream token change moves both or + // fails here. + const composerRadii = await page.evaluate(() => { + const wrappers = [...document.querySelectorAll('.maka-composer-astryx')]; + const inCompanion = (element: Element) => element.closest('.maka-quote-companion') !== null; + const effectiveRadius = (element: Element | undefined) => { + if (!element) return null; + const styles = getComputedStyle(element); + return ( + styles.getPropertyValue('--_chat-composer-radius').trim() || + styles.getPropertyValue('--radius-chat').trim() + ); + }; + return { + side: effectiveRadius(wrappers.find(inCompanion)), + main: effectiveRadius(wrappers.find((element) => !inCompanion(element))), + }; + }); + expect(composerRadii.side).toBeTruthy(); + expect(composerRadii.side).toBe(composerRadii.main); // Match the long model-label pressure from the reported side-chat screenshot // without coupling the fixture's globally useful default model to this test. await companion.locator('.maka-composer-model-chip-text').evaluate((element) => { diff --git a/apps/desktop/src/renderer/styles/chat-message.css b/apps/desktop/src/renderer/styles/chat-message.css index d9c12e890e..fcb71e4d87 100644 --- a/apps/desktop/src/renderer/styles/chat-message.css +++ b/apps/desktop/src/renderer/styles/chat-message.css @@ -62,26 +62,23 @@ font: var(--maka-text-body); } -/* Padding and max-width belong to ChatMessageBubble; re-declaring those here - only forks the primitive. What stays is the product tint, the verbatim-text - rule (Maka renders user input as typed, which the primitive has no opinion - about) — and, since Visual System 2.0 (T3), the radius TIER. - - The primitive resolves `--radius-container` (12px). DESIGN.md §6 assigns - radius by box height, and container is scoped to "modals, panels, portal - surfaces" — a message bubble is none of those. Measured in the chat fixture - the filled user bubble is 44px tall, which lands it in the card tier (10px, - "cards, rows-as-cards, list containers"). Astryx publishes no radius prop on - this component (only `variant`), so the tier is expressed on Maka's own - class rather than by reaching into the primitive's internals. +/* Padding, max-width and radius belong to ChatMessageBubble; re-declaring any + of them here only forks the primitive. What stays is the product tint and the + verbatim-text rule (Maka renders user input as typed, which the primitive has + no opinion about). + + Radius is absent on purpose. The primitive resolves `--radius-chat` (28px), + the same token `ChatComposer` reads through `--_chat-composer-radius`, so the + bubble and the dock round together as one conversation surface — DESIGN.md §6 + records that as the chat rung. Astryx publishes no radius prop on this + component (only `variant`), so a product radius could only fight the + primitive, never configure it. Scoped to the filled bubble on purpose: the assistant bubble is `ghost` (no - fill, measured transparent), so it has no corner to round and inherits - nothing it needs to override. */ + fill, measured transparent), so it carries no tint to override. */ .maka-chat-message-bubble-user { background: var(--chat-user-bg); color: var(--chat-user-foreground, var(--foreground)); - border-radius: var(--radius-surface); white-space: pre-wrap; } diff --git a/apps/desktop/src/renderer/styles/quote-side-panel.css b/apps/desktop/src/renderer/styles/quote-side-panel.css index e79dcc2be7..f473005fa7 100644 --- a/apps/desktop/src/renderer/styles/quote-side-panel.css +++ b/apps/desktop/src/renderer/styles/quote-side-panel.css @@ -78,14 +78,13 @@ padding-bottom: var(--space-2); } -.maka-session-workbar-panel[data-placement="right"] - .maka-quote-companion - .maka-composer-astryx { - --_chat-composer-radius: var(--radius-surface); - - /* Let ChatComposer paint its own radius and elevation. Clipping this wrapper - hides the body's hover/focus shadow against the white side panel. */ -} +/* `.maka-composer-astryx` is deliberately unstyled on this surface. Side Chat is + a branch of the main conversation, not a second one, so the composer resolves + `--radius-chat` here exactly as it does at full width and pairs with the + bubble; a side-only radius token forked the pair the moment the bubbles moved + to the primitive's default. The wrapper is also left unclipped — `overflow: + hidden` here would hide the body's hover/focus shadow against the white side + panel, and `session-workbar.spec.ts` asserts `overflow: visible`. */ .maka-session-workbar-panel[data-placement="right"] .maka-quote-companion diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx index 6d1358e9c9..d45637cddd 100644 --- a/packages/ui/src/chat-view.tsx +++ b/packages/ui/src/chat-view.tsx @@ -483,8 +483,6 @@ export function ChatView(props: { owns. */} {conversationItems.length > 0 ? ( @@ -589,8 +587,6 @@ export function ChatView(props: {