Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 11 additions & 8 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6285,13 +6285,17 @@
the real sidebar down beneath it as a floating card, like a dropdown menu.
The panel stays collapsed throughout: no cookie write, no layout push, and
the floating toggle and collapsed thread tabs stay mounted. The card sits
8px off the window's left and bottom edges with its top 4px under the
toggle, floating over the workspace on the narrow overlay's own stacking
and title-bar drag release rules (one :is() each), with an opaque
fill and no backdrop blur, since a blur this size over a streaming
transcript would repaint on every token. The panel's own header row is
hidden inside it. Under Glass it takes the floating overlay's opaque fill,
never the docked see-through tint. It opens after a short hover intent
14px off the window's left (clear of the stage card's 8px edge) and 8px
off its bottom, with a tail pointing up at the toggle's glyph, floating
over the workspace on the narrow overlay's own stacking and title-bar drag
release rules (one :is() each), with no backdrop blur, since a blur this
size over a streaming transcript would repaint on every token. Its outline
is a pseudo-element with a gap where the tail joins, and the tail lays the
edge over the fill, so the join stays seamless on a translucent fill. The
panel's own header row is hidden inside it. Under Glass it wears the
docked panel's see-through tint, and a mask cuts the card and tail out of
the sidebar inset beneath it, so only the window's material shows
through. It opens after a short hover intent
delay, and only once the pointer has been off the toggle since the
collapse (collapsing from the header toggle mounts it under the pointer).
It stays open while the pointer is on the toggle, the card or a menu the
Expand All @@ -6309,7 +6313,6 @@
- apps/web/src/custom/sidebarPeek.ts
- apps/web/src/theme.custom.css
- apps/web/src/theme.custom.palettes.css
- apps/web/src/theme.custom.glass-surfaces.css
shadows: []
watch:
# Fenced: store subscription, the panel's peek prop, the wrapper attribute,
Expand Down
26 changes: 20 additions & 6 deletions apps/web/src/__fork_guards__/forkSidebarPeek.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,26 @@ describe("fork-sidebar-peek", () => {
).toBe(true);
});

it("gives the card Glass's opaque floating fill, not the see-through docked tint", () => {
const rules = cssRules(readSibling("../theme.custom.palettes.css"));
it("gives the card the docked Glass tint over a hole cut in the stage", () => {
const peekSelector = '[data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]';
const docked = rules.find((rule) => rule.body.includes("--sidebar: var(--fork-glass-panel)"));
expect(docked?.selector.replace(/\s+/gu, "")).toContain(`:not(${peekSelector})`);
const floating = rules.find((rule) => rule.body.includes("--sidebar: #262626"));
expect(floating?.selector.replace(/\s+/gu, "")).toContain(peekSelector);
const palettes = cssRules(readSibling("../theme.custom.palettes.css"));
const docked = palettes.find((rule) =>
rule.body.includes("--sidebar: var(--fork-glass-panel)"),
);
// narrowChatOverlay flags a collapsed wrapper too, so the peek needs its own arm.
expect(docked?.selector.replace(/\s+/gu, "")).toContain(
`[data-slot="sidebar-wrapper"]${peekSelector}[data-sidebar-version="v2"]`,
);
const floating = palettes.find((rule) => rule.body.includes("--sidebar: #262626"));
expect(floating?.selector).not.toContain("data-fork-sidebar-peek");

// Without the hole the translucent card shows the transcript through its rows.
const hole = cssRules(readSibling("../theme.custom.css")).find(
(rule) =>
rule.selector.includes('[data-fork-sidebar-vibrancy="true"]') &&
rule.selector.includes(peekSelector) &&
rule.selector.endsWith('[data-slot="sidebar-inset"]'),
);
expect(hole?.body).toContain("mask-composite: subtract");
});
});
149 changes: 136 additions & 13 deletions apps/web/src/theme.custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -1410,31 +1410,154 @@
Collapsed-sidebar peek. Hovering the floating toggle drops the real panel
down beneath it as a card (custom/sidebarPeek.ts stamps the wrapper). The
panel stays collapsed — the gap spacer is already zero — so only the
container moves: in from off-canvas, 8px off the window edges, its top 4px
under the toggle's glyph box. Opaque fill, no backdrop blur: a blur this
size over a streaming transcript would repaint every token.
container moves: in from off-canvas, 14px in from the window's left so it
does not line up with the stage card's 8px edge, with a tail up to the
toggle's glyph. No backdrop blur: a blur this size over a streaming
transcript would repaint every token. Under Glass the stage is cut away
beneath it instead (below).
"dismissed" drops the slide transition, so dismissing it vanishes like a
menu instead of sliding out at full height. Stacking and the title-bar drag
release are the narrow overlay's rules above. */
release are the narrow overlay's rules above.

