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
47 changes: 28 additions & 19 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@
- id: sidebar-v2-project-grouping
intent: >
Sidebar V2's active cards can be grouped under project headers, from a
switch at the top of the fork's project scope row. Upstream's flat list
switch at the top of the fork's projects filter menu. Upstream's flat list
stays the default, and the preference is device-local (localStorage) rather
than a field in packages/contracts.

Expand Down Expand Up @@ -791,6 +791,11 @@
surface grain: upstream's noise would compound unspecified drift onto every
chrome surface this palette freezes.

Dark --primary and --ring are white (#ffffff), not upstream's blue oklch —
focus underlines (border-ring), switches, and primary fills stay
monochrome. --primary-foreground is the stage (#191919) so white primary
buttons keep contrast. Cool Dark inherits these unless it overrides them.

Two blocks are required and the second is not redundant: upstream
re-declares --background, --card, --border and the whole --sidebar-* family
on the panel element itself, and a custom property set only at :root never
Expand Down Expand Up @@ -852,9 +857,10 @@
Sidebar V2 body type is 0.8125rem (13px at the default root) for project
headers and slim shelves. The panel remaps Tailwind's --text-xs and
--text-sm (and their --line-height companions) to 0.8125rem / 1rem so those
share one size without rewriting each className. Search and All projects
opt out at 0.875rem (14px) via CHROME_CONTROL; card titles and the
repo/branch line opt out with literal rem sizes (0.875rem / 0.75rem — see
share one size without rewriting each className. Search, New thread, Add
project, and the Projects label opt out at 0.875rem (14px) via CHROME_TYPE;
card titles and the repo/branch line opt out with literal rem sizes
(0.875rem / 0.75rem — see
sidebar-v2-card-rows) so the remap cannot flatten them. Rem, not px:
a px pin would freeze the sidebar while h-4 and the rest of the app still
honour the browser font-size setting. Line-height stays 1rem so the card's
Expand Down Expand Up @@ -925,21 +931,24 @@
one leaves the other subtracting from a stale number, and the cards drift
off the chrome icons' axis with nothing failing.

Chrome rows use ps-2 pe-3 with an inner px-1 control so their leading icon
also sits at 12px — matching card status and the group folder glyph. Search
and All projects controls are 28px tall (h-7), not Figma's 24, at 0.875rem
(14px) type — literal so the panel's 13px text-xs remap cannot shrink them.
Action rows (Search, New thread, Add project) use ps-2 pe-3 with an inner
px-1 control so their leading icon also sits at 12px — matching card status
and the group folder glyph. The Projects header uses symmetric px-3 (no
leading icon). Those controls are 32px tall (h-8), not Figma's 24, at
0.875rem (14px) type — literal so the panel's 13px text-xs remap cannot
shrink them.

These rows' own trailing inset is then 4px, and that 4px belongs to
sidebar-v2-row-action-hit-area, which owns the trailing glyph column's
single axis. It is not a scrollbar term and must not become one again.

The brand moves to the header's trailing edge, where it stops competing
with the toggle for the corner the eye starts at. The search and project
rows are 36px and 12px rather than 32px and 14px so they read as chrome
rather than as two more list items; the project row takes a leading
caret-up-down with no trailing chevron and no favicon, and its trailing
action is folder-open.
with the toggle for the corner the eye starts at. Search, New thread, and
Add project are labeled action rows; Projects is a static muted label
whose trailing ListFilter (FunnelSimple) opens the scope / group-by menu
that used to live on the clickable projects control. When a project scope
is active the funnel lifts to foreground (and names the scope in its
aria-label / tooltip) so a filtered list is never a silent mode.

The Dev channel's header art is the designer's own dither PNG instead of
upstream's blue blueprint, painted as a covering background rather than a
Expand Down Expand Up @@ -1012,12 +1021,12 @@

- id: fork-workspace-header
intent: >
The workspace topbar is 56px. One token drives both it and the sidebar
header; the design draws them at 56 and 52, but they sit side by side
across a 1px divider, so a 4px split would misalign the seam. Read as
unintentional and kept equal. The .wco rule re-derives the token from
env(titlebar-area-height), so on Windows with Controls Overlay the OS still
dictates the height and only the fallback moves.
The workspace topbar is 52px. One token (--workspace-topbar-height) drives
both it and the sidebar header so they stay seam-aligned across the
divider. Settings and DiffPanelShell already hardcode the same 52px. The
.wco rule re-derives the token from env(titlebar-area-height), so on
Windows with Controls Overlay the OS still dictates the height and only
the fallback moves.

Project and title swap weight: the project is the fixed, repeating half, so
it takes semibold and becomes the landmark you navigate to, while the title
Expand Down
48 changes: 38 additions & 10 deletions apps/web/src/__fork_guards__/forkSidebarChrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,23 @@ describe("fork guard: fork-sidebar-chrome", () => {
});

it("keeps the search and project rows fork-owned", () => {
// ~150 lines of pure presentation. Fenced in place it left SidebarV2
// carrying the whole rewrite; here the fence is two call sites.
// Pure presentation. Fenced in place it left SidebarV2 carrying the whole
// rewrite; here the fence is two call sites (actions + projects filter).
const sidebarV2 = readSibling("../components/SidebarV2.tsx");
expect(sidebarV2).toContain("<SidebarV2SearchRow");
expect(sidebarV2).toContain("<SidebarV2ChromeActionRows");
expect(sidebarV2).toContain("<SidebarV2ProjectScopeRow");
expect(sidebarV2).not.toContain('aria-label="Filter threads by project"');
expect(sidebarV2).toContain("scopedProjectDisplayName=");
expect(sidebarV2).not.toContain('data-testid="command-palette-trigger"');
const rows = readSibling("../custom/SidebarV2ChromeRows.tsx");
// Composition / labels / testids — not internal row export names that a
// helper collapse is free to erase.
expect(rows).toContain("function ChromeLabeledAction");
expect(rows).toContain('data-testid="command-palette-trigger"');
expect(rows).toContain('testId="sidebar-v2-new-thread"');
expect(rows).toContain('testId="sidebar-v2-add-project"');
expect(rows).toContain('data-testid="sidebar-v2-project-filter"');
expect(rows).toContain("New thread");
expect(rows).toContain("Add project");
});

it("pays for the thread list's scroll gutter out of its own end padding", () => {
Expand Down Expand Up @@ -245,12 +255,30 @@ describe("fork guard: fork-sidebar-chrome", () => {
expect(rows.split("CHROME_ROW_ICON_TINT").length - 1).toBeGreaterThanOrEqual(3);
});

it("keeps Search and All projects at 14px, outside the panel's 13px remap", () => {
it("keeps chrome type at 14px for actions and the Projects label", () => {
const rows = readSibling("../custom/SidebarV2ChromeRows.tsx");
const type = /const CHROME_TYPE\s*=\s*"([^"]+)"/u.exec(rows)?.[1];
expect(type).toBeDefined();
expect(type).toContain("text-[0.875rem]");
expect(type).not.toMatch(/\btext-xs\b/u);
expect(type).not.toMatch(/\btext-sm\b/u);
// Both the interactive control and the static Projects label read it.
expect(rows).toContain("CHROME_TYPE");
expect(rows.split("CHROME_TYPE").length - 1).toBeGreaterThanOrEqual(3);
});

it("keeps Projects as a static label with an active-aware filter funnel", () => {
const rows = readSibling("../custom/SidebarV2ChromeRows.tsx");
const className = /const CHROME_CONTROL[\s\S]*?"([^"]+)"/u.exec(rows)?.[1];
expect(className).toBeDefined();
expect(className).toContain("text-[0.875rem]");
expect(className).not.toMatch(/\btext-xs\b/u);
expect(className).not.toMatch(/\btext-sm\b/u);
expect(rows).toMatch(/>\s*Projects\s*</u);
expect(rows).toContain("ListFilterIcon");
expect(rows).toContain("FolderPlusIcon");
// Scope on-state: glyph lifts and aria/tooltip name the active project.
expect(rows).toContain("data-active={isScoped");
expect(rows).toContain("scopedProjectDisplayName");
expect(rows).toContain("Filter threads by project — showing");
expect(rows).toContain("TooltipPopup");
// The label is not a menu trigger — the funnel owns the menu.
expect(rows).not.toContain("ChevronsUpDownIcon");
expect(rows).not.toContain("FolderOpenIcon");
});
});
11 changes: 11 additions & 0 deletions apps/web/src/__fork_guards__/forkSurfacePalette.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ describe("fork guard: fork-surface-palette", () => {
expect(blockFor(theme, STAGE)).toContain("--background: #191919");
});

it("keeps dark primary and ring monochrome white", () => {
// Upstream's dark --primary / --ring are the same blue oklch. Focus
// underlines (border-ring on Search refs, etc.), switches, and primary
// fills must stay white in the fork — losing these drops blue accents
// back into an otherwise monochrome shell.
const stage = blockFor(theme, STAGE);
expect(stage).toContain("--primary: #ffffff");
expect(stage).toContain("--primary-foreground: #191919");
expect(stage).toContain("--ring: #ffffff");
});

it("keeps the panel above the stage, as the intent names", () => {
// Intent: fork-surface-palette. Assert the ordering, not only the
// constants — a value tweak is fine, silently inverting the hierarchy is
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/__fork_guards__/forkWorkspaceHeader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ describe("fork guard: fork-workspace-header", () => {
expect(theme).toMatch(/--fork-pill-fg:\s*var\(--foreground\)/u);
});

it("raises the topbar to 56px in both the default and WCO derivations", () => {
// Two declarations, and missing the second leaves Windows Controls Overlay
// builds 4px short of every other platform.
expect(upstreamCss).toMatch(/--workspace-topbar-height:\s*56px/u);
it("keeps the topbar at 52px in both the default and WCO derivations", () => {
// One token for sidebar header and workspace header — a drift between the
// two misaligns the seam. Missing the WCO fallback leaves Windows Controls
// Overlay builds on a different height than every other platform.
expect(upstreamCss).toMatch(/--workspace-topbar-height:\s*52px/u);
expect(upstreamCss).toMatch(
/--workspace-topbar-height:\s*env\(titlebar-area-height,\s*56px\)/u,
/--workspace-topbar-height:\s*env\(titlebar-area-height,\s*52px\)/u,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const sidebar = readSibling("../components/SidebarV2.tsx");
const chromeRows = readSibling("../custom/SidebarV2ChromeRows.tsx");

describe("fork guard: sidebar-v2-project-grouping", () => {
it("offers the switch from the project scope menu", () => {
it("offers the switch from the projects filter menu", () => {
expect(chromeRows).toContain("Group by project");
expect(chromeRows).toContain("onGroupByProjectChange");
// Toggling a view preference must not dismiss the menu it lives in.
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/__fork_guards__/sidebarV2RowActionHitArea.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,14 @@ describe("fork guard: sidebar-v2-row-action-hit-area", () => {
// 16px icons, matching the marks they line up with. Asserted positively:
// a negative on `size-5` is anchored to class order and walks straight
// through `className="shrink-0 size-5"`.
for (const name of ["PlusCircleIcon", "FolderOpenIcon"]) {
const tag = new RegExp(`<${name} className="([^"]*)"`, "u").exec(chromeRows)?.[1];
expect(tag, `${name} is not rendered in the chrome rows`).toBeDefined();
expect(tag).toMatch(/\bsize-4\b/u);
}
// New thread / Add project render through ChromeLabeledAction's Icon slot;
// the filter still mounts ListFilterIcon directly. Pin size-4 either way.
expect(chromeRows).toContain("icon={PlusCircleIcon}");
expect(chromeRows).toContain("icon={FolderPlusIcon}");
const labeledIcon = /<Icon className="([^"]*)"/u.exec(chromeRows)?.[1];
expect(labeledIcon, "ChromeLabeledAction icon class missing").toBeDefined();
expect(labeledIcon).toMatch(/\bsize-4\b/u);
expect(chromeRows).toMatch(/<ListFilterIcon[\s\S]{0,120}?\bsize-4\b/u);
// The shelf headers' chevrons, 4px the other way: their row is px-2.5,
// so a flush 12px glyph centres 6px in where a card's trailing box takes 8.
const chevrons = [...sidebarV2.matchAll(/"size-3 ([^"]*transition-transform[^"]*)"/gu)];
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ import {
type SidebarV2DotTone,
} from "~/custom/SidebarV2StatusIndicator";
import { SidebarV2ThreadCardMeta, threadCardShowsMetaRow } from "~/custom/SidebarV2ThreadCardMeta";
import { SidebarV2ProjectScopeRow, SidebarV2SearchRow } from "~/custom/SidebarV2ChromeRows";
import { SidebarV2ChromeActionRows, SidebarV2ProjectScopeRow } from "~/custom/SidebarV2ChromeRows";
import {
threadCardTitleClassName,
threadCardTitleRecedes,
Expand Down Expand Up @@ -2915,25 +2915,25 @@ export default function SidebarV2() {
<SidebarChromeHeader isElectron={isElectron} />
<SidebarContent className="gap-0">
{/* fork:begin fork-sidebar-chrome — see .fork/customizations.yaml#fork-sidebar-chrome
Both control rows are fork-owned; only these call sites live here.
Control rows are fork-owned; only these call sites live here.
See custom/SidebarV2ChromeRows.tsx. */}
<SidebarV2SearchRow
<SidebarV2ChromeActionRows
commandPaletteShortcutLabel={commandPaletteShortcutLabel}
newThreadShortcutLabel={newThreadShortcutLabel}
newThreadDisabled={projects.length === 0}
onNewThread={handleNewThreadClick}
onAddProject={openAddProjectCommandPalette}
/>
<SidebarV2ProjectScopeRow
projectGroups={projectGroups}
scopedProjectGroup={scopedProjectGroup}
projectScopeKey={projectScopeKey}
scopedProjectDisplayName={scopedProjectGroup?.displayName ?? null}
onProjectScopeChange={setProjectScopeKey}
menuOpen={projectScopeMenuOpen}
onMenuOpenChange={setProjectScopeMenuOpen}
onProjectActions={(event, project) => {
void handleProjectActions(event, project);
}}
onAddProject={openAddProjectCommandPalette}
groupByProject={groupByProject}
onGroupByProjectChange={setGroupByProject}
// A switch that visibly does nothing teaches nothing. Where headers
Expand Down
Loading
Loading