Files
Jyotisha/frontend/tests/sidebar-contract.test.ts
T
Jesse_Chen c2d705ef6e
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(frontend): add a warm dark theme and close the DESIGN.md drift
The palette is restated for dark rather than inverted: elevation reads
through lightness on dark and through darkness on light, so the floor is
the darkest surface here and the second-lightest there. The clay hue is
kept and lifted, because #85432f is 2.1:1 on a dark ground. All 37
themeable tokens are covered, in an OS-preference block and a data-theme
block that a contract test keeps identical, and ink, action, danger,
success and warning are asserted at 4.5:1 against the dark canvas.

Four raw colors that would have stayed light-theme values are tokenised
(the avatar hairline, the sheen sweep, a one-off shadow, a literal
warning hex). The QR keeps literal white in both themes, since scanners
need light modules to be light, and print keeps white paper.

The four root boundary pages cannot read a token, so they restate the
handful they need in both themes. forbidden.tsx also stops painting a
bespoke near-black page in four colours that appear nowhere in the
palette, which broke the rule that dark ink is never a page-scale
surface.

Also fixes what the audit found in DESIGN.md itself: two ink values that
had drifted from the code, a motion tier documented at 360ms that was
never implemented, a breakpoint section claiming three tiers where the
stylesheet has eleven, an undocumented report-paper palette, and an admin
section describing a bespoke panel that antd + Refine replaced. Five
zero-reference admin rules go with it.

The sidebar gets the accent, opaque drawer, heading rank and empty-state
guidance settled earlier, and fenced code blocks finally get a container.

BUG-439.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-08-29 12:01:50 +00:00

452 lines
26 KiB
TypeScript