The geometry sits on the wrapper because the Glass cutout on the inset
reads it too. The tail centres on the floating toggle (AppSidebarLayout's
controls row: its left inset, ml-px, half the control), clear of the
card's corner radius. */
:root[data-fork="noahhendrickson-t3code"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"] {
--fork-peek-left: 14px;
--fork-peek-bottom: 8px;
--fork-peek-radius: 12px;
--fork-peek-tail-width: 18px;
--fork-peek-tail-height: 8px;
--fork-peek-top: calc(
var(--workspace-controls-top) +
(var(--workspace-topbar-height) + var(--workspace-titlebar-control-size)) / 2 + 3px +
var(--fork-peek-tail-height)
);
--fork-peek-tail-left: max(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off macOS, the tail misses the toggle glyph. The tail only lines up when --workspace-controls-left is the traffic-light inset. With the default 0.75rem (web, Windows, Linux, and macOS fullscreen), the inner calc gives 12 + 1 + 14 − 9 − 14 = 4px, so max() falls back to the 12px radius.

The tail then covers window x 26–44 with its apex at 35. The toggle glyph is centred at 12 + 1 + 14 = 27, so the apex is 8px off centre, right at the glyph's edge. That contradicts the header comment's "The tail centres on the floating toggle".

The clamp makes sense, since the tail can't sit inside the 12px corner. But the geometry needs a non-mac answer: for example, derive --fork-peek-left from the toggle so the tail can stay centred, or shrink the radius at the tail corner. Otherwise, at least correct the comment.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 923e2f8. Instead of clamping the tail, the card moves: --fork-peek-left: min(14px, toggle-x − radius − tail/2), and the tail is always toggle-x − tail/2 − left. In Chromium at controls-left 12px the card sits at 6px with the tail centred on 27, and at the mac inset it's still 14px with the tail on 100.

var(--fork-peek-radius),
calc(
var(--workspace-controls-left) + 1px + var(--workspace-titlebar-control-size) / 2 -
var(--fork-peek-tail-width) / 2 - var(--fork-peek-left)
)
);
}

