Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions apps/web/src/__fork_guards__/forkComposerBannerSurface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
Expand All @@ -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"),
Expand Down Expand Up @@ -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 [
Expand Down
59 changes: 17 additions & 42 deletions apps/web/src/theme.custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -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;
}

Expand All @@ -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"] {
Expand Down
Loading