import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import test from "node:test";
import { cssDeclarations } from "./css-contract-test-support.ts";
const projectFile = (path: string) => new URL(`../${path}`, import.meta.url);
const readProjectFile = (path: string) => readFileSync(projectFile(path), "utf8");
const globalStyles = readProjectFile("src/app/globals.css");
const cssBlock = (selector: string) => cssDeclarations(selector, globalStyles);
/** The base-layer rule only — media-query copies of a selector are indented. */
const topLevelRule = (selector: string) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const hit = globalStyles.match(new RegExp(`^${escaped}\\s*\\{([^}]*)\\}`, "m"));
assert.ok(hit, `no base-layer rule for ${selector}`);
return hit[1];
};
test("sidebar nav items read left-aligned everywhere; centering is the desktop rail only", () => {
// `data-state` tracks the DESKTOP open state, so it cannot decide layout on
// the mobile drawer — that drawer is always expanded no matter what the
// desktop rail is doing. Centering as the base layer, flipped back by a
// `[data-state="expanded"]` override, left 新建对话 / 我的报告 floating in
// the middle of the drawer on phones while every other row sat flush left.
for (const selector of [".new-chat", ".report-nav-button"]) {
assert.match(topLevelRule(selector), /justify-content:\s*flex-start/);
assert.doesNotMatch(topLevelRule(selector), /justify-content:\s*center/);
}
assert.doesNotMatch(globalStyles, /\[data-state="expanded"\][^{]*\.(?:new-chat|report-nav-button)/);
// The rail lives at >=768px, beside the other collapsed-state rules.
assert.match(
globalStyles,
/@media\s*\(min-width:\s*768px\)[\s\S]*\[data-state="collapsed"\]\s+\.new-chat,\s*\[data-state="collapsed"\]\s+\.report-nav-button\s*\{[^}]*justify-content:\s*center/,
);
});
test("新建对话 carries the sidebar's only accent; 我的报告 stays neutral", () => {
// The sidebar had no accent pixel at all in the empty state: its only two
// action-colored surfaces are the 2px active-session bar and the terracotta
// profile initial, and neither renders with no sessions and an uploaded
// avatar. DESIGN.md keeps the action color scarce, so exactly one row gets
// it — as tinted text, not a filled block, so the surface stays light.
assert.match(topLevelRule(".new-chat"), /color:\s*var\(--color-action\)/);
assert.doesNotMatch(topLevelRule(".new-chat"), /background:\s*var\(--color-action\)/);
assert.match(topLevelRule(".report-nav-button"), /color:\s*var\(--sidebar-foreground\)/);
assert.doesNotMatch(topLevelRule(".report-nav-button"), /--color-action/);
// Hover deepens the same hue rather than reverting to ink.
for (const body of [...globalStyles.matchAll(/\.new-chat(?::not\(:disabled\))?:hover\s*\{([^}]*)\}/g)]) {
assert.match(body[1], /color:\s*var\(--color-action-hover\)/);
}
});
test("the mobile drawer is opaque; the glass surface is a desktop treatment", () => {
// rgba(235,233,227,.86) assumes a light backdrop. On mobile the drawer sits
// above the scrim, which showed through and pulled #EBE9E3 down to #E3E1DC.
assert.match(
globalStyles,
/@media\s*\(max-width:\s*767px\)[\s\S]*\[data-sidebar="sidebar"\]\s*\{[^}]*background:\s*var\(--sidebar-solid\)[^}]*backdrop-filter:\s*none/,
);
// Desktop keeps the glass.
assert.match(topLevelRule(".sidebar"), /background:\s*var\(--sidebar-background\)/);
assert.match(topLevelRule(".sidebar"), /backdrop-filter:\s*saturate/);
});
test("a section heading outranks its own body copy", () => {
// Headings and their empty-state copy both sat on ink-tertiary, so the nav
// read as one flat grey with no hierarchy to scan.
const heading = cssDeclarations(".sidebar-label, .sidebar-section-summary", globalStyles);
assert.match(heading, /color:\s*var\(--color-ink-secondary\)/);
assert.doesNotMatch(topLevelRule(".sidebar-empty"), /--color-ink-secondary/);
assert.match(topLevelRule(".sidebar-empty"), /color:\s*var\(--color-ink-tertiary\)/);
});
test("empty sidebar sections name the next step instead of dead-ending", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
// Neutral on purpose: 新建对话 already carries the accent a few rows above,
// and a second tinted call to action would compete with it.
assert.match(appSidebar, /还没有收藏,可在对话的「更多操作」里收藏/);
assert.match(appSidebar, /暂无对话,点上方「新建对话」开始/);
assert.doesNotMatch(topLevelRule(".sidebar-empty"), /--color-action/);
});
test("provides the generic composable sidebar primitive", () => {
assert.equal(existsSync(projectFile("src/components/ui/sidebar.tsx")), true);
});
test("exports only the retained sidebar composition surface", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
for (const name of [
"SidebarProvider", "Sidebar", "SidebarHeader", "SidebarContent",
"SidebarGroup", "SidebarGroupLabel", "SidebarGroupContent",
"SidebarMenu", "SidebarMenuItem", "SidebarMenuButton",
"SidebarFooter", "SidebarInset", "SidebarTrigger", "SidebarRail", "useSidebar",
]) {
assert.match(sidebar, new RegExp(`export (?:function|const) ${name}\\b`));
}
assert.doesNotMatch(sidebar, /SidebarMenuBadge|SidebarMenuSkeleton|SidebarMenuSub|side\?:|variant\?:/);
});
test("keeps sidebar behavior in the generic primitive", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /useSidebarViewport/);
assert.match(sidebar, /defaultSidebarOpen/);
assert.match(sidebar, /shouldHandleSidebarShortcut/);
assert.match(sidebar, /data-state/);
assert.match(sidebar, /data-viewport/);
assert.match(sidebar, /data-mobile-open/);
assert.match(sidebar, /addEventListener\("keydown"/);
assert.match(sidebar, /preventDefault\(\)/);
assert.match(sidebar, /viewport === "mobile" \|\| !openMobile\) return;[\s\S]*setOpenMobile\(false\)/);
assert.match(sidebar, /@base-ui\/react\/tooltip/);
assert.doesNotMatch(sidebar, /Sheet/);
assert.match(sidebar, /cn\(/);
assert.doesNotMatch(sidebar, /#[0-9a-fA-F]{3,8}|hsl\(/);
});
test("provides the mobile drawer closing surface", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /data-sidebar="scrim"/);
assert.match(sidebar, /data-slot="sidebar-scrim"/);
assert.match(sidebar, /aria-label="关闭聊天记录"/);
assert.match(sidebar, /onClick=\{\(\) => setOpenMobile\(false\)\}/);
assert.match(sidebar, /isMobile && openMobile \? <button/);
});
test("keeps the sidebar subtree stable while toggling its mobile scrim", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /return <>\s*\{sidebar\}\s*\{isMobile && openMobile \? <button/);
});
test("cancels a stale mobile drawer focus frame", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /const focusDrawer = window\.requestAnimationFrame/);
assert.match(sidebar, /sidebarSurfaceRef\.current\?\.focus\(\)/);
assert.match(sidebar, /return \(\) => window\.cancelAnimationFrame\(focusDrawer\);/);
});
test("uses the approved localized trigger actions", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /"收起侧边栏"/);
assert.match(sidebar, /"展开侧边栏"/);
assert.match(sidebar, /"打开聊天记录"/);
assert.match(sidebar, /"关闭聊天记录"/);
assert.match(sidebar, /PanelLeft/);
assert.doesNotMatch(sidebar, /PanelLeftClose|PanelLeftOpen/);
assert.match(sidebar, /<PanelLeft aria-hidden="true" \/>/);
});
test("keeps provider primitive defaults and consumer handlers composable", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(sidebar, /id=\{id \?\? "chat-sidebar"\}/);
assert.match(sidebar, /if \(viewport === "mobile" \|\| !openMobile\) return;/);
assert.match(sidebar, /onClick\?\.\(event\);\s*if \(!event\.defaultPrevented\) setOpen\(!open\);/);
});
test("retains viewport state within an unchanged breakpoint", () => {
const viewportHook = readProjectFile("src/hooks/use-sidebar-viewport.ts");
assert.match(viewportHook, /previous\.ready && previous\.viewport === viewport \? previous : \{ viewport, ready: true \}/);
});
test("documents the sidebar shell design contract", () => {
const design = readProjectFile("DESIGN.md");
assert.match(design, /### Sidebar shell/);
assert.match(design, /Scroll ownership/);
assert.match(design, /session-local/);
assert.match(design, /single visible collapse\/expand trigger/);
assert.match(design, /Sidebar state changes are immediate/);
});
test("composes the Jyotisha app sidebar from the generic shell", () => {
assert.equal(existsSync(projectFile("src/components/app-sidebar.tsx")), true);
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
for (const component of ["SidebarHeader", "SidebarContent", "SidebarFooter", "SidebarRail"]) {
assert.match(appSidebar, new RegExp(`<${component}\\b`));
}
});
test("keeps the sidebar brand row free of a duplicate collapse trigger", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.doesNotMatch(appSidebar, /SidebarTrigger/);
});
test("uses one collapsed history action instead of icon-only session rows", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.match(appSidebar, /MessageSquareText/);
assert.match(appSidebar, /state === "collapsed" && !isMobile/);
assert.match(appSidebar, /favoriteSessions\.map/);
assert.match(appSidebar, /historySessions\.map/);
assert.match(appSidebar, /星盘列表/);
assert.match(appSidebar, /收藏对话/);
assert.match(appSidebar, /历史对话/);
assert.doesNotMatch(appSidebar, /sessions\.map\([^)]*\)\s*=>\s*<[^>]+aria-label=/);
});
test("keeps session navigation independent of request state", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.match(appSidebar, /onSelectSession\(session\.id\)/);
assert.doesNotMatch(appSidebar, /pendingSession|isLoading|cancellationPending|requestPending/);
});
test("uses portaled Base UI menus with safe collision padding", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
const sessionRow = readProjectFile("src/components/sidebar-session-row.tsx");
assert.match(appSidebar, /import \{ Menu \} from "@base-ui\/react\/menu"/);
assert.match(appSidebar, /<Menu\.Portal>/);
assert.match(appSidebar, /collisionPadding=\{12\}/);
assert.match(appSidebar, /<Menu\.Popup className="account-menu-popup"/);
assert.match(sessionRow, /import \{ Menu \} from "@base-ui\/react\/menu"/);
assert.match(sessionRow, /<Menu\.Portal>/);
assert.match(sessionRow, /<Menu\.Popup className="session-actions"/);
assert.doesNotMatch(sessionRow, /role="menu(?:item)?"/);
});
test("closes an open account menu when the sidebar viewport or state changes", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.match(appSidebar, /const \{[^}]*\bviewport\b[^}]*\} = useSidebar\(\)/);
assert.match(appSidebar, /const menuPlacement = `\$\{viewport\}:\$\{state\}`/);
assert.match(appSidebar, /previousMenuPlacement/);
assert.match(appSidebar, /previousMenuPlacement\.current !== menuPlacement && accountMenuOpen/);
});
test("keeps app sidebar props as product data and callbacks", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.match(appSidebar, /export type AppSidebarProps/);
assert.match(appSidebar, /onSelectSession: \(sessionId: string\) => void/);
assert.match(appSidebar, /onSelectChart: \(chartId: string\) => void/);
assert.match(appSidebar, /charts: readonly SidebarChart\[\]/);
assert.doesNotMatch(appSidebar, /supabase|fetch\(|\/api\//i);
});
test("removes the user-facing admin entry and routes credit control to membership", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
const page = readProjectFile("src/app/page.tsx");
assert.doesNotMatch(appSidebar, /adminUrl|account\.isAdmin|后台管理|KeyRound/);
assert.doesNotMatch(page, /admin-button|ShieldCheck|后台管理/);
assert.doesNotMatch(page, /account\.isAdmin && account\.adminUrl/);
assert.match(page, /className="chat-header-actions"/);
assert.match(page, /className="credit-button"/);
assert.match(page, /membershipHref\("credits"\)/);
});
test("composes the chat page with the app sidebar shell", () => {
const page = readProjectFile("src/app/page.tsx");
assert.match(page, /const modalOpen = activeAccountDialog !== null \|\| onboardingPaywallOpen/);
assert.match(page, /<SidebarProvider escapeBlocked=\{accountMenuOpen \|\| modalOpen\}>/);
assert.match(page, /<main className="chat-app">[\s\S]*<AppSidebar\b/);
assert.match(
page,
/<SidebarInset className=\{`chat-panel\$\{rectificationSurfaceOpen \? " is-rectification" : ""\}`\} inert=\{modalOpen\}>/,
);
assert.match(page, /<SidebarTrigger placement="inset" \/>/);
});
test("removes page-local mobile sidebar ownership", () => {
const page = readProjectFile("src/app/page.tsx");
assert.doesNotMatch(page, /mobileSidebarOpen|setMobileSidebarOpen/);
assert.doesNotMatch(page, /className="sidebar-backdrop"/);
assert.doesNotMatch(page, /<aside className="sidebar"/);
assert.doesNotMatch(page, /className="mobile-menu"/);
});
test("blocks the provider mobile Escape action behind layered account UI", () => {
const page = readProjectFile("src/app/page.tsx");
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
assert.match(page, /escapeBlocked=\{accountMenuOpen \|\| modalOpen\}/);
assert.match(sidebar, /event\.key === "Escape" && isMobile && openMobile && !escapeBlocked/);
});
test("keeps the page session selection callback free of request locks", () => {
const page = readProjectFile("src/app/page.tsx");
const selectSession = page.match(/function selectSession\(sessionId: string\) \{([\s\S]*?)\n \}/);
assert.ok(selectSession);
assert.match(selectSession[1], /setActiveSessionId\(sessionId\)/);
assert.match(selectSession[1], /setDraft\(""\)/);
assert.match(selectSession[1], /setComposerNotice\(""\)/);
assert.doesNotMatch(selectSession[1], /pendingSessionId|isLoading|cancellationPending|creatingSession/);
});
test("maps the sidebar semantic aliases and responsive dimensions", () => {
for (const declaration of [
"--sidebar-background: var(--color-sidebar);",
"--sidebar-solid: var(--color-sidebar-solid);",
"--sidebar-foreground: var(--color-ink);",
"--sidebar-muted-foreground: var(--color-ink-secondary);",
"--sidebar-accent: var(--color-selected);",
"--sidebar-accent-foreground: var(--color-ink);",
"--sidebar-border: var(--color-border);",
"--sidebar-ring: var(--color-focus);",
"--sidebar-primary: var(--color-surface-dark);",
"--sidebar-primary-foreground: var(--color-on-dark);",
"--sidebar-width-desktop: 288px;",
"--sidebar-width-tablet: 240px;",
"--sidebar-width-icon: 64px;",
"--sidebar-width-mobile: min(86vw, 320px);",
]) {
assert.equal(globalStyles.includes(declaration), true, `missing ${declaration}`);
}
});
test("selects desktop and tablet shell widths from provider data", () => {
assert.match(globalStyles, /\.group\\\/sidebar-provider\[data-viewport\][^{]*\{[^}]*height:\s*100dvh/);
assert.match(globalStyles, /\[data-viewport="desktop"\]\[data-state="expanded"\]\s+\.chat-app\s*\{[^}]*grid-template-columns:\s*var\(--sidebar-width-desktop\)\s+minmax\(0,\s*1fr\)/);
assert.match(globalStyles, /\[data-viewport="tablet"\]\[data-state="expanded"\]\s+\.chat-app\s*\{[^}]*grid-template-columns:\s*var\(--sidebar-width-tablet\)\s+minmax\(0,\s*1fr\)/);
assert.match(globalStyles, /\[data-state="collapsed"\]\s+\.chat-app\s*\{[^}]*grid-template-columns:\s*var\(--sidebar-width-icon\)\s+minmax\(0,\s*1fr\)/);
assert.doesNotMatch(globalStyles, /transition:[^;}]*\b(?:width|grid-template-columns)\b/);
});
test("changes sidebar state without transition frames", () => {
const sidebar = readProjectFile("src/components/ui/sidebar.tsx");
const design = readProjectFile("DESIGN.md");
assert.doesNotMatch(sidebar, /document\.startViewTransition/);
assert.doesNotMatch(sidebar, /skipMotion/);
assert.match(sidebar, /const setOpen = useCallback\(\(nextOpen: boolean\) => \{\s*commitOpen\(nextOpen\);/);
assert.match(sidebar, /else commitOpen\(!open\);/);
assert.doesNotMatch(globalStyles, /view-transition-name/);
assert.doesNotMatch(globalStyles, /::view-transition-/);
assert.match(globalStyles, /\[data-sidebar="trigger"\]\s+svg\s*\{[^}]*width:\s*18px[^}]*height:\s*18px/);
assert.doesNotMatch(cssBlock('[data-sidebar="trigger"]'), /transition:/);
assert.match(globalStyles, /\[data-sidebar="sidebar"\]\[data-mobile-open="false"\][^{]*\{[^}]*visibility:\s*hidden[^}]*transform:\s*translateX\(-100%\)/);
assert.match(design, /Sidebar state changes are immediate/);
});
test("keeps the chat title in the flexible left-aligned header column", () => {
const page = readProjectFile("src/app/page.tsx");
const header = page.slice(page.indexOf('<header className="chat-header">'), page.indexOf("</header>", page.indexOf('<header className="chat-header">')));
assert.match(cssBlock(".chat-header"), /grid-template-columns:\s*auto\s+minmax\(0,\s*1fr\)\s+auto/);
assert.match(cssBlock(".chat-header"), /text-align:\s*left/);
assert.doesNotMatch(cssBlock(".chat-header"), /justify-content:\s*space-between/);
assert.match(header, /<strong>\{activeSession\?\.title \|\| "新对话"\}<\/strong>/);
assert.doesNotMatch(header, /status-loading|基于星盘证据回答|回答一般占星知识|正在校正出生时间/);
});
test("anchors the account footer to the bottom edge without trailing sidebar padding", () => {
assert.match(globalStyles, /\.sidebar \{ position:[^}]*padding:\s*var\(--space-5\)\s+var\(--space-3\)\s+0/);
assert.match(cssBlock(".sidebar-footer"), /margin-top:\s*0/);
});
test("makes SidebarContent the only sidebar scroll owner", () => {
assert.match(cssBlock('[data-sidebar="header"]'), /flex:\s*0\s+0\s+auto/);
assert.match(cssBlock('[data-sidebar="content"]'), /min-height:\s*0/);
assert.match(cssBlock('[data-sidebar="content"]'), /overflow-y:\s*auto/);
assert.match(cssBlock('[data-sidebar="footer"]'), /flex:\s*0\s+0\s+auto/);
assert.doesNotMatch(cssBlock(".session-list"), /overflow(?:-y)?:\s*auto/);
assert.match(readProjectFile("src/components/sidebar-session-row.tsx"), /className="session-title"[\s\S]*className="truncate"/);
assert.match(globalStyles, /\[data-active="true"\][^{]*\{[^}]*background:\s*var\(--sidebar-accent\)/);
});
test("styles the session history scrollbar as a quiet overlay", () => {
const content = cssBlock('[data-sidebar="content"]');
assert.match(content, /scrollbar-width:\s*thin/);
assert.match(content, /scrollbar-color:\s*transparent\s+transparent/);
assert.doesNotMatch(content, /scrollbar-gutter/);
assert.match(cssBlock('[data-sidebar="content"]:hover, [data-sidebar="content"]:focus-within'), /scrollbar-color:\s*color-mix\(in srgb,\s*var\(--color-ink\)\s+26%,\s*transparent\)\s+transparent/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar'), /width:\s*10px/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar'), /background:\s*transparent/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-button'), /display:\s*none/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-track, [data-sidebar="content"]::-webkit-scrollbar-corner'), /background:\s*transparent/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb'), /background-color:\s*transparent/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb'), /border:\s*3px\s+solid\s+transparent/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb'), /background-clip:\s*content-box/);
assert.match(cssBlock('[data-sidebar="content"]:hover::-webkit-scrollbar-thumb, [data-sidebar="content"]:focus-within::-webkit-scrollbar-thumb'), /background-color:\s*color-mix\(in srgb,\s*var\(--color-ink\)\s+26%,\s*transparent\)/);
assert.match(cssBlock('[data-sidebar="content"]::-webkit-scrollbar-thumb:hover'), /background-color:\s*color-mix\(in srgb,\s*var\(--color-ink\)\s+40%,\s*transparent\)/);
assert.match(globalStyles, /@media\s*\(prefers-contrast:\s*more\)[\s\S]*\[data-sidebar="content"\][^{]*\{[^}]*scrollbar-color:\s*var\(--color-ink-secondary\)\s+transparent/);
assert.match(globalStyles, /@media\s*\(forced-colors:\s*active\)[\s\S]*\[data-sidebar="content"\][^{]*\{[^}]*scrollbar-color:\s*auto/);
assert.match(readProjectFile("DESIGN.md"), /quiet overlay scrollbar/);
assert.match(readProjectFile("DESIGN.md"), /ordinary session/);
});
test("nests sidebar lists under one heading scale without an archive toggle", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.doesNotMatch(appSidebar, /归档 \$\{/);
assert.doesNotMatch(appSidebar, /onToggleArchivedView/);
assert.match(appSidebar, /className="sidebar-nested"/);
assert.match(cssBlock(".sidebar-nested"), /calc\(var\(--space-3\) \+ 18px \+ var\(--space-2\)\)/);
assert.match(cssBlock(".session-title"), /font-size:\s*var\(--type-caption\)/);
assert.match(cssBlock(".session-title"), /font-weight:\s*500/);
assert.match(cssBlock(".session-row"), /color:\s*var\(--sidebar-foreground\)/);
// Was ink-tertiary, identical to .sidebar-empty below it, which left the
// headings and their own body copy indistinguishable. The shared scale is
// what this test guards; the rank between the two levels is asserted in
// "a section heading outranks its own body copy".
assert.match(cssBlock(".sidebar-label"), /color:\s*var\(--color-ink-secondary\)/);
assert.match(cssBlock(".sidebar-section-summary"), /color:\s*var\(--color-ink-secondary\)/);
assert.match(cssBlock(".sidebar-label"), /font-size:\s*var\(--type-caption\)/);
assert.match(cssBlock(".sidebar-section-summary"), /font-size:\s*var\(--type-caption\)/);
assert.match(cssBlock(".new-chat"), /font-size:\s*var\(--type-body-sm\)/);
assert.match(cssBlock(".report-nav-button"), /font-size:\s*var\(--type-body-sm\)/);
assert.match(cssBlock(".new-chat svg"), /width:\s*18px/);
assert.match(cssBlock(".report-nav-button svg"), /width:\s*18px/);
assert.match(cssBlock(".sidebar-label > svg"), /width:\s*18px/);
assert.match(cssBlock(".sidebar-section-summary > svg"), /width:\s*18px/);
assert.match(cssBlock(".session-title > svg"), /color:\s*currentColor/);
assert.doesNotMatch(appSidebar, /session-nav-header-inline/);
});
test("renders each session title and menu as one unified row surface", () => {
assert.match(cssBlock(".session-row"), /grid-template-columns:\s*minmax\(0,\s*1fr\)\s+44px/);
assert.match(globalStyles, /\.session-row:has\(\.session-main\[data-active="true"\]\)[^{]*\{[^}]*background:\s*var\(--sidebar-accent\)/);
assert.match(cssBlock('.session-main[data-active="true"]'), /background:\s*transparent/);
assert.match(cssBlock(".session-menu-trigger"), /border-radius:\s*0/);
assert.doesNotMatch(cssBlock(".session-menu-trigger"), /border-radius:\s*50%/);
});
test("styles the non-mobile collapsed rail without repeated session rows", () => {
assert.match(globalStyles, /@media\s*\(min-width:\s*768px\)[\s\S]*\[data-state="collapsed"\][^{]*\.brand-row/);
assert.match(globalStyles, /\[data-state="collapsed"\][^{]*\[data-sidebar="menu-button"\][^{]*\{[^}]*width:\s*44px/);
assert.match(globalStyles, /\[data-state="collapsed"\][^{]*\.profile-trigger[^{]*\{[^}]*width:\s*44px/);
assert.match(globalStyles, /\[data-sidebar="rail"\][^{]*\{[^}]*position:\s*absolute[^}]*width:\s*var\(--space-2\)/);
assert.match(globalStyles, /\[data-sidebar="rail"\]:focus-visible[^{]*\{[^}]*outline/);
});
test("uses provider attributes for the mobile drawer and scrim", () => {
assert.match(globalStyles, /@media\s*\(max-width:\s*767px\)[\s\S]*\.chat-app\s*\{[^}]*grid-template-columns:\s*1fr/);
assert.match(globalStyles, /\[data-sidebar="sidebar"\][^{]*\{[^}]*position:\s*fixed[^}]*width:\s*var\(--sidebar-width-mobile\)/);
assert.match(globalStyles, /\[data-sidebar="sidebar"\]\[data-mobile-open="false"\][^{]*\{[^}]*transform:\s*translateX\(-100%\)/);
assert.match(globalStyles, /\[data-sidebar="sidebar"\]\[data-mobile-open="true"\][^{]*\{[^}]*visibility:\s*visible[^}]*transform:\s*translateX\(0\)/);
assert.match(globalStyles, /\.sidebar-scrim\s*\{[^}]*position:\s*fixed[^}]*background:\s*var\(--color-scrim\)/);
assert.match(globalStyles, /\[data-sidebar="sidebar"\]:focus\s*\{[^}]*outline:\s*none/);
assert.match(globalStyles, /\[data-sidebar="rail"\]\s*\{[^}]*display:\s*none/);
});
test("styles the portaled account popup and collapsed tooltips", () => {
assert.match(globalStyles, /:has\(>\s*\.account-menu-popup\)[^{]*\{[^}]*z-index:\s*30/);
assert.match(globalStyles, /\.account-menu-popup\s*\{[^}]*width:\s*min\(280px,\s*calc\(100vw\s*-\s*var\(--space-6\)\)\)[^}]*transform-origin:\s*var\(--transform-origin\)/);
assert.match(globalStyles, /\.account-menu-popup\[data-starting-style\][^{]*\.account-menu-popup\[data-ending-style\][^{]*\{[^}]*opacity:\s*0[^}]*translateY\(var\(--space-1\)\)/);
assert.match(globalStyles, /\[role="tooltip"\]\s*\{[^}]*pointer-events:\s*none[^}]*font-size:\s*var\(--type-caption\)/);
assert.match(globalStyles, /\[role="tooltip"\]\[data-starting-style\][^{]*\{[^}]*opacity:\s*0[^}]*translateX\(-?var\(--space-1\)\)/);
});
test("extends sidebar accessibility preference styles", () => {
assert.match(globalStyles, /@media\s*\(prefers-reduced-motion:\s*reduce\)[\s\S]*\.account-menu-popup\[data-starting-style\][^{]*\{[^}]*transform:\s*none/);
assert.match(globalStyles, /@media\s*\(prefers-reduced-transparency:\s*reduce\)[\s\S]*\.sidebar\s*\{[^}]*background:\s*var\(--sidebar-solid\)[^}]*backdrop-filter:\s*none/);
assert.match(globalStyles, /@media\s*\(prefers-contrast:\s*more\)[\s\S]*\[data-active="true"\]::before\s*\{[^}]*width:\s*var\(--space-1\)/);
});
test("removes class-owned drawer state and obsolete sidebar anchoring", () => {
assert.doesNotMatch(globalStyles, /\.(?:sidebar-backdrop|sidebar-close|mobile-menu|sidebar-open)\b/);
assert.doesNotMatch(globalStyles, /\.account-menu\s*\{/);
assert.doesNotMatch(globalStyles, /\.session-list\s*\{[^}]*overflow(?:-y)?:\s*auto/);
});