/* No border or shadow of its own: both are the ::after below, so they can
leave a gap where the tail joins and the tail reads as one shape with the
card. The inner panel clips the rows to the corners instead of the
container, which has to let the tail out. */
:root[data-fork="noahhendrickson-t3code"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
[data-slot="sidebar"][data-collapsible="offcanvas"][data-side="left"]
[data-slot="sidebar-container"] {
top: calc(
var(--workspace-controls-top) +
(var(--workspace-topbar-height) + var(--workspace-titlebar-control-size)) / 2 + 4px
);
bottom: 8px;
left: 8px;
--fork-peek-edge: var(--fork-popup-border, var(--sidebar-border));
top: var(--fork-peek-top);
bottom: var(--fork-peek-bottom);
left: var(--fork-peek-left);
height: auto;
translate: 0 0;
overflow: visible;
border-width: 0;
border-radius: var(--fork-peek-radius);
transition: none;
}

:root[data-fork="noahhendrickson-t3code"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
[data-slot="sidebar-container"]
> [data-slot="sidebar-inner"] {
border-radius: inherit;
overflow: hidden;
border: 1px solid var(--fork-popup-border, var(--sidebar-border));
border-radius: 12px;
}

:root[data-fork="noahhendrickson-t3code"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
[data-slot="sidebar-container"]::after {
content: "";
position: absolute;
inset: 0;
z-index: 50;
border: 1px solid var(--fork-peek-edge);
border-radius: inherit;
box-shadow:
0 12px 32px rgb(0 0 0 / 0.32),
0 2px 8px rgb(0 0 0 / 0.2);
transition: none;
pointer-events: none;
/* Everything but the edge under the tail's base and the shadow under the
tail itself, which would darken a translucent tail. */
mask-image:
linear-gradient(#000 0 0), linear-gradient(#000 0 0),
linear-gradient(to bottom right, transparent 50%, #000 50%),
linear-gradient(to bottom left, transparent 50%, #000 50%);
mask-position:
-48px -48px,
calc(var(--fork-peek-tail-left) + 1px) 0,
var(--fork-peek-tail-left) calc(var(--fork-peek-tail-height) * -1),
calc(var(--fork-peek-tail-left) + var(--fork-peek-tail-width) / 2)
calc(var(--fork-peek-tail-height) * -1);
mask-size:
calc(100% + 96px) calc(100% + 96px),
calc(var(--fork-peek-tail-width) - 2px) 1px,
calc(var(--fork-peek-tail-width) / 2) var(--fork-peek-tail-height),
calc(var(--fork-peek-tail-width) / 2) var(--fork-peek-tail-height);
mask-repeat: no-repeat;
mask-clip: no-clip;
mask-composite: subtract, add, add, add;
}

/* The tail. Each half is a diagonal gradient: the panel's fill below the
slant with the outline's 1px edge laid over it, so it composites like the
card's own edge does over the inner fill, translucent under Glass included.
It takes hits, so the pointer crossing from the toggle stays on the card. */
:root[data-fork="noahhendrickson-t3code"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
[data-slot="sidebar-container"]::before {
--fork-peek-tail-edge:
transparent calc(50% - 0.25px), var(--fork-peek-edge) calc(50% + 0.25px) calc(50% + 0.75px),
transparent calc(50% + 1.25px);
--fork-peek-tail-fill: transparent calc(50% - 0.25px), var(--sidebar) calc(50% + 0.25px);
content: "";
position: absolute;
top: calc(var(--fork-peek-tail-height) * -1);
left: var(--fork-peek-tail-left);
width: var(--fork-peek-tail-width);
height: var(--fork-peek-tail-height);
background:
linear-gradient(to bottom right, var(--fork-peek-tail-edge)) left top / 50% 100% no-repeat,
linear-gradient(to bottom left, var(--fork-peek-tail-edge)) right top / 50% 100% no-repeat,
linear-gradient(to bottom right, var(--fork-peek-tail-fill)) left top / 50% 100% no-repeat,
linear-gradient(to bottom left, var(--fork-peek-tail-fill)) right top / 50% 100% no-repeat;
}

/* Glass: the card wears the docked panel's own tint (theme.custom.palettes.css),
so the stage beneath it is cut away, card and tail, the way
custom/forkGlassPopupCutout.ts cuts #root under a popup. The inset spans the
window while collapsed, so its box is the viewport. The mask makes the inset
a stacking context, which keeps its z-indexed layers under the card. */
:root[data-fork="noahhendrickson-t3code"][data-fork-sidebar-vibrancy="true"].dark[data-fork-theme="cool-darker"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
[data-slot="sidebar-inset"] {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under Glass, the hole shows up before the card does. The inset mask switches on at full strength when data-fork-sidebar-peek="open" lands. The container is still running fork-sidebar-peek-in at that point (120ms, opacity: 0 → 1, translateY(-4px) → 0) whenever data-panel-animations="true".

So every time the peek opens with animations on, the transcript disappears in the card-and-tail shape and leaves bare window material. The card then fades in over that gap, starting 4px above it, so the hole's edges and the tail cutout visibly fail to line up for the first ~120ms.

Possible fixes: drop the animation under the vibrancy selector, or animate the inset's mask in step with the card (for example, fade the subtracted layer's alpha over the same 120ms).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 923e2f8. Under Glass the card now opens without fork-sidebar-peek-in, so the hole and the card land in the same frame. Other palettes keep the fade, since they have no hole.

--fork-peek-tail-x: calc(var(--fork-peek-left) + var(--fork-peek-tail-left));
--fork-peek-tail-top: calc(var(--fork-peek-top) - var(--fork-peek-tail-height));
mask-image:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the floating browser controls above the hosted webview

With a local desktop browser tab floating over the chat, opening the Glass sidebar peek applies this mask and creates a stacking context around the entire inset. ThreadPreviewMiniPlayer renders its drag handle, reopen/close controls, and resize handles inside that inset at z-index 49, while AppRoot renders ElectronBrowserHost outside it and the hosted webview at z-index 48. The new context traps those controls below the webview, so they disappear and clicks hit the page until the peek closes, even when the floating browser does not overlap the sidebar. A Chromium fixture using this rule reproduces the hit-target change from the z-49 control to the z-48 sibling. Please keep the cutout from isolating the mini-player chrome below its hosted surface, or coordinate the host and chrome stacking when the mask is active; increasing the descendant control z-index alone cannot escape this context.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 923e2f8. The cutout is skipped while [data-preview-mini-player] [data-browser-surface-slot] is present, which is the same exception the glass stage card uses for its isolation. In that state the peek gets the opaque floating fill instead of the translucent tint, so it never sits see-through over the transcript. The tint, the active-row wash and the hole all key off the same predicate.

linear-gradient(#000 0 0),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' rx='12'/%3E%3C/svg%3E"),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: rx='12' in the SVG copies --fork-peek-radius as a literal. If the radius var changes, the corners of the hole and the card stop matching, and the stage shows at the corners (or a gap opens up). The guard wouldn't catch it. A short comment tying the two together would help. So would a guard assertion that the SVG's rx equals the var's value.

Related: because the mask makes the inset a stacking context while the peek is open, the comment above the z-index 45 rule ("Raising the panel, rather than isolating the inset…") is no longer strictly true in that state. In practice it's harmless: the lightbox can't open while you're hovering the toggle. Still, a single clause there noting the exception would keep that comment accurate.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 923e2f8. Both shapes are one SVG token each (--fork-peek-card-shape, --fork-peek-tail-shape), and a new guard asserts rx matches --fork-peek-radius and the polygon matches the tail width and height. I also added a clause to the z-index 45 comment noting that the peek cutout makes the inset a stacking context while it's open.

linear-gradient(to bottom right, transparent 50%, #000 50%),
linear-gradient(to bottom left, transparent 50%, #000 50%);
mask-position:
0 0,
var(--fork-peek-left) var(--fork-peek-top),
var(--fork-peek-tail-x) var(--fork-peek-tail-top),
calc(var(--fork-peek-tail-x) + var(--fork-peek-tail-width) / 2) var(--fork-peek-tail-top);
mask-size:
100% 100%,
var(--sidebar-width) calc(100% - var(--fork-peek-top) - var(--fork-peek-bottom)),
calc(var(--fork-peek-tail-width) / 2) var(--fork-peek-tail-height),
calc(var(--fork-peek-tail-width) / 2) var(--fork-peek-tail-height);
mask-repeat: no-repeat;
mask-composite: subtract, add, add, add;
}

:root[data-fork="noahhendrickson-t3code"]
Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/theme.custom.glass-surfaces.css
Original file line number Diff line number Diff line change
Expand Up @@ -679,9 +679,7 @@

/* Figma's 6% white, under the panel's real glass only. */
&[data-fork-sidebar-vibrancy="true"].dark[data-fork-theme="cool-darker"]
[data-slot="sidebar-wrapper"]:not([data-fork-sidebar-overlay="true"]):not(
[data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
)
[data-slot="sidebar-wrapper"]:not([data-fork-sidebar-overlay="true"])

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Active rows render differently depending on the chat column's width. The palettes docked block now has a separate peek arm, so the peek keeps the tint when narrowChatOverlay flags the collapsed wrapper. This 6% rule didn't get a matching arm, though.

With Glass, a narrow chat column (data-fork-sidebar-overlay="true" on the collapsed wrapper) and the peek open, the active thread and chrome rows fall back to the docked block's --sidebar-row-active (8% white). The same card in a wide window gets 6%.

Adding the same [data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"] arm here would make them match. This file would then belong back in the manifest's files list for fork-sidebar-peek.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 923e2f8. This rule now takes both arms of the docked predicate, so the active rows get 6% white whenever the tint applies. glass-surfaces is back in the manifest's files.

[data-sidebar-version="v2"]
:is([data-thread-item] > [aria-current="page"], [data-fork-chrome-row][data-active="true"]) {
background-color: rgb(255 255 255 / 6%);
Expand Down
18 changes: 10 additions & 8 deletions apps/web/src/theme.custom.palettes.css
Original file line number Diff line number Diff line change
Expand Up @@ -1235,11 +1235,16 @@
flag on the wrapper and `data-sidebar-version` sits on the sidebar below it,
hence the ancestor form. While it floats the palette's opaque #181b1e applies,
which is the correct read for a panel over content. The window keeps its
vibrancy either way; only this fill changes. */
vibrancy either way; only this fill changes. The collapsed sidebar's peek
card floats too but keeps the tint: theme.custom.css cuts the stage away
beneath it (fork-sidebar-peek), so the window is all that is behind it.
narrowChatOverlay can flag a collapsed wrapper too, so the peek arm stands
apart, and its extra attributes outrank the floating fill below. */
:root[data-fork="noahhendrickson-t3code"][data-fork-sidebar-vibrancy="true"].dark[data-fork-theme="cool-darker"]
[data-slot="sidebar-wrapper"]:not([data-fork-sidebar-overlay="true"]):not(
[data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
)
[data-slot="sidebar-wrapper"]:not([data-fork-sidebar-overlay="true"])
[data-sidebar-version="v2"],
:root[data-fork="noahhendrickson-t3code"][data-fork-sidebar-vibrancy="true"].dark[data-fork-theme="cool-darker"]
[data-slot="sidebar-wrapper"][data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
[data-sidebar-version="v2"] {
--background: var(--fork-glass-panel);
--card: var(--fork-glass-panel);
Expand Down Expand Up @@ -1315,10 +1320,7 @@
Opaque on purpose, so the "keep alpha" rule the docked block follows does not
apply here: alpha is exactly what this state gives up. */
:root[data-fork="noahhendrickson-t3code"][data-fork-sidebar-vibrancy="true"].dark[data-fork-theme="cool-darker"]
[data-slot="sidebar-wrapper"]:is(
[data-fork-sidebar-overlay="true"],
[data-sidebar-state="collapsed"][data-fork-sidebar-peek="open"]
)
[data-slot="sidebar-wrapper"][data-fork-sidebar-overlay="true"]
[data-sidebar-version="v2"] {
--background: #262626;
--card: #262626;
Expand Down
Loading