diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 40c8047fc9563..c9ee3aae5e604 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -3994,7 +3994,8 @@ prompt into one bordered box on the composer fill, is the deliberate exception and keeps that construction — including its own top edge under a notice, since the notice above it paints no outline to share. Shoulder tabs (stash, tasks) are - excluded by name and keep riding the card's shoulder. The remaining Figma + banner Roots and take the same floor treatment; only the dock stacking and + the tabs' shared drawer density name them. The remaining Figma 467:33308 card (branch changed) is stamped data-fork-composer-notice so drawer density stays on Reconnect / Update / tasks. It drops font-medium / leading-7 at the source, stacks title over description, and stamps its @@ -4134,8 +4135,7 @@ keeps one ring), and square the prompt surface's top corners so the two read as one box. Because the card takes the whole dock row, the stash tab cannot sit beside it as it does beside upstream's inset drawer: the dock stacks - and the tab rides above the card's right shoulder, its overlap padding - tucked under the card. + and the tab sits as a floor row above the card, right-aligned. tier: 2 files: - apps/web/src/custom/useComposerPendingUserInputCard.ts diff --git a/apps/web/src/__fork_guards__/forkComposerBannerSurface.test.ts b/apps/web/src/__fork_guards__/forkComposerBannerSurface.test.ts index e45976639aaf5..e9b3a0bf37406 100644 --- a/apps/web/src/__fork_guards__/forkComposerBannerSurface.test.ts +++ b/apps/web/src/__fork_guards__/forkComposerBannerSurface.test.ts @@ -142,7 +142,7 @@ describe("fork guard: fork-composer-banner-surface", () => { const [rule, ...rest] = cardRules(() => true); expect(rest).toHaveLength(0); expect(rule?.selector).toContain(`${MARKER}.dark`); - expect(rule?.selector).toContain("[data-composer-shoulder-tab]"); + expect(rule?.selector).not.toContain("[data-composer-shoulder-tab]"); expect(rule?.body).toMatch(/--chat-composer-attachment-overlap:\s*0px/u); for (const painted of ["background", "border", "box-shadow"]) { expect(rule?.body, painted).not.toContain(painted); @@ -162,13 +162,19 @@ describe("fork guard: fork-composer-banner-surface", () => { }); it("pulls the attachment wrappers out to the vessel's full width", () => { - const rule = rules.find( + const widened = rules.filter( (candidate) => flat(candidate.selector).includes('[data-slot="composer-banner-attachment"]:has(') && - flat(candidate.selector).includes(CARD) && candidate.body.includes("width: 100%"), ); + expect(widened).toHaveLength(1); + const [rule] = widened; expect(rule?.selector).toContain(`${MARKER}.dark`); + // By descendant and with no tab carve-out: the dock reaches the tasks row + // only through its column, so a child match or a `:not` on the shoulder + // tab sends the tasks-only dock back under the prompt box. + expect(flat(rule?.selector ?? "")).toContain(`:has( ${CARD} )`); + expect(rule?.selector).not.toContain(":not("); expect(rule?.body).toMatch(/max-width:\s*none/u); expect(rule?.body).toMatch(/margin:\s*0/u); }); @@ -178,9 +184,18 @@ describe("fork guard: fork-composer-banner-surface", () => { (candidate) => flat(candidate.selector).includes(CARD) && flat(candidate.selector).endsWith("::before"), ); + expect(rule?.selector).not.toContain("[data-composer-shoulder-tab]"); expect(rule?.body).toMatch(/display:\s*none/u); }); + it("gives both shoulder tabs the tasks drawer's density", () => { + const rule = rules.find((candidate) => + flat(candidate.selector).endsWith("[data-fork-composer-vessel] [data-composer-shoulder-tab]"), + ); + expect(rule?.selector).toContain(`${MARKER}.dark`); + expect(rule?.body).toMatch(/--composer-banner-padding-block:\s*calc\(var\(--spacing\) \* 1\)/u); + }); + it("gives notices and the activity strip the design's metrics", () => { const rule = rules.find((candidate) => candidate.body.includes("--composer-banner-icon-column"), @@ -242,7 +257,7 @@ describe("fork guard: fork-composer-banner-surface", () => { expect(componentSource("Root")).toContain('data-slot="composer-banner"'); const attachment = componentSource("Attachment"); expect(attachment).toContain('data-slot="composer-banner-attachment"'); - // The tuck the card rule zeroes, and the shoulder-tab margin still reads. + // The tuck the wrapper rule cancels; the card rule zeroes its overlap. expect(attachment).toContain("-mb-[calc(1rem+1px)]"); expect(banner).toContain("--chat-composer-attachment-overlap"); for (const slot of [ diff --git a/apps/web/src/theme.custom.css b/apps/web/src/theme.custom.css index fa4ebda30d0f6..432549c4d1a14 100644 --- a/apps/web/src/theme.custom.css +++ b/apps/web/src/theme.custom.css @@ -2317,8 +2317,8 @@ literal value would stop tracking the token the control row reads. Shoulder tabs (the stash and tasks badges) are ComposerBanner.Roots too and - are excluded by name everywhere below: they are tabs on the card's shoulder, - not cards. The collapsed stack's peek cap has no surface attribute but reads + take the same floor treatment; only the dock stacking and the tab density + below name them. The collapsed stack's peek cap has no surface attribute but reads the same fill variable, so the colour remap lists its slot beside the surface and gives it the hairline in place of its severity border utility; it takes the composer radius so the cap reads as one more card behind. @@ -2340,25 +2340,23 @@ /* The attachment wrappers carry the inset width and the tuck, and the dock is one of them, so any wrapper around a card goes full width and flush. The - overlap variable is what upstream's tuck is measured in — zeroing it on the - card drops the padding the tuck reserved. It is read off the tab's own - element below, which keeps its value. */ + shoulder tabs (stash, tasks) are banner Roots too, so they count: upstream's + inset tab tucked under the prompt box reads as a dark slab jammed against + the vessel's top edge, and like the banners they are vessel floor instead. + The match is by descendant so the dock also goes flush around the tasks row + nested in its column, whether or not the stash tab is showing. */ :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-vessel] [data-slot="composer-banner-attachment"]:has( - [data-slot="composer-banner"][data-composer-banner-surface="attached"]:not( - [data-composer-shoulder-tab] - ) + [data-slot="composer-banner"][data-composer-banner-surface="attached"] ) { width: 100%; max-width: none; margin: 0; } -/* A full-width card would squeeze the stash / tasks tab sharing the dock row. - Stack the dock instead: the tab rides above the card's right shoulder and - its own overlap padding tucks it under the card, which paints over it in - order-modified paint order. */ +/* A full-width card would squeeze the stash tab sharing the dock row. Stack + the dock instead: the tab is a floor row above the card, right-aligned. */ :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-vessel] [data-slot="composer-banner-attachment"]:has(> [data-composer-shoulder-tab]):has( @@ -2380,14 +2378,11 @@ > [data-composer-shoulder-tab] { order: -1; align-self: flex-end; - margin-bottom: calc(-1 * var(--chat-composer-attachment-overlap) - 4px); } :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-vessel] - [data-slot="composer-banner"][data-composer-banner-surface="attached"]:not( - [data-composer-shoulder-tab] - )::before { + [data-slot="composer-banner"][data-composer-banner-surface="attached"]::before { display: none; } @@ -2400,40 +2395,20 @@ measures its tuck in, and the Root reserves bottom padding for it. */ :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-vessel] - [data-slot="composer-banner"][data-composer-banner-surface="attached"]:not( - [data-composer-shoulder-tab] - ) { + [data-slot="composer-banner"][data-composer-banner-surface="attached"] { --chat-composer-attachment-overlap: 0px; } -/* The collapsed tasks badge is not a shoulder tab here: it is the header row - of the open tasks drawer, so it gets the drawer's floor treatment — vessel - width, no tuck, no card, the drawer's default density — and toggling only - adds or removes the list beneath it. It is only ever shown without a banner - card, so it never needs to ride a card's shoulder the way stash does. */ +/* The collapsed tasks badge is the header row of the open tasks drawer, so + both shoulder tabs take the drawer's default density: toggling tasks only + adds or removes the list beneath it, and a stash tab sharing the dock row + keeps the same text line. */ :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-vessel] - [data-slot="composer-banner-attachment"]:has( - [data-composer-shoulder-tab] > [data-composer-tasks-badge] - ) { - width: 100%; - max-width: none; - margin: 0; -} - -:root[data-fork="noahhendrickson-t3code"].dark - [data-fork-composer-vessel] - [data-composer-shoulder-tab]:has(> [data-composer-tasks-badge]) { - --chat-composer-attachment-overlap: 0px; + [data-composer-shoulder-tab] { --composer-banner-padding-block: calc(var(--spacing) * 1); } -:root[data-fork="noahhendrickson-t3code"].dark - [data-fork-composer-vessel] - [data-composer-shoulder-tab]:has(> [data-composer-tasks-badge])::before { - display: none; -} - :root[data-fork="noahhendrickson-t3code"].dark [data-fork-composer-vessel] [data-slot="composer-banner-peek"] {