authorgravatar for maria@kuuro.netmaria-rcks <maria@kuuro.net> 2026-07-22 19:08:00-04:00
committergravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-07-23 01:13:29-04:00
logc5ff51ec1fff1a9f0447b0336d645ab0f089db62
treef0659ec536f281a1ea4fbd1e4323785aafc81f67
parent2d31cb022dee43e5a729273a6936228f30077e29

feat(web): refresh application surfaces


40 files changed, 605 insertions(+), 441 deletions(-)

apps/web/src/components/AppSidebarLayout.tsx+2-1
...@@ -106,6 +106,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {...@@ -106,6 +106,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
106 const pathname = useLocation({ select: (location) => location.pathname });106 const pathname = useLocation({ select: (location) => location.pathname });
107 const isOnSettings = pathname === "/settings" || pathname.startsWith("/settings/");107 const isOnSettings = pathname === "/settings" || pathname.startsWith("/settings/");
108 const useSidebarV2 = sidebarV2Enabled && !isOnSettings;108 const useSidebarV2 = sidebarV2Enabled && !isOnSettings;
109 const useSidebarV2Theme = useSidebarV2 || isOnSettings;
109 const isMacosDesktop = isElectron && isMacPlatform(navigator.platform);110 const isMacosDesktop = isElectron && isMacPlatform(navigator.platform);
110 const [sidebarWidth, setSidebarWidth] = useState(readInitialThreadSidebarWidth);111 const [sidebarWidth, setSidebarWidth] = useState(readInitialThreadSidebarWidth);
111 const sidebarMaximumWidth = resolveThreadSidebarMaximumWidth(window.innerWidth);112 const sidebarMaximumWidth = resolveThreadSidebarMaximumWidth(window.innerWidth);
...@@ -165,7 +166,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {...@@ -165,7 +166,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
165 side="left"166 side="left"
166 collapsible="offcanvas"167 collapsible="offcanvas"
167 data-app-sidebar=""168 data-app-sidebar=""
168 data-sidebar-version={useSidebarV2 ? "v2" : "v1"}169 data-sidebar-version={useSidebarV2Theme ? "v2" : "v1"}
169 className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"170 className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
170 resizable={{171 resizable={{
171 maxWidth: sidebarMaximumWidth,172 maxWidth: sidebarMaximumWidth,
apps/web/src/components/BranchToolbar.tsx+1-1
...@@ -248,7 +248,7 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -248,7 +248,7 @@ export const BranchToolbar = memo(function BranchToolbar({
248 if (!hasActiveThread || !activeProject) return null;248 if (!hasActiveThread || !activeProject) return null;
249249
250 return (250 return (
251 <div className="mx-auto flex w-full max-w-3xl items-center gap-2 px-2.5 pb-3 pt-1 sm:px-3">251 <div className="chat-composer-context-strip -mt-2 mx-auto flex w-[calc(100%-1.5rem)] max-w-[calc(48rem-1.5rem)] items-center gap-2 pt-3 pe-2 pb-1 ps-2">
252 {isMobile ? (252 {isMobile ? (
253 <MobileRunContextSelector253 <MobileRunContextSelector
254 envLocked={envLocked}254 envLocked={envLocked}
apps/web/src/components/ChatView.tsx+13-21
...@@ -2320,6 +2320,11 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2320,6 +2320,11 @@ function ChatViewContent(props: ChatViewProps) {
2320 const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider);2320 const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider);
2321 return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null;2321 return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null;
2322 }, [activeProviderInstanceId, providerStatuses, selectedProvider]);2322 }, [activeProviderInstanceId, providerStatuses, selectedProvider]);
2323 const hasTimelineTopBanner =
2324 Boolean(threadError) ||
2325 (activeProviderStatus !== null &&
2326 activeProviderStatus.status !== "ready" &&
2327 activeProviderStatus.status !== "disabled");
2323 const activeProjectCwd = activeProject?.workspaceRoot ?? null;2328 const activeProjectCwd = activeProject?.workspaceRoot ?? null;
2324 const activeThreadWorktreePath = activeThread?.worktreePath ?? null;2329 const activeThreadWorktreePath = activeThread?.worktreePath ?? null;
2325 const activeWorkspaceRoot = activeThreadWorktreePath ?? activeProjectCwd ?? undefined;2330 const activeWorkspaceRoot = activeThreadWorktreePath ?? activeProjectCwd ?? undefined;
...@@ -5412,7 +5417,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5412,7 +5417,7 @@ function ChatViewContent(props: ChatViewProps) {
5412 <header5417 <header
5413 data-chat-header5418 data-chat-header
5414 className={cn(5419 className={cn(
5415 "border-b border-border transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",5420 "bg-background transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",
5416 isElectron5421 isElectron
5417 ? cn(5422 ? cn(
5418 "workspace-topbar drag-region relative px-3 sm:px-5",5423 "workspace-topbar drag-region relative px-3 sm:px-5",
...@@ -5448,8 +5453,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5448,8 +5453,6 @@ function ChatViewContent(props: ChatViewProps) {
5448 />5453 />
5449 </header>5454 </header>
54505455
5451 {/* Error banner */}
5452 <ProviderStatusBanner status={activeProviderStatus} />
5453 <ThreadErrorBanner5456 <ThreadErrorBanner
5454 error={threadError}5457 error={threadError}
5455 onDismiss={() => setThreadError(activeThread.id, null)}5458 onDismiss={() => setThreadError(activeThread.id, null)}
...@@ -5458,6 +5461,10 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5458,6 +5461,10 @@ function ChatViewContent(props: ChatViewProps) {
5458 <div className="flex min-h-0 min-w-0 flex-1">5461 <div className="flex min-h-0 min-w-0 flex-1">
5459 {/* Chat column */}5462 {/* Chat column */}
5460 <div className="relative flex min-h-0 min-w-0 flex-1 flex-col">5463 <div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
5464 {/* Provider status overlays the timeline without changing its content height. */}
5465 <div className="pointer-events-none absolute inset-x-0 top-0 z-20">
5466 <ProviderStatusBanner status={activeProviderStatus} />
5467 </div>
5461 {/* Messages Wrapper */}5468 {/* Messages Wrapper */}
5462 <div className="relative flex min-h-0 flex-1 flex-col">5469 <div className="relative flex min-h-0 flex-1 flex-col">
5463 {/* Messages — LegendList handles virtualization and scrolling internally */}5470 {/* Messages — LegendList handles virtualization and scrolling internally */}
...@@ -5494,6 +5501,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5494,6 +5501,7 @@ function ChatViewContent(props: ChatViewProps) {
5494 onIsAtEndChange={onIsAtEndChange}5501 onIsAtEndChange={onIsAtEndChange}
5495 onManualNavigation={cancelTimelineLiveFollowForUserNavigation}5502 onManualNavigation={cancelTimelineLiveFollowForUserNavigation}
5496 hideEmptyPlaceholder={isDraftHeroState}5503 hideEmptyPlaceholder={isDraftHeroState}
5504 topFadeEnabled={!hasTimelineTopBanner}
5497 />5505 />
54985506
5499 {/* scroll to end pill — shown when user has scrolled away from the live edge */}5507 {/* scroll to end pill — shown when user has scrolled away from the live edge */}
...@@ -5526,17 +5534,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5526,17 +5534,6 @@ function ChatViewContent(props: ChatViewProps) {
5526 : "pointer-events-none absolute inset-x-0 bottom-0 z-20 pt-1.5 sm:pt-2"5534 : "pointer-events-none absolute inset-x-0 bottom-0 z-20 pt-1.5 sm:pt-2"
5527 }5535 }
5528 >5536 >
5529 {!isDraftHeroState ? (
5530 <div
5531 key="docked-composer-blur"
5532 aria-hidden="true"
5533 className="chat-composer-horizontal-inset pointer-events-none absolute inset-x-0 top-1.5 bottom-0 z-0 sm:top-2"
5534 >
5535 <div className="relative mx-auto h-full w-full max-w-3xl overflow-clip rounded-t-[20px]">
5536 <div className="chat-composer-shared-blur absolute -inset-8" />
5537 </div>
5538 </div>
5539 ) : null}
5540 <div5537 <div
5541 ref={attachDraftHeroTransitionGroupRef}5538 ref={attachDraftHeroTransitionGroupRef}
5542 className="chat-composer-horizontal-inset w-full"5539 className="chat-composer-horizontal-inset w-full"
...@@ -5574,7 +5571,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5574,7 +5571,7 @@ function ChatViewContent(props: ChatViewProps) {
5574 >5571 >
5575 <div5572 <div
5576 ref={attachDraftHeroComposerAnchorRef}5573 ref={attachDraftHeroComposerAnchorRef}
5577 className="relative z-10 mx-auto w-full max-w-3xl"5574 className="relative z-10 isolate mx-auto w-full max-w-3xl before:pointer-events-none before:absolute before:inset-x-0 before:bottom-0 before:-z-10 before:h-2 before:rounded-b-[22px] before:bg-background"
5578 >5575 >
5579 <ChatComposer5576 <ChatComposer
5580 composerRef={composerRef}5577 composerRef={composerRef}
...@@ -5658,12 +5655,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5658,12 +5655,7 @@ function ChatViewContent(props: ChatViewProps) {
5658 >5655 >
5659 <div5656 <div
5660 data-terminal-open={terminalUiState.terminalOpen ? "true" : undefined}5657 data-terminal-open={terminalUiState.terminalOpen ? "true" : undefined}
5661 className={cn(5658 className="chat-composer-lower-chrome relative z-0 pb-[calc(env(safe-area-inset-bottom)+1rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1.25rem)]"
5662 "chat-composer-lower-chrome relative z-10",
5663 isGitRepo
5664 ? "pb-[calc(env(safe-area-inset-bottom)+0.25rem)]"
5665 : "pb-[calc(env(safe-area-inset-bottom)+0.75rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1rem)]",
5666 )}
5667 >5659 >
5668 {isGitRepo && (5660 {isGitRepo && (
5669 <div className="pointer-events-auto">5661 <div className="pointer-events-auto">
apps/web/src/components/CommandPalette.tsx+1-1
...@@ -1948,7 +1948,7 @@ function OpenCommandPaletteDialog(props: {...@@ -1948,7 +1948,7 @@ function OpenCommandPaletteDialog(props: {
1948 <Button1948 <Button
1949 variant="ghost"1949 variant="ghost"
1950 size="xs"1950 size="xs"
1951 className="h-auto px-2 text-zinc-700 text-xs hover:bg-transparent hover:text-foreground dark:text-zinc-300"1951 className="h-auto px-2 text-muted-foreground text-xs hover:bg-transparent hover:text-foreground"
1952 disabled={isPickingProjectFolder}1952 disabled={isPickingProjectFolder}
1953 onClick={() => {1953 onClick={() => {
1954 void handleOpenProjectFromFileManager();1954 void handleOpenProjectFromFileManager();
apps/web/src/components/DiffPanel.tsx+24-12
...@@ -521,27 +521,37 @@ export default function DiffPanel({...@@ -521,27 +521,37 @@ export default function DiffPanel({
521 <ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />521 <ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />
522 </DropdownMenuTrigger>522 </DropdownMenuTrigger>
523 <DropdownMenuContent align="start" className="w-60">523 <DropdownMenuContent align="start" className="w-60">
524 <DropdownMenuItem onClick={() => selectGitScope("unstaged")}>524 <DropdownMenuItem
525 className={
526 selectedTurnId === null && selectedGitScope === "unstaged"
527 ? "bg-foreground/[0.08]"
528 : undefined
529 }
530 onClick={() => selectGitScope("unstaged")}
531 >
525 <span>Working tree</span>532 <span>Working tree</span>
526 {selectedTurnId === null && selectedGitScope === "unstaged" && (
527 <CheckIcon className="ml-auto" />
528 )}
529 </DropdownMenuItem>533 </DropdownMenuItem>
530 <DropdownMenuItem onClick={() => selectGitScope("branch")}>534 <DropdownMenuItem
535 className={
536 selectedTurnId === null && selectedGitScope === "branch"
537 ? "bg-foreground/[0.08]"
538 : undefined
539 }
540 onClick={() => selectGitScope("branch")}
541 >
531 <span>Branch changes</span>542 <span>Branch changes</span>
532 {selectedTurnId === null && selectedGitScope === "branch" && (
533 <CheckIcon className="ml-auto" />
534 )}
535 </DropdownMenuItem>543 </DropdownMenuItem>
536 <DropdownMenuItem544 <DropdownMenuItem
545 className={
546 selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId
547 ? "bg-foreground/[0.08]"
548 : undefined
549 }
537 onClick={() => {550 onClick={() => {
538 if (latestTurn) selectTurn(latestTurn.turnId);551 if (latestTurn) selectTurn(latestTurn.turnId);
539 }}552 }}
540 >553 >
541 <span>Latest turn</span>554 <span>Latest turn</span>
542 {selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId && (
543 <CheckIcon className="ml-auto" />
544 )}
545 </DropdownMenuItem>555 </DropdownMenuItem>
546 <DropdownMenuSub>556 <DropdownMenuSub>
547 <DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger>557 <DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger>
...@@ -554,13 +564,15 @@ export default function DiffPanel({...@@ -554,13 +564,15 @@ export default function DiffPanel({
554 return (564 return (
555 <DropdownMenuItem565 <DropdownMenuItem
556 key={summary.turnId}566 key={summary.turnId}
567 className={
568 summary.turnId === selectedTurn?.turnId ? "bg-foreground/[0.08]" : undefined
569 }
557 onClick={() => selectTurn(summary.turnId)}570 onClick={() => selectTurn(summary.turnId)}
558 >571 >
559 <span>Turn {turnCount}</span>572 <span>Turn {turnCount}</span>
560 <span className="ml-auto text-xs tabular-nums text-muted-foreground">573 <span className="ml-auto text-xs tabular-nums text-muted-foreground">
561 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}574 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
562 </span>575 </span>
563 {summary.turnId === selectedTurn?.turnId && <CheckIcon className="ml-1" />}
564 </DropdownMenuItem>576 </DropdownMenuItem>
565 );577 );
566 })}578 })}
apps/web/src/components/GitActionsControl.tsx+9-9
...@@ -544,9 +544,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {...@@ -544,9 +544,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
544544
545 return (545 return (
546 <Dialog open={props.open} onOpenChange={handleOpenChange}>546 <Dialog open={props.open} onOpenChange={handleOpenChange}>
547 <DialogPopup className="max-w-xl overflow-hidden">547 <DialogPopup className="workflow-dialog-surface max-w-xl overflow-hidden before:hidden">
548 <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl">548 <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-transparent">
549 <DialogHeader className="border-b border-border/70 bg-background">549 <DialogHeader className="border-b border-border/70 bg-foreground/[0.025]">
550 <DialogTitle>Publish repository</DialogTitle>550 <DialogTitle>Publish repository</DialogTitle>
551 <DialogDescription>551 <DialogDescription>
552 Pick where to host it, then point us at a repo to push to.552 Pick where to host it, then point us at a repo to push to.
...@@ -913,7 +913,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {...@@ -913,7 +913,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
913 </AnimatedHeight>913 </AnimatedHeight>
914 </DialogPanel>914 </DialogPanel>
915915
916 <DialogFooter>916 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025]">
917 {publishWizardStep === 2 ? (917 {publishWizardStep === 2 ? (
918 <Button size="sm" onClick={() => handleOpenChange(false)}>918 <Button size="sm" onClick={() => handleOpenChange(false)}>
919 Done919 Done
...@@ -1736,7 +1736,7 @@ export default function GitActionsControl({...@@ -1736,7 +1736,7 @@ export default function GitActionsControl({
1736 >1736 >
1737 <ChevronDownIcon aria-hidden="true" className="size-4" />1737 <ChevronDownIcon aria-hidden="true" className="size-4" />
1738 </MenuTrigger>1738 </MenuTrigger>
1739 <MenuPopup align="end" className="w-full">1739 <MenuPopup align="end" className="command-menu-surface w-full">
1740 {gitActionMenuItems.map((item) => {1740 {gitActionMenuItems.map((item) => {
1741 const disabledReason = getMenuActionDisabledReason({1741 const disabledReason = getMenuActionDisabledReason({
1742 item,1742 item,
...@@ -1825,7 +1825,7 @@ export default function GitActionsControl({...@@ -1825,7 +1825,7 @@ export default function GitActionsControl({
1825 }1825 }
1826 }}1826 }}
1827 >1827 >
1828 <DialogPopup>1828 <DialogPopup className="workflow-dialog-surface before:hidden">
1829 <DialogHeader>1829 <DialogHeader>
1830 <DialogTitle>{COMMIT_DIALOG_TITLE}</DialogTitle>1830 <DialogTitle>{COMMIT_DIALOG_TITLE}</DialogTitle>
1831 <DialogDescription>{COMMIT_DIALOG_DESCRIPTION}</DialogDescription>1831 <DialogDescription>{COMMIT_DIALOG_DESCRIPTION}</DialogDescription>
...@@ -1955,7 +1955,7 @@ export default function GitActionsControl({...@@ -1955,7 +1955,7 @@ export default function GitActionsControl({
1955 />1955 />
1956 </div>1956 </div>
1957 </DialogPanel>1957 </DialogPanel>
1958 <DialogFooter>1958 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025]">
1959 <Button1959 <Button
1960 variant="outline"1960 variant="outline"
1961 size="sm"1961 size="sm"
...@@ -1998,14 +1998,14 @@ export default function GitActionsControl({...@@ -1998,14 +1998,14 @@ export default function GitActionsControl({
1998 }1998 }
1999 }}1999 }}
2000 >2000 >
2001 <DialogPopup className="max-w-xl">2001 <DialogPopup className="workflow-dialog-surface max-w-xl before:hidden">
2002 <DialogHeader>2002 <DialogHeader>
2003 <DialogTitle>2003 <DialogTitle>
2004 {pendingDefaultBranchActionCopy?.title ?? "Run action on default refName?"}2004 {pendingDefaultBranchActionCopy?.title ?? "Run action on default refName?"}
2005 </DialogTitle>2005 </DialogTitle>
2006 <DialogDescription>{pendingDefaultBranchActionCopy?.description}</DialogDescription>2006 <DialogDescription>{pendingDefaultBranchActionCopy?.description}</DialogDescription>
2007 </DialogHeader>2007 </DialogHeader>
2008 <DialogFooter className="sm:flex-wrap sm:items-center">2008 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025] sm:flex-wrap sm:items-center">
2009 <Button2009 <Button
2010 className="w-full sm:mr-auto sm:w-auto"2010 className="w-full sm:mr-auto sm:w-auto"
2011 variant="outline"2011 variant="outline"
apps/web/src/components/ProjectScriptsControl.tsx+4-4
...@@ -276,7 +276,7 @@ export default function ProjectScriptsControl({...@@ -276,7 +276,7 @@ export default function ProjectScriptsControl({
276 >276 >
277 <ChevronDownIcon className="size-4" />277 <ChevronDownIcon className="size-4" />
278 </MenuTrigger>278 </MenuTrigger>
279 <MenuPopup align="end">279 <MenuPopup align="end" className="command-menu-surface">
280 {scripts.map((script) => {280 {scripts.map((script) => {
281 const shortcutLabel = shortcutLabelForCommand(281 const shortcutLabel = shortcutLabelForCommand(
282 keybindings,282 keybindings,
...@@ -364,7 +364,7 @@ export default function ProjectScriptsControl({...@@ -364,7 +364,7 @@ export default function ProjectScriptsControl({
364 }}364 }}
365 open={dialogOpen}365 open={dialogOpen}
366 >366 >
367 <DialogPopup>367 <DialogPopup className="workflow-dialog-surface before:hidden">
368 <DialogHeader>368 <DialogHeader>
369 <DialogTitle>{isEditing ? "Edit Action" : "Add Action"}</DialogTitle>369 <DialogTitle>{isEditing ? "Edit Action" : "Add Action"}</DialogTitle>
370 <DialogDescription>370 <DialogDescription>
...@@ -389,7 +389,7 @@ export default function ProjectScriptsControl({...@@ -389,7 +389,7 @@ export default function ProjectScriptsControl({
389 >389 >
390 <ScriptIcon icon={icon} className="size-4.5" />390 <ScriptIcon icon={icon} className="size-4.5" />
391 </PopoverTrigger>391 </PopoverTrigger>
392 <PopoverPopup align="start">392 <PopoverPopup align="start" className="command-menu-surface">
393 <div className="grid grid-cols-3 gap-2">393 <div className="grid grid-cols-3 gap-2">
394 {SCRIPT_ICONS.map((entry) => {394 {SCRIPT_ICONS.map((entry) => {
395 const isSelected = entry.id === icon;395 const isSelected = entry.id === icon;
...@@ -480,7 +480,7 @@ export default function ProjectScriptsControl({...@@ -480,7 +480,7 @@ export default function ProjectScriptsControl({
480 {validationError && <p className="text-sm text-destructive">{validationError}</p>}480 {validationError && <p className="text-sm text-destructive">{validationError}</p>}
481 </form>481 </form>
482 </DialogPanel>482 </DialogPanel>
483 <DialogFooter>483 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025]">
484 {isEditing && (484 {isEditing && (
485 <Button485 <Button
486 type="button"486 type="button"
apps/web/src/components/Sidebar.logic.test.ts+10-12
...@@ -834,26 +834,24 @@ describe("resolveThreadStatusPill", () => {...@@ -834,26 +834,24 @@ describe("resolveThreadStatusPill", () => {
834});834});
835835
836describe("resolveThreadRowClassName", () => {836describe("resolveThreadRowClassName", () => {
837 it("uses the darker selected palette when a thread is both selected and active", () => {837 it("uses the active sidebar surface when a thread is both selected and active", () => {
838 const className = resolveThreadRowClassName({ isActive: true, isSelected: true });838 const className = resolveThreadRowClassName({ isActive: true, isSelected: true });
839 expect(className).toContain("bg-primary/22");839 expect(className).toContain("bg-sidebar-row-active");
840 expect(className).toContain("hover:bg-primary/26");840 expect(className).toContain("text-sidebar-foreground");
841 expect(className).toContain("dark:bg-primary/30");841 expect(className).not.toContain("bg-primary");
842 expect(className).not.toContain("bg-accent/85");
843 });842 });
844843
845 it("uses selected hover colors for selected threads", () => {844 it("uses selected hover colors for selected threads", () => {
846 const className = resolveThreadRowClassName({ isActive: false, isSelected: true });845 const className = resolveThreadRowClassName({ isActive: false, isSelected: true });
847 expect(className).toContain("bg-primary/15");846 expect(className).toContain("bg-sidebar-row-selected");
848 expect(className).toContain("hover:bg-primary/19");847 expect(className).toContain("hover:bg-sidebar-row-active");
849 expect(className).toContain("dark:bg-primary/22");848 expect(className).not.toContain("bg-primary");
850 expect(className).not.toContain("hover:bg-accent");
851 });849 });
852850
853 it("keeps the accent palette for active-only threads", () => {851 it("uses the active sidebar surface for active-only threads", () => {
854 const className = resolveThreadRowClassName({ isActive: true, isSelected: false });852 const className = resolveThreadRowClassName({ isActive: true, isSelected: false });
855 expect(className).toContain("bg-accent/85");853 expect(className).toContain("bg-sidebar-row-active");
856 expect(className).toContain("hover:bg-accent");854 expect(className).toContain("hover:bg-sidebar-row-active");
857 });855 });
858});856});
859857
apps/web/src/components/Sidebar.logic.ts+8-5
...@@ -391,30 +391,33 @@ export function resolveThreadRowClassName(input: {...@@ -391,30 +391,33 @@ export function resolveThreadRowClassName(input: {
391 isSelected: boolean;391 isSelected: boolean;
392}): string {392}): string {
393 const baseClassName =393 const baseClassName =
394 "h-6 w-full translate-x-0 cursor-pointer justify-start px-2 text-left select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring sm:h-7";394 "h-8 w-full translate-x-0 cursor-pointer justify-start rounded-md px-2 text-left text-sm select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring";
395395
396 if (input.isSelected && input.isActive) {396 if (input.isSelected && input.isActive) {
397 return cn(397 return cn(
398 baseClassName,398 baseClassName,
399 "bg-primary/22 text-foreground font-medium hover:bg-primary/26 hover:text-foreground dark:bg-primary/30 dark:hover:bg-primary/36",399 "bg-sidebar-row-active text-sidebar-foreground font-medium hover:bg-sidebar-row-active hover:text-sidebar-foreground",
400 );400 );
401 }401 }
402402
403 if (input.isSelected) {403 if (input.isSelected) {
404 return cn(404 return cn(
405 baseClassName,405 baseClassName,
406 "bg-primary/15 text-foreground hover:bg-primary/19 hover:text-foreground dark:bg-primary/22 dark:hover:bg-primary/28",406 "bg-sidebar-row-selected text-sidebar-foreground hover:bg-sidebar-row-active hover:text-sidebar-foreground",
407 );407 );
408 }408 }
409409
410 if (input.isActive) {410 if (input.isActive) {
411 return cn(411 return cn(
412 baseClassName,412 baseClassName,
413 "bg-accent/85 text-foreground font-medium hover:bg-accent hover:text-foreground dark:bg-accent/55 dark:hover:bg-accent/70",413 "bg-sidebar-row-active text-sidebar-foreground font-medium hover:bg-sidebar-row-active hover:text-sidebar-foreground",
414 );414 );
415 }415 }
416416
417 return cn(baseClassName, "text-muted-foreground hover:bg-accent hover:text-foreground");417 return cn(
418 baseClassName,
419 "text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
420 );
418}421}
419422
420// ── Sidebar v2 status model ─────────────────────────────────────────423// ── Sidebar v2 status model ─────────────────────────────────────────
apps/web/src/components/Sidebar.tsx+12-14
...@@ -714,7 +714,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr...@@ -714,7 +714,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
714 {renamingThreadKey === threadKey ? (714 {renamingThreadKey === threadKey ? (
715 <input715 <input
716 ref={handleRenameInputRef}716 ref={handleRenameInputRef}
717 className="min-w-0 flex-1 truncate text-base sm:text-xs bg-transparent outline-none border border-ring rounded px-0.5"717 className="min-w-0 flex-1 truncate rounded border border-ring bg-transparent px-0.5 text-sm outline-none"
718 value={renamingTitle}718 value={renamingTitle}
719 onChange={handleRenameInputChange}719 onChange={handleRenameInputChange}
720 onKeyDown={handleRenameInputKeyDown}720 onKeyDown={handleRenameInputKeyDown}
...@@ -727,7 +727,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr...@@ -727,7 +727,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
727 <TooltipTrigger727 <TooltipTrigger
728 render={728 render={
729 <span729 <span
730 className="min-w-0 flex-1 truncate text-xs"730 className="min-w-0 flex-1 truncate text-sm"
731 data-testid={`thread-title-${thread.id}`}731 data-testid={`thread-title-${thread.id}`}
732 >732 >
733 {thread.title}733 {thread.title}
...@@ -982,13 +982,13 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(...@@ -982,13 +982,13 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
982 return (982 return (
983 <SidebarMenuSub983 <SidebarMenuSub
984 ref={attachThreadListAutoAnimateRef}984 ref={attachThreadListAutoAnimateRef}
985 className="mx-0.5 my-0 w-full translate-x-0 gap-0.5 overflow-hidden px-1 py-0 sm:mx-1 sm:px-1.5"985 className="mx-0.5 my-0 w-full translate-x-0 gap-0.5 overflow-hidden border-l-0 px-1 py-0 sm:mx-1 sm:px-1.5"
986 >986 >
987 {shouldShowThreadPanel && showEmptyThreadState ? (987 {shouldShowThreadPanel && showEmptyThreadState ? (
988 <SidebarMenuSubItem className="w-full" data-thread-selection-safe>988 <SidebarMenuSubItem className="w-full" data-thread-selection-safe>
989 <div989 <div
990 data-thread-selection-safe990 data-thread-selection-safe
991 className="flex h-6 w-full translate-x-0 items-center px-2 text-left text-[10px] text-muted-foreground/60"991 className="flex h-8 w-full translate-x-0 items-center px-2 text-left text-xs text-sidebar-muted-foreground/75"
992 >992 >
993 <span>No threads yet</span>993 <span>No threads yet</span>
994 </div>994 </div>
...@@ -1034,7 +1034,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(...@@ -1034,7 +1034,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
1034 render={showMoreButtonRender}1034 render={showMoreButtonRender}
1035 data-thread-selection-safe1035 data-thread-selection-safe
1036 size="sm"1036 size="sm"
1037 className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"1037 className="h-8 w-full translate-x-0 justify-start px-2 text-left text-xs text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
1038 onClick={() => {1038 onClick={() => {
1039 expandThreadListForProject(projectKey);1039 expandThreadListForProject(projectKey);
1040 }}1040 }}
...@@ -1052,7 +1052,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(...@@ -1052,7 +1052,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
1052 render={showLessButtonRender}1052 render={showLessButtonRender}
1053 data-thread-selection-safe1053 data-thread-selection-safe
1054 size="sm"1054 size="sm"
1055 className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"1055 className="h-8 w-full translate-x-0 justify-start px-2 text-left text-xs text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
1056 onClick={() => {1056 onClick={() => {
1057 collapseThreadListForProject(projectKey);1057 collapseThreadListForProject(projectKey);
1058 }}1058 }}
...@@ -2252,7 +2252,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2252,7 +2252,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2252 <SidebarMenuButton2252 <SidebarMenuButton
2253 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}2253 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
2254 size="sm"2254 size="sm"
2255 className={`gap-2 px-2 py-1.5 pr-8 text-left hover:bg-sidebar-row-hover group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${2255 className={`h-8 gap-2 rounded-md px-2 py-1.5 pr-8 text-left hover:bg-sidebar-row-hover group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${
2256 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"2256 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"
2257 }`}2257 }`}
2258 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}2258 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
...@@ -2292,7 +2292,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2292,7 +2292,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2292 )}2292 )}
2293 <ProjectFavicon environmentId={project.environmentId} cwd={project.workspaceRoot} />2293 <ProjectFavicon environmentId={project.environmentId} cwd={project.workspaceRoot} />
2294 <span className="flex min-w-0 flex-1 items-center gap-2">2294 <span className="flex min-w-0 flex-1 items-center gap-2">
2295 <span className="truncate text-xs font-medium text-foreground/90">2295 <span className="truncate text-sm font-medium text-sidebar-foreground/90">
2296 {project.displayName}2296 {project.displayName}
2297 </span>2297 </span>
2298 {project.groupedProjectCount > 1 ? (2298 {project.groupedProjectCount > 1 ? (
...@@ -2899,13 +2899,13 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2899,13 +2899,13 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2899 render={2899 render={
2900 <SidebarMenuButton2900 <SidebarMenuButton
2901 size="sm"2901 size="sm"
2902 className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground focus-visible:ring-0"2902 className="h-8 gap-2 rounded-md px-2 py-1.5 text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-0"
2903 data-testid="command-palette-trigger"2903 data-testid="command-palette-trigger"
2904 />2904 />
2905 }2905 }
2906 >2906 >
2907 <SearchIcon className="size-3.5 text-muted-foreground/70" />2907 <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2908 <span className="flex-1 truncate text-left text-xs">Search</span>2908 <span className="flex-1 truncate text-left text-sm font-medium">Search</span>
2909 {commandPaletteShortcutLabel ? (2909 {commandPaletteShortcutLabel ? (
2910 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">2910 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">
2911 {commandPaletteShortcutLabel}2911 {commandPaletteShortcutLabel}
...@@ -2941,9 +2941,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2941,9 +2941,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2941 <LocalSecondaryStatus />2941 <LocalSecondaryStatus />
2942 <SidebarGroup className="px-2 py-2">2942 <SidebarGroup className="px-2 py-2">
2943 <div className="mb-1 flex items-center justify-between pl-2 pr-1.5">2943 <div className="mb-1 flex items-center justify-between pl-2 pr-1.5">
2944 <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">2944 <span className="text-xs font-medium text-sidebar-muted-foreground/80">Projects</span>
2945 Projects
2946 </span>
2947 <div className="flex items-center gap-1">2945 <div className="flex items-center gap-1">
2948 <ProjectSortMenu2946 <ProjectSortMenu
2949 projectSortOrder={projectSortOrder}2947 projectSortOrder={projectSortOrder}
apps/web/src/components/SidebarStageBackdrop.test.tsx created+22
...@@ -0,0 +1,22 @@
1import { describe, expect, it } from "vite-plus/test";
2import { renderToStaticMarkup } from "react-dom/server";
3
4import { StageBackdropArt, StageBackdropButtonArt } from "./SidebarStageBackdrop";
5
6describe("SidebarStageBackdrop", () => {
7 it.each(["nightly", "dev"] as const)(
8 "uses unique SVG definition ids when %s artwork is rendered more than once",
9 (variant) => {
10 const markup = renderToStaticMarkup(
11 <>
12 <StageBackdropArt variant={variant} />
13 <StageBackdropButtonArt variant={variant} />
14 </>,
15 );
16 const ids = Array.from(markup.matchAll(/\sid="([^"]+)"/g), (match) => match[1]);
17
18 expect(ids.length).toBeGreaterThan(0);
19 expect(new Set(ids).size).toBe(ids.length);
20 },
21 );
22});
apps/web/src/components/SidebarStageBackdrop.tsx+60-50
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { useAtomValue } from "@effect/atom-react";1import { useAtomValue } from "@effect/atom-react";
2import { useId } from "react";
23
3import { APP_STAGE_LABEL } from "../branding";4import { APP_STAGE_LABEL } from "../branding";
4import { resolveServerBackedAppStageLabel } from "../branding.logic";5import { resolveServerBackedAppStageLabel } from "../branding.logic";
...@@ -47,6 +48,10 @@ export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVar...@@ -47,6 +48,10 @@ export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVar
47 return variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />;48 return variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />;
48}49}
4950
51export function StageBackdropButtonArt({ variant }: { variant: SidebarStageBackdropVariant }) {
52 return variant === "nightly" ? <NightlySkyArt compact /> : <DevBlueprintArt compact />;
53}
54
50const NIGHTLY_STARS: ReadonlyArray<{55const NIGHTLY_STARS: ReadonlyArray<{
51 cx: number;56 cx: number;
52 cy: number;57 cy: number;
...@@ -78,18 +83,26 @@ const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [...@@ -78,18 +83,26 @@ const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [
78 { x: 246, y: 26 },83 { x: 246, y: 26 },
79];84];
8085
81function NightlySkyArt() {86function NightlySkyArt({ compact = false }: { compact?: boolean }) {
87 const idPrefix = useId().replaceAll(":", "");
88 const skyId = `${idPrefix}-stage-night-sky`;
89 const glowId = `${idPrefix}-stage-night-glow`;
90 const cloudId = `${idPrefix}-stage-night-cloud`;
91 const softId = `${idPrefix}-stage-night-soft`;
92 const starsId = `${idPrefix}-stage-night-stars`;
93 const glowsId = `${idPrefix}-stage-night-glows`;
94
82 return (95 return (
83 <svg96 <svg
84 className="h-full w-full"97 className="h-full w-full"
85 fill="none"98 fill="none"
86 preserveAspectRatio="xMinYMin slice"99 preserveAspectRatio="xMinYMin slice"
87 viewBox={STAGE_BACKDROP_VIEW_BOX}100 viewBox={compact ? "96 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
88 xmlns="http://www.w3.org/2000/svg"101 xmlns="http://www.w3.org/2000/svg"
89 >102 >
90 <defs>103 <defs>
91 <linearGradient104 <linearGradient
92 id="stage-night-sky"105 id={skyId}
93 x1="24"106 x1="24"
94 y1="0"107 y1="0"
95 x2="264"108 x2="264"
...@@ -102,7 +115,7 @@ function NightlySkyArt() {...@@ -102,7 +115,7 @@ function NightlySkyArt() {
102 <stop offset="1" stopColor="#32155B" />115 <stop offset="1" stopColor="#32155B" />
103 </linearGradient>116 </linearGradient>
104 <radialGradient117 <radialGradient
105 id="stage-night-glow"118 id={glowId}
106 cx="0"119 cx="0"
107 cy="0"120 cy="0"
108 r="1"121 r="1"
...@@ -113,29 +126,15 @@ function NightlySkyArt() {...@@ -113,29 +126,15 @@ function NightlySkyArt() {
113 <stop offset="0.5" stopColor="#283075" stopOpacity="0.16" />126 <stop offset="0.5" stopColor="#283075" stopOpacity="0.16" />
114 <stop offset="1" stopColor="#111635" stopOpacity="0" />127 <stop offset="1" stopColor="#111635" stopOpacity="0" />
115 </radialGradient>128 </radialGradient>
116 <linearGradient129 <linearGradient id={cloudId} x1="0" y1="60" x2="288" y2="96" gradientUnits="userSpaceOnUse">
117 id="stage-night-cloud"
118 x1="0"
119 y1="60"
120 x2="288"
121 y2="96"
122 gradientUnits="userSpaceOnUse"
123 >
124 <stop stopColor="#4EA4FF" stopOpacity="0.5" />130 <stop stopColor="#4EA4FF" stopOpacity="0.5" />
125 <stop offset="0.52" stopColor="#696FEA" stopOpacity="0.62" />131 <stop offset="0.52" stopColor="#696FEA" stopOpacity="0.62" />
126 <stop offset="1" stopColor="#A85BEA" stopOpacity="0.5" />132 <stop offset="1" stopColor="#A85BEA" stopOpacity="0.5" />
127 </linearGradient>133 </linearGradient>
128 <filter134 <filter id={softId} x="-24" y="-24" width="336" height="144" filterUnits="userSpaceOnUse">
129 id="stage-night-soft"
130 x="-24"
131 y="-24"
132 width="336"
133 height="144"
134 filterUnits="userSpaceOnUse"
135 >
136 <feGaussianBlur stdDeviation="4" />135 <feGaussianBlur stdDeviation="4" />
137 </filter>136 </filter>
138 <pattern id="stage-night-stars" width="288" height="96" patternUnits="userSpaceOnUse">137 <pattern id={starsId} width="288" height="96" patternUnits="userSpaceOnUse">
139 <g fill="#E4EAFF">138 <g fill="#E4EAFF">
140 {NIGHTLY_STARS.map((star) => (139 {NIGHTLY_STARS.map((star) => (
141 <circle140 <circle
...@@ -156,25 +155,25 @@ function NightlySkyArt() {...@@ -156,25 +155,25 @@ function NightlySkyArt() {
156 ))}155 ))}
157 </g>156 </g>
158 </pattern>157 </pattern>
159 <pattern id="stage-night-glows" width="640" height="96" patternUnits="userSpaceOnUse">158 <pattern id={glowsId} width="640" height="96" patternUnits="userSpaceOnUse">
160 <rect width="640" height="96" fill="url(#stage-night-glow)" />159 <rect width="640" height="96" fill={`url(#${glowId})`} />
161 </pattern>160 </pattern>
162 </defs>161 </defs>
163162
164 <rect width="100%" height="96" fill="url(#stage-night-sky)" />163 <rect width="100%" height="96" fill={`url(#${skyId})`} />
165 <rect width="100%" height="96" fill="url(#stage-night-glows)" />164 <rect width="100%" height="96" fill={`url(#${glowsId})`} />
166 <rect width="100%" height="96" fill="url(#stage-night-stars)" />165 <rect width="100%" height="96" fill={`url(#${starsId})`} />
167166
168 <g filter="url(#stage-night-soft)">167 <g filter={`url(#${softId})`}>
169 <path168 <path
170 d="M-12 88C-12 74 0 63 14 63C18 50 30 41 44 41C58 41 70 49 74 62C79 57 86 54 94 54C110 54 123 66 124 82C132 83 138 88 141 96H-12V88Z"169 d="M-12 88C-12 74 0 63 14 63C18 50 30 41 44 41C58 41 70 49 74 62C79 57 86 54 94 54C110 54 123 66 124 82C132 83 138 88 141 96H-12V88Z"
171 fill="url(#stage-night-cloud)"170 fill={`url(#${cloudId})`}
172 />171 />
173 </g>172 </g>
174 <g filter="url(#stage-night-soft)">173 <g filter={`url(#${softId})`}>
175 <path174 <path
176 d="M150 96C151 84 161 75 173 75C176 64 186 57 198 57C210 57 220 64 223 75C231 75 238 80 241 87C250 87 257 91 260 96H150Z"175 d="M150 96C151 84 161 75 173 75C176 64 186 57 198 57C210 57 220 64 223 75C231 75 238 80 241 87C250 87 257 91 260 96H150Z"
177 fill="url(#stage-night-cloud)"176 fill={`url(#${cloudId})`}
178 fillOpacity="0.8"177 fillOpacity="0.8"
179 />178 />
180 </g>179 </g>
...@@ -182,18 +181,29 @@ function NightlySkyArt() {...@@ -182,18 +181,29 @@ function NightlySkyArt() {
182 );181 );
183}182}
184183
185function DevBlueprintArt() {184function DevBlueprintArt({ compact = false }: { compact?: boolean }) {
185 const idPrefix = useId().replaceAll(":", "");
186 const paperId = `${idPrefix}-stage-bp-paper`;
187 const glowId = `${idPrefix}-stage-bp-glow`;
188 const celesteGlowId = `${idPrefix}-stage-bp-glow-celeste`;
189 const violetGlowId = `${idPrefix}-stage-bp-glow-violet`;
190 const minorGridId = `${idPrefix}-stage-bp-grid-minor`;
191 const majorGridId = `${idPrefix}-stage-bp-grid-major`;
192 const rulerId = `${idPrefix}-stage-bp-ruler`;
193 const glowsId = `${idPrefix}-stage-bp-glows`;
194 const annotationsId = `${idPrefix}-stage-bp-annotations`;
195
186 return (196 return (
187 <svg197 <svg
188 className="stage-blueprint h-full w-full"198 className="stage-blueprint h-full w-full"
189 fill="none"199 fill="none"
190 preserveAspectRatio="xMinYMin slice"200 preserveAspectRatio="xMinYMin slice"
191 viewBox={STAGE_BACKDROP_VIEW_BOX}201 viewBox={compact ? "64 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
192 xmlns="http://www.w3.org/2000/svg"202 xmlns="http://www.w3.org/2000/svg"
193 >203 >
194 <defs>204 <defs>
195 <linearGradient205 <linearGradient
196 id="stage-bp-paper"206 id={paperId}
197 x1="60"207 x1="60"
198 y1="0"208 y1="0"
199 x2="220"209 x2="220"
...@@ -206,7 +216,7 @@ function DevBlueprintArt() {...@@ -206,7 +216,7 @@ function DevBlueprintArt() {
206 <stop offset="1" style={{ stopColor: "var(--stage-bp-top)" }} />216 <stop offset="1" style={{ stopColor: "var(--stage-bp-top)" }} />
207 </linearGradient>217 </linearGradient>
208 <radialGradient218 <radialGradient
209 id="stage-bp-glow"219 id={glowId}
210 cx="0"220 cx="0"
211 cy="0"221 cy="0"
212 r="1"222 r="1"
...@@ -218,7 +228,7 @@ function DevBlueprintArt() {...@@ -218,7 +228,7 @@ function DevBlueprintArt() {
218 <stop offset="1" stopColor="#276AF1" stopOpacity="0" />228 <stop offset="1" stopColor="#276AF1" stopOpacity="0" />
219 </radialGradient>229 </radialGradient>
220 <radialGradient230 <radialGradient
221 id="stage-bp-glow-celeste"231 id={celesteGlowId}
222 cx="0"232 cx="0"
223 cy="0"233 cy="0"
224 r="1"234 r="1"
...@@ -230,7 +240,7 @@ function DevBlueprintArt() {...@@ -230,7 +240,7 @@ function DevBlueprintArt() {
230 <stop offset="1" stopColor="#277EF1" stopOpacity="0" />240 <stop offset="1" stopColor="#277EF1" stopOpacity="0" />
231 </radialGradient>241 </radialGradient>
232 <radialGradient242 <radialGradient
233 id="stage-bp-glow-violet"243 id={violetGlowId}
234 cx="0"244 cx="0"
235 cy="0"245 cy="0"
236 r="1"246 r="1"
...@@ -241,13 +251,13 @@ function DevBlueprintArt() {...@@ -241,13 +251,13 @@ function DevBlueprintArt() {
241 <stop offset="0.52" stopColor="#7C8BFF" stopOpacity="0.14" />251 <stop offset="0.52" stopColor="#7C8BFF" stopOpacity="0.14" />
242 <stop offset="1" stopColor="#3155DF" stopOpacity="0" />252 <stop offset="1" stopColor="#3155DF" stopOpacity="0" />
243 </radialGradient>253 </radialGradient>
244 <pattern id="stage-bp-grid-minor" width="8" height="8" patternUnits="userSpaceOnUse">254 <pattern id={minorGridId} width="8" height="8" patternUnits="userSpaceOnUse">
245 <path d="M8 0H0V8" stroke="#EAF6FF" strokeOpacity="0.14" strokeWidth="0.5" />255 <path d="M8 0H0V8" stroke="#EAF6FF" strokeOpacity="0.14" strokeWidth="0.5" />
246 </pattern>256 </pattern>
247 <pattern id="stage-bp-grid-major" width="32" height="32" patternUnits="userSpaceOnUse">257 <pattern id={majorGridId} width="32" height="32" patternUnits="userSpaceOnUse">
248 <path d="M32 0H0V32" stroke="#EAF6FF" strokeOpacity="0.26" strokeWidth="0.6" />258 <path d="M32 0H0V32" stroke="#EAF6FF" strokeOpacity="0.26" strokeWidth="0.6" />
249 </pattern>259 </pattern>
250 <pattern id="stage-bp-ruler" width="32" height="6" patternUnits="userSpaceOnUse">260 <pattern id={rulerId} width="32" height="6" patternUnits="userSpaceOnUse">
251 <path261 <path
252 d="M4 0V2.5M12 0V2.5M20 0V4M28 0V2.5"262 d="M4 0V2.5M12 0V2.5M20 0V4M28 0V2.5"
253 stroke="#DDF7FF"263 stroke="#DDF7FF"
...@@ -255,12 +265,12 @@ function DevBlueprintArt() {...@@ -255,12 +265,12 @@ function DevBlueprintArt() {
255 strokeWidth="0.5"265 strokeWidth="0.5"
256 />266 />
257 </pattern>267 </pattern>
258 <pattern id="stage-bp-glows" width="768" height="96" patternUnits="userSpaceOnUse">268 <pattern id={glowsId} width="768" height="96" patternUnits="userSpaceOnUse">
259 <rect width="768" height="96" fill="url(#stage-bp-glow)" />269 <rect width="768" height="96" fill={`url(#${glowId})`} />
260 <rect width="768" height="96" fill="url(#stage-bp-glow-celeste)" />270 <rect width="768" height="96" fill={`url(#${celesteGlowId})`} />
261 <rect width="768" height="96" fill="url(#stage-bp-glow-violet)" />271 <rect width="768" height="96" fill={`url(#${violetGlowId})`} />
262 </pattern>272 </pattern>
263 <pattern id="stage-bp-annotations" width="768" height="96" patternUnits="userSpaceOnUse">273 <pattern id={annotationsId} width="768" height="96" patternUnits="userSpaceOnUse">
264 <g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.6" strokeWidth="0.7">274 <g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.6" strokeWidth="0.7">
265 <path d="M180 64H264" strokeDasharray="5 4" />275 <path d="M180 64H264" strokeDasharray="5 4" />
266 <path d="M180 61V67M264 61V67" />276 <path d="M180 61V67M264 61V67" />
...@@ -309,12 +319,12 @@ function DevBlueprintArt() {...@@ -309,12 +319,12 @@ function DevBlueprintArt() {
309 </pattern>319 </pattern>
310 </defs>320 </defs>
311321
312 <rect width="100%" height="96" fill="url(#stage-bp-paper)" />322 <rect width="100%" height="96" fill={`url(#${paperId})`} />
313 <rect width="100%" height="96" fill="url(#stage-bp-glows)" />323 <rect width="100%" height="96" fill={`url(#${glowsId})`} />
314 <rect width="100%" height="96" fill="url(#stage-bp-grid-minor)" />324 <rect width="100%" height="96" fill={`url(#${minorGridId})`} />
315 <rect width="100%" height="96" fill="url(#stage-bp-grid-major)" />325 <rect width="100%" height="96" fill={`url(#${majorGridId})`} />
316 <rect width="100%" height="6" fill="url(#stage-bp-ruler)" />326 <rect width="100%" height="6" fill={`url(#${rulerId})`} />
317 <rect width="100%" height="96" fill="url(#stage-bp-annotations)" />327 <rect width="100%" height="96" fill={`url(#${annotationsId})`} />
318 </svg>328 </svg>
319 );329 );
320}330}
apps/web/src/components/SidebarV2.tsx+120-68
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import { autoAnimate } from "@formkit/auto-animate";1import { autoAnimate } from "@formkit/auto-animate";
2import { useAtomValue } from "@effect/atom-react";2import { useAtomValue } from "@effect/atom-react";
3import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled";3import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled";
4import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/models";4import type {
5 EnvironmentProject,
6 EnvironmentThreadShell,
7} from "@t3tools/client-runtime/state/models";
5import {8import {
6 scopeProjectRef,9 scopeProjectRef,
7 scopeThreadRef,10 scopeThreadRef,
...@@ -11,9 +14,9 @@ import type { ScopedThreadRef } from "@t3tools/contracts";...@@ -11,9 +14,9 @@ import type { ScopedThreadRef } from "@t3tools/contracts";
11import {14import {
12 CheckIcon,15 CheckIcon,
13 ChevronDownIcon,16 ChevronDownIcon,
17 CircleAlertIcon,
14 CircleCheckIcon,18 CircleCheckIcon,
15 CircleDashedIcon,19 CircleDashedIcon,
16 CircleAlertIcon,
17 FolderIcon,20 FolderIcon,
18 FolderPlusIcon,21 FolderPlusIcon,
19 GitBranchIcon,22 GitBranchIcon,
...@@ -22,6 +25,7 @@ import {...@@ -22,6 +25,7 @@ import {
22 SearchIcon,25 SearchIcon,
23 ServerIcon,26 ServerIcon,
24 SquarePenIcon,27 SquarePenIcon,
28 Trash2Icon,
25 Undo2Icon,29 Undo2Icon,
26} from "lucide-react";30} from "lucide-react";
27import {31import {
...@@ -69,6 +73,7 @@ import { useProjects, useThreadShells } from "../state/entities";...@@ -69,6 +73,7 @@ import { useProjects, useThreadShells } from "../state/entities";
69import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server";73import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server";
70import { vcsEnvironment } from "../state/vcs";74import { vcsEnvironment } from "../state/vcs";
71import { threadEnvironment } from "../state/threads";75import { threadEnvironment } from "../state/threads";
76import { projectEnvironment } from "../state/projects";
72import { useEnvironmentQuery } from "../state/query";77import { useEnvironmentQuery } from "../state/query";
73import { useAtomCommand } from "../state/use-atom-command";78import { useAtomCommand } from "../state/use-atom-command";
74import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes";79import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes";
...@@ -94,15 +99,10 @@ import { stackedThreadToast, toastManager } from "./ui/toast";...@@ -94,15 +99,10 @@ import { stackedThreadToast, toastManager } from "./ui/toast";
94import { CommandDialogTrigger } from "./ui/command";99import { CommandDialogTrigger } from "./ui/command";
95import { Kbd } from "./ui/kbd";100import { Kbd } from "./ui/kbd";
96import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";101import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
97import {102import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar";
98 SidebarContent,
99 SidebarGroup,
100 SidebarMenuButton,
101 SidebarSeparator,
102 useSidebar,
103} from "./ui/sidebar";
104import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome";103import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome";
105import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";104import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
105import { useComposerDraftStore } from "../composerDraftStore";
106106
107// Settled-tail paging: recent history is the common lookup; the deep tail107// Settled-tail paging: recent history is the common lookup; the deep tail
108// stays behind an explicit Show more.108// stays behind an explicit Show more.
...@@ -127,7 +127,6 @@ function SidebarV2ThreadTooltip({...@@ -127,7 +127,6 @@ function SidebarV2ThreadTooltip({
127 driverKind,127 driverKind,
128 modelInstanceId,128 modelInstanceId,
129 modelLabel,129 modelLabel,
130 status,
131 branchMismatch,130 branchMismatch,
132}: {131}: {
133 thread: SidebarThreadSummary;132 thread: SidebarThreadSummary;
...@@ -137,11 +136,6 @@ function SidebarV2ThreadTooltip({...@@ -137,11 +136,6 @@ function SidebarV2ThreadTooltip({
137 driverKind: ProviderInstanceEntry["driverKind"] | null;136 driverKind: ProviderInstanceEntry["driverKind"] | null;
138 modelInstanceId: string;137 modelInstanceId: string;
139 modelLabel: string;138 modelLabel: string;
140 status: {
141 label: string;
142 className: string;
143 icon: "working" | "done" | null;
144 } | null;
145 branchMismatch: {139 branchMismatch: {
146 threadBranch: string;140 threadBranch: string;
147 currentBranch: string;141 currentBranch: string;
...@@ -152,58 +146,31 @@ function SidebarV2ThreadTooltip({...@@ -152,58 +146,31 @@ function SidebarV2ThreadTooltip({
152 side="right"146 side="right"
153 align="start"147 align="start"
154 sideOffset={8}148 sideOffset={8}
155 className="w-80 text-left whitespace-normal shadow-lg/10 dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5"149 className="dropdown-glass max-w-80 border-0! bg-secondary! text-left whitespace-normal shadow-lg/10 before:hidden dark:shadow-none"
156 >150 >
157 <div className="flex w-full flex-col gap-3 p-2">151 <div className="flex max-w-80 flex-col gap-2 p-2">
158 <div className="flex min-w-0 items-start gap-3">152 <div className="whitespace-nowrap text-sm font-medium text-foreground">{thread.title}</div>
159 <div className="min-w-0 flex-1 text-sm font-medium text-foreground">{thread.title}</div>153 <div className="grid gap-1.5 text-xs text-muted-foreground">
160 {status ? (
161 <div
162 className={cn(
163 "inline-flex shrink-0 items-center gap-1 text-xs font-medium",
164 status.className,
165 )}
166 >
167 {status.icon === "working" ? (
168 <CircleDashedIcon aria-hidden className="size-4 shrink-0" />
169 ) : status.icon === "done" ? (
170 <CircleCheckIcon aria-hidden className="size-4 shrink-0" />
171 ) : null}
172 {status.label}
173 </div>
174 ) : (
175 <div className="shrink-0 tabular-nums text-xs text-muted-foreground">
176 {threadTimeLabel(thread)}
177 </div>
178 )}
179 </div>
180 <div className="grid gap-2 text-xs text-muted-foreground">
181 {projectTitle ? (154 {projectTitle ? (
182 <div className="flex min-w-0 items-start gap-2">155 <div className="flex min-w-0 items-center gap-2">
183 <ProjectFavicon156 <ProjectFavicon
184 environmentId={thread.environmentId}157 environmentId={thread.environmentId}
185 cwd={projectCwd ?? ""}158 cwd={projectCwd ?? ""}
186 className="size-4 shrink-0"159 className="size-4 shrink-0"
187 />160 />
188 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">161 <div className="min-w-0 wrap-break-word text-foreground/90">{projectTitle}</div>
189 {projectTitle}
190 </div>
191 </div>162 </div>
192 ) : null}163 ) : null}
193 {environmentLabel ? (164 {environmentLabel ? (
194 <div className="flex min-w-0 items-start gap-2">165 <div className="flex min-w-0 items-center gap-2">
195 <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" />166 <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" />
196 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">167 <div className="min-w-0 wrap-break-word text-foreground/90">{environmentLabel}</div>
197 {environmentLabel}
198 </div>
199 </div>168 </div>
200 ) : null}169 ) : null}
201 {thread.branch ? (170 {thread.branch ? (
202 <div className="flex min-w-0 items-start gap-2">171 <div className="flex min-w-0 items-center gap-2">
203 <GitBranchIcon className="size-4 shrink-0 stroke-muted-foreground" />172 <GitBranchIcon className="size-4 shrink-0 stroke-muted-foreground" />
204 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">173 <div className="min-w-0 wrap-break-word text-foreground/90">{thread.branch}</div>
205 {thread.branch}
206 </div>
207 </div>174 </div>
208 ) : null}175 ) : null}
209 {branchMismatch ? (176 {branchMismatch ? (
...@@ -215,19 +182,19 @@ function SidebarV2ThreadTooltip({...@@ -215,19 +182,19 @@ function SidebarV2ThreadTooltip({
215 </div>182 </div>
216 ) : null}183 ) : null}
217 {driverKind ? (184 {driverKind ? (
218 <div className="flex min-w-0 items-start gap-2">185 <div className="flex min-w-0 items-center gap-2">
219 <ProviderInstanceIcon186 <ProviderInstanceIcon
220 driverKind={driverKind}187 driverKind={driverKind}
221 displayName={thread.session?.providerName ?? modelInstanceId}188 displayName={thread.session?.providerName ?? modelInstanceId}
222 iconClassName="size-4 shrink-0"189 iconClassName="size-4 shrink-0"
223 />190 />
224 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">{modelLabel}</div>191 <div className="min-w-0 wrap-break-word text-foreground/90">{modelLabel}</div>
225 </div>192 </div>
226 ) : null}193 ) : null}
227 {thread.session?.lastError ? (194 {thread.session?.lastError ? (
228 <div className="flex min-w-0 items-start gap-2 text-red-600 dark:text-red-400">195 <div className="flex min-w-0 items-center gap-2 text-red-600 dark:text-red-400">
229 <CircleAlertIcon className="size-4 shrink-0 stroke-current" />196 <CircleAlertIcon className="size-4 shrink-0 stroke-current" />
230 <div className="min-w-0 flex-1 wrap-break-word">{thread.session.lastError}</div>197 <div className="min-w-0 wrap-break-word">{thread.session.lastError}</div>
231 </div>198 </div>
232 ) : null}199 ) : null}
233 </div>200 </div>
...@@ -383,7 +350,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {...@@ -383,7 +350,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
383 driverKind={driverKind}350 driverKind={driverKind}
384 modelInstanceId={modelInstanceId}351 modelInstanceId={modelInstanceId}
385 modelLabel={modelLabel}352 modelLabel={modelLabel}
386 status={topStatus}
387 branchMismatch={branchMismatch}353 branchMismatch={branchMismatch}
388 />354 />
389 );355 );
...@@ -474,10 +440,11 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {...@@ -474,10 +440,11 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
474 // content; surface is reserved for interaction (hover, multi-select, route).440 // content; surface is reserved for interaction (hover, multi-select, route).
475 const rowSurfaceClassName = cn(441 const rowSurfaceClassName = cn(
476 "group/v2-row relative w-full cursor-pointer overflow-hidden rounded-md text-left outline-none select-none",442 "group/v2-row relative w-full cursor-pointer overflow-hidden rounded-md text-left outline-none select-none",
443 variant === "card" && "backdrop-blur-[16px]",
477 props.isActive444 props.isActive
478 ? "bg-sidebar-row-active text-sidebar-foreground dark:inset-ring-1 dark:inset-ring-white/5"445 ? "bg-sidebar-row-active text-sidebar-foreground"
479 : isSelected446 : isSelected
480 ? "bg-sidebar-row-selected text-sidebar-foreground dark:inset-ring-1 dark:inset-ring-white/5"447 ? "bg-sidebar-row-selected text-sidebar-foreground"
481 : shouldRecede448 : shouldRecede
482 ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"449 ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
483 : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover",450 : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover",
...@@ -758,6 +725,9 @@ export default function SidebarV2() {...@@ -758,6 +725,9 @@ export default function SidebarV2() {
758 const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, {725 const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, {
759 reportFailure: false,726 reportFailure: false,
760 });727 });
728 const deleteProject = useAtomCommand(projectEnvironment.delete, {
729 reportFailure: false,
730 });
761 const newThreadContext = useHandleNewThread();731 const newThreadContext = useHandleNewThread();
762 const openAddProjectCommandPalette = useCallback(732 const openAddProjectCommandPalette = useCallback(
763 () => openCommandPalette({ open: "add-project" }),733 () => openCommandPalette({ open: "add-project" }),
...@@ -872,6 +842,77 @@ export default function SidebarV2() {...@@ -872,6 +842,77 @@ export default function SidebarV2() {
872 clearSelection();842 clearSelection();
873 }, [clearSelection, projectScopeKey]);843 }, [clearSelection, projectScopeKey]);
874844
845 const handleRemoveProject = useCallback(
846 async (project: EnvironmentProject) => {
847 const api = readLocalApi();
848 if (!api) return;
849
850 const projectThreads = threads.filter(
851 (thread) =>
852 thread.environmentId === project.environmentId && thread.projectId === project.id,
853 );
854 const confirmed = await settlePromise(() =>
855 api.dialogs.confirm(
856 projectThreads.length > 0
857 ? [
858 `Remove project "${project.title}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?`,
859 `Path: ${project.workspaceRoot}`,
860 "This permanently clears conversation history for those threads.",
861 "This removes only the project entry, not the files on disk.",
862 "This action cannot be undone.",
863 ].join("\n")
864 : [
865 `Remove project "${project.title}"?`,
866 `Path: ${project.workspaceRoot}`,
867 "This removes only the project entry, not the files on disk.",
868 ].join("\n"),
869 ),
870 );
871 if (confirmed._tag === "Failure" || !confirmed.value) return;
872
873 const projectRef = scopeProjectRef(project.environmentId, project.id);
874 const result = await deleteProject({
875 environmentId: project.environmentId,
876 input: {
877 projectId: project.id,
878 ...(projectThreads.length > 0 ? { force: true } : {}),
879 },
880 });
881 if (result._tag === "Failure") {
882 if (!isAtomCommandInterrupted(result)) {
883 const error = squashAtomCommandFailure(result);
884 toastManager.add(
885 stackedThreadToast({
886 type: "error",
887 title: `Failed to remove "${project.title}"`,
888 description: error instanceof Error ? error.message : "An error occurred.",
889 }),
890 );
891 }
892 return;
893 }
894
895 const draftStore = useComposerDraftStore.getState();
896 const projectDraftThread = draftStore.getDraftThreadByProjectRef(projectRef);
897 if (projectDraftThread) {
898 draftStore.clearDraftThread(projectDraftThread.draftId);
899 }
900 draftStore.clearProjectDraftThreadId(projectRef);
901
902 if (
903 routeThreadRef &&
904 projectThreads.some(
905 (thread) =>
906 thread.environmentId === routeThreadRef.environmentId &&
907 thread.id === routeThreadRef.threadId,
908 )
909 ) {
910 void router.navigate({ to: "/" });
911 }
912 },
913 [deleteProject, routeThreadRef, router, threads],
914 );
915
875 // Settled threads stay in the live shell stream (settled ≠ archived), so916 // Settled threads stay in the live shell stream (settled ≠ archived), so
876 // the partition works directly off live shells: no archived-snapshot917 // the partition works directly off live shells: no archived-snapshot
877 // merging, no optimistic holds. Archived threads remain hidden here —918 // merging, no optimistic holds. Archived threads remain hidden here —
...@@ -1542,10 +1583,10 @@ export default function SidebarV2() {...@@ -1542,10 +1583,10 @@ export default function SidebarV2() {
1542 <MenuRadioItem1583 <MenuRadioItem
1543 value="all"1584 value="all"
1544 closeOnClick1585 closeOnClick
1545 className="[&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"1586 className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
1546 >1587 >
1547 <FolderIcon className="size-3.5" />1588 <FolderIcon className="size-4 shrink-0" />
1548 <span className="min-w-0 truncate">All projects</span>1589 <span className="min-w-0 truncate text-sm">All projects</span>
1549 </MenuRadioItem>1590 </MenuRadioItem>
1550 {projects.map((project) => {1591 {projects.map((project) => {
1551 const scopeKey = `${project.environmentId}:${project.id}`;1592 const scopeKey = `${project.environmentId}:${project.id}`;
...@@ -1554,14 +1595,28 @@ export default function SidebarV2() {...@@ -1554,14 +1595,28 @@ export default function SidebarV2() {
1554 key={scopeKey}1595 key={scopeKey}
1555 value={scopeKey}1596 value={scopeKey}
1556 closeOnClick1597 closeOnClick
1557 className="[&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"1598 className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
1558 >1599 >
1559 <ProjectFavicon1600 <ProjectFavicon
1560 environmentId={project.environmentId}1601 environmentId={project.environmentId}
1561 cwd={project.workspaceRoot}1602 cwd={project.workspaceRoot}
1562 className="size-3.5"1603 className="size-4 shrink-0"
1563 />1604 />
1564 <span className="min-w-0 truncate">{project.title}</span>1605 <span className="min-w-0 truncate text-sm">{project.title}</span>
1606 <button
1607 type="button"
1608 aria-label={`Remove project ${project.title}`}
1609 title={`Remove ${project.title}`}
1610 className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-destructive/12 hover:text-destructive focus-visible:bg-destructive/12 focus-visible:text-destructive focus-visible:ring-2 focus-visible:ring-destructive/40"
1611 onPointerDown={(event) => event.stopPropagation()}
1612 onClick={(event) => {
1613 event.preventDefault();
1614 event.stopPropagation();
1615 void handleRemoveProject(project);
1616 }}
1617 >
1618 <Trash2Icon className="size-3.5" />
1619 </button>
1565 </MenuRadioItem>1620 </MenuRadioItem>
1566 );1621 );
1567 })}1622 })}
...@@ -1718,9 +1773,6 @@ export default function SidebarV2() {...@@ -1718,9 +1773,6 @@ export default function SidebarV2() {
1718 ) : null}1773 ) : null}
1719 </SidebarGroup>1774 </SidebarGroup>
1720 </SidebarContent>1775 </SidebarContent>
1721 <div className="shrink-0 px-2">
1722 <SidebarSeparator className="mx-0 w-full bg-sidebar-border/80" />
1723 </div>
1724 <SidebarChromeFooter />1776 <SidebarChromeFooter />
1725 </>1777 </>
1726 );1778 );
apps/web/src/components/chat/ChangedFilesTree.tsx+1-1
...@@ -41,7 +41,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {...@@ -41,7 +41,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
41 const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]);41 const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]);
4242
43 return (43 return (
44 <div className="mt-4 rounded-2xl border border-input bg-background p-2 pt-4 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32">44 <div className="mt-4 rounded-2xl bg-background p-2 pt-4 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32">
45 <div className="sticky top-0 z-10 mb-3 flex items-center justify-between gap-2 bg-background px-2 before:absolute before:inset-x-0 before:-top-4 before:h-4 before:bg-background before:content-[''] dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))] dark:before:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]">45 <div className="sticky top-0 z-10 mb-3 flex items-center justify-between gap-2 bg-background px-2 before:absolute before:inset-x-0 before:-top-4 before:h-4 before:bg-background before:content-[''] dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))] dark:before:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]">
46 <p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">46 <p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">
47 <span>47 <span>
apps/web/src/components/chat/ChatComposer.tsx+2-12
...@@ -99,7 +99,6 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";...@@ -99,7 +99,6 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
99import { toastManager } from "../ui/toast";99import { toastManager } from "../ui/toast";
100import {100import {
101 BotIcon,101 BotIcon,
102 CheckIcon,
103 CircleAlertIcon,102 CircleAlertIcon,
104 ListTodoIcon,103 ListTodoIcon,
105 PencilRulerIcon,104 PencilRulerIcon,
...@@ -280,7 +279,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -280,7 +279,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
280 {runtimeModeOptions.map((mode) => {279 {runtimeModeOptions.map((mode) => {
281 const option = runtimeModeConfig[mode];280 const option = runtimeModeConfig[mode];
282 const OptionIcon = option.icon;281 const OptionIcon = option.icon;
283 const isSelected = props.runtimeMode === mode;
284 return (282 return (
285 <SelectItem key={mode} value={mode} hideIndicator className="min-w-64 py-2">283 <SelectItem key={mode} value={mode} hideIndicator className="min-w-64 py-2">
286 <div className="flex min-w-0 items-center gap-3">284 <div className="flex min-w-0 items-center gap-3">
...@@ -293,12 +291,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -293,12 +291,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
293 {option.description}291 {option.description}
294 </span>292 </span>
295 </div>293 </div>
296 <CheckIcon
297 className={cn(
298 "size-4 text-blue-400",
299 isSelected ? "opacity-100" : "opacity-0",
300 )}
301 />
302 </div>294 </div>
303 </SelectItem>295 </SelectItem>
304 );296 );
...@@ -2162,10 +2154,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2162,10 +2154,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2162 ref={composerSurfaceRef}2154 ref={composerSurfaceRef}
2163 data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}2155 data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}
2164 className={cn(2156 className={cn(
2165 "chat-composer-glass rounded-[20px] border transition-[background-color] duration-200 has-focus-visible:border-foreground/40",2157 "chat-composer-glass rounded-[20px] transition-[background-color] duration-200",
2166 isDragOverComposer2158 isDragOverComposer ? "bg-accent/45 ring-1 ring-primary/70" : null,
2167 ? "border-primary/70 bg-accent/45"
2168 : "border-black/12 dark:border-transparent dark:inset-ring-1 dark:inset-ring-white/5",
2169 projectSelectionRequired ? "opacity-75" : null,2159 projectSelectionRequired ? "opacity-75" : null,
2170 composerProviderState.composerSurfaceClassName,2160 composerProviderState.composerSurfaceClassName,
2171 )}2161 )}
apps/web/src/components/chat/ComposerPrimaryActions.tsx+13-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import { memo, type PointerEventHandler } from "react";1import { memo, type PointerEventHandler } from "react";
2import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react";2import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react";
3import { cn } from "~/lib/utils";3import { cn } from "~/lib/utils";
4import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop";
4import { Button } from "../ui/button";5import { Button } from "../ui/button";
5import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu";6import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu";
6import { Spinner } from "../ui/spinner";7import { Spinner } from "../ui/spinner";
...@@ -71,6 +72,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -71,6 +72,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
71 const pointerFocusProps = preserveComposerFocusOnPointerDown72 const pointerFocusProps = preserveComposerFocusOnPointerDown
72 ? { onPointerDown: preventPointerFocus }73 ? { onPointerDown: preventPointerFocus }
73 : undefined;74 : undefined;
75 const stageBackdropVariant = useSidebarStageBackdropVariant();
7476
75 if (pendingAction) {77 if (pendingAction) {
76 return (78 return (
...@@ -196,7 +198,12 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -196,7 +198,12 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
196 return (198 return (
197 <button199 <button
198 type="submit"200 type="submit"
199 className="flex h-9 w-9 enabled:cursor-pointer items-center justify-center rounded-full bg-primary/90 text-primary-foreground shadow-xs enabled:shadow-primary/24 enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] transition-all duration-150 hover:bg-primary hover:scale-105 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:pointer-events-none disabled:opacity-30 disabled:shadow-none disabled:hover:scale-100 sm:h-8 sm:w-8"201 className={cn(
202 "relative isolate flex h-9 w-9 items-center justify-center overflow-hidden rounded-full text-primary-foreground shadow-xs transition-all duration-150 enabled:cursor-pointer enabled:inset-shadow-[0_1px_--theme(--color-white/16%)] hover:scale-105 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none disabled:pointer-events-none disabled:opacity-30 disabled:shadow-none disabled:hover:scale-100 sm:h-8 sm:w-8",
203 stageBackdropVariant
204 ? "bg-transparent enabled:shadow-black/24 enabled:hover:brightness-110"
205 : "bg-primary/90 enabled:shadow-primary/24 hover:bg-primary",
206 )}
200 {...pointerFocusProps}207 {...pointerFocusProps}
201 disabled={isSendBusy || isConnecting || isEnvironmentUnavailable || !hasSendableContent}208 disabled={isSendBusy || isConnecting || isEnvironmentUnavailable || !hasSendableContent}
202 aria-label={209 aria-label={
...@@ -211,6 +218,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -211,6 +218,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
211 : "Send message"218 : "Send message"
212 }219 }
213 >220 >
221 {stageBackdropVariant ? (
222 <span className="absolute inset-0 -z-10" aria-hidden="true">
223 <StageBackdropButtonArt variant={stageBackdropVariant} />
224 </span>
225 ) : null}
214 {isConnecting || isSendBusy ? (226 {isConnecting || isSendBusy ? (
215 <Spinner className="size-3.5" aria-hidden="true" />227 <Spinner className="size-3.5" aria-hidden="true" />
216 ) : (228 ) : (
apps/web/src/components/chat/ContextWindowMeter.tsx+12-5
...@@ -25,7 +25,9 @@ export function ContextWindowMeter(props: {...@@ -25,7 +25,9 @@ export function ContextWindowMeter(props: {
25 const totalProcessedTokens = usage.totalProcessedTokens ?? null;25 const totalProcessedTokens = usage.totalProcessedTokens ?? null;
26 const showTotalProcessed = totalProcessedTokens !== null && totalProcessedTokens > 0;26 const showTotalProcessed = totalProcessedTokens !== null && totalProcessedTokens > 0;
27 const isOverloaded = normalizedPercentage > 90;27 const isOverloaded = normalizedPercentage > 90;
28 const usageColor = isOverloaded ? "var(--color-red-500)" : "var(--color-blue-500)";28 const usageColor = isOverloaded
29 ? "var(--color-red-500)"
30 : "color-mix(in oklab, var(--color-muted-foreground) 72%, transparent)";
2931
30 return (32 return (
31 <Popover>33 <Popover>
...@@ -37,7 +39,7 @@ export function ContextWindowMeter(props: {...@@ -37,7 +39,7 @@ export function ContextWindowMeter(props: {
37 <button39 <button
38 type="button"40 type="button"
39 className={cn(41 className={cn(
40 "inline-flex size-6 cursor-pointer items-center justify-center rounded-full border border-transparent text-muted-foreground outline-none transition-colors",42 "inline-flex size-7 cursor-pointer items-center justify-center rounded-full border border-transparent text-muted-foreground outline-none transition-colors",
41 "hover:bg-accent data-[pressed]:bg-accent",43 "hover:bg-accent data-[pressed]:bg-accent",
42 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",44 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
43 )}45 )}
...@@ -47,7 +49,7 @@ export function ContextWindowMeter(props: {...@@ -47,7 +49,7 @@ export function ContextWindowMeter(props: {
47 : `Context window ${formatContextWindowTokens(usage.usedTokens)} tokens used`49 : `Context window ${formatContextWindowTokens(usage.usedTokens)} tokens used`
48 }50 }
49 >51 >
50 <span className="relative flex size-4 items-center justify-center">52 <span className="relative flex size-5 items-center justify-center">
51 <svg53 <svg
52 viewBox="0 0 24 24"54 viewBox="0 0 24 24"
53 className="-rotate-90 absolute inset-0 size-full transform-gpu"55 className="-rotate-90 absolute inset-0 size-full transform-gpu"
...@@ -58,7 +60,7 @@ export function ContextWindowMeter(props: {...@@ -58,7 +60,7 @@ export function ContextWindowMeter(props: {
58 cy="12"60 cy="12"
59 r={radius}61 r={radius}
60 fill="none"62 fill="none"
61 stroke="color-mix(in oklab, var(--color-muted-foreground) 35%, transparent)"63 stroke="color-mix(in oklab, var(--color-muted-foreground) 24%, transparent)"
62 strokeWidth="3"64 strokeWidth="3"
63 />65 />
64 <circle66 <circle
...@@ -78,7 +80,12 @@ export function ContextWindowMeter(props: {...@@ -78,7 +80,12 @@ export function ContextWindowMeter(props: {
78 </button>80 </button>
79 }81 }
80 />82 />
81 <PopoverPopup tooltipStyle side="top" align="end" className="w-64 max-w-none p-0">83 <PopoverPopup
84 tooltipStyle
85 side="top"
86 align="end"
87 className="dropdown-glass w-64 max-w-none border-0! bg-secondary! p-0 shadow-none! before:hidden"
88 >
82 <div className="flex flex-col gap-2 p-3">89 <div className="flex flex-col gap-2 p-3">
83 <div className="flex items-center justify-between gap-3">90 <div className="flex items-center justify-between gap-3">
84 <div className="font-medium text-muted-foreground text-xs">Context Window</div>91 <div className="font-medium text-muted-foreground text-xs">Context Window</div>
apps/web/src/components/chat/MessagesTimeline.test.tsx+17
...@@ -219,6 +219,23 @@ function buildUserTimelineEntry(text: string) {...@@ -219,6 +219,23 @@ function buildUserTimelineEntry(text: string) {
219}219}
220220
221describe("MessagesTimeline", () => {221describe("MessagesTimeline", () => {
222 it("uses the larger leading inset only when the top fade is enabled", async () => {
223 const { MessagesTimeline } = await import("./MessagesTimeline");
224 const timelineEntries = [buildUserTimelineEntry("Hello")];
225
226 const compactMarkup = renderToStaticMarkup(
227 <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} />,
228 );
229 const fadedMarkup = renderToStaticMarkup(
230 <MessagesTimeline {...buildProps()} timelineEntries={timelineEntries} topFadeEnabled />,
231 );
232
233 expect(compactMarkup).toContain('class="h-3 sm:h-4"');
234 expect(compactMarkup).not.toContain("chat-timeline-scroll-fade");
235 expect(fadedMarkup).toContain('class="h-10 sm:h-12"');
236 expect(fadedMarkup).toContain("chat-timeline-scroll-fade");
237 });
238
222 it("keeps assistant changed-files headers sticky below the thread header", async () => {239 it("keeps assistant changed-files headers sticky below the thread header", async () => {
223 const { MessagesTimeline } = await import("./MessagesTimeline");240 const { MessagesTimeline } = await import("./MessagesTimeline");
224 const assistantMessageId = MessageId.make("message-assistant-with-files");241 const assistantMessageId = MessageId.make("message-assistant-with-files");
apps/web/src/components/chat/MessagesTimeline.tsx+11-5
...@@ -150,6 +150,7 @@ interface TimelineRowActivityState {...@@ -150,6 +150,7 @@ interface TimelineRowActivityState {
150const TimelineRowCtx = createContext<TimelineRowSharedState>(null!);150const TimelineRowCtx = createContext<TimelineRowSharedState>(null!);
151const TimelineRowActivityCtx = createContext<TimelineRowActivityState>(null!);151const TimelineRowActivityCtx = createContext<TimelineRowActivityState>(null!);
152const TIMELINE_LIST_HEADER = <div className="h-3 sm:h-4" />;152const TIMELINE_LIST_HEADER = <div className="h-3 sm:h-4" />;
153const TIMELINE_LIST_FADE_HEADER = <div className="h-10 sm:h-12" />;
153const TIMELINE_LIST_FOOTER = <div className="h-3 sm:h-4" />;154const TIMELINE_LIST_FOOTER = <div className="h-3 sm:h-4" />;
154const EMPTY_TIMELINE_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];155const EMPTY_TIMELINE_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
155156
...@@ -185,6 +186,7 @@ interface MessagesTimelineProps {...@@ -185,6 +186,7 @@ interface MessagesTimelineProps {
185 onIsAtEndChange: (isAtEnd: boolean) => void;186 onIsAtEndChange: (isAtEnd: boolean) => void;
186 onManualNavigation: () => void;187 onManualNavigation: () => void;
187 hideEmptyPlaceholder?: boolean;188 hideEmptyPlaceholder?: boolean;
189 topFadeEnabled?: boolean;
188}190}
189191
190// ---------------------------------------------------------------------------192// ---------------------------------------------------------------------------
...@@ -219,6 +221,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -219,6 +221,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
219 onIsAtEndChange,221 onIsAtEndChange,
220 onManualNavigation,222 onManualNavigation,
221 hideEmptyPlaceholder = false,223 hideEmptyPlaceholder = false,
224 topFadeEnabled = false,
222}: MessagesTimelineProps) {225}: MessagesTimelineProps) {
223 const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());226 const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());
224 const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());227 const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());
...@@ -510,8 +513,11 @@ export const MessagesTimeline = memo(function MessagesTimeline({...@@ -510,8 +513,11 @@ export const MessagesTimeline = memo(function MessagesTimeline({
510 size: false,513 size: false,
511 }}514 }}
512 onScroll={handleScroll}515 onScroll={handleScroll}
513 className="scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5"516 className={cn(
514 ListHeaderComponent={TIMELINE_LIST_HEADER}517 "scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5",
518 topFadeEnabled && "chat-timeline-scroll-fade",
519 )}
520 ListHeaderComponent={topFadeEnabled ? TIMELINE_LIST_FADE_HEADER : TIMELINE_LIST_HEADER}
515 ListFooterComponent={TIMELINE_LIST_FOOTER}521 ListFooterComponent={TIMELINE_LIST_FOOTER}
516 />522 />
517 <TimelineMinimap523 <TimelineMinimap
...@@ -797,7 +803,7 @@ function TimelineMinimap({...@@ -797,7 +803,7 @@ function TimelineMinimap({
797 transform: `translateY(${activeTooltipTranslate})`,803 transform: `translateY(${activeTooltipTranslate})`,
798 }}804 }}
799 >805 >
800 <span className="block rounded-xl border border-border/70 bg-popover/95 p-3 text-left text-popover-foreground shadow-xl shadow-black/25 backdrop-blur">806 <span className="dropdown-glass block rounded-xl p-3 text-left text-popover-foreground shadow-xl shadow-black/25">
801 <span className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5">807 <span className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5">
802 {activeItem.userText ?? "User message"}808 {activeItem.userText ?? "User message"}
803 </span>809 </span>
...@@ -886,7 +892,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"...@@ -886,7 +892,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
886892
887 return (893 return (
888 <div className="group flex flex-col items-end gap-1">894 <div className="group flex flex-col items-end gap-1">
889 <div className="relative max-w-[80%] rounded-2xl border border-border bg-secondary p-3">895 <div className="relative max-w-[80%] rounded-2xl bg-secondary p-3">
890 {regularImages.length > 0 && (896 {regularImages.length > 0 && (
891 <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">897 <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
892 {regularImages.map((image: NonNullable<TimelineMessage["attachments"]>[number]) => (898 {regularImages.map((image: NonNullable<TimelineMessage["attachments"]>[number]) => (
...@@ -1279,7 +1285,7 @@ function AssistantChangedFilesSectionInner({...@@ -1279,7 +1285,7 @@ function AssistantChangedFilesSectionInner({
1279 const summaryStat = summarizeTurnDiffStats(checkpointFiles);1285 const summaryStat = summarizeTurnDiffStats(checkpointFiles);
12801286
1281 return (1287 return (
1282 <div className="mt-4 rounded-2xl border border-input bg-background p-2 pt-4 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32">1288 <div className="mt-4 rounded-2xl bg-background p-2 pt-4 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32">
1283 <div className="sticky top-2 z-10 mb-3 flex items-center justify-between gap-2 bg-background px-2 before:absolute before:inset-x-0 before:-top-4 before:h-4 before:bg-background before:content-[''] dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))] dark:before:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]">1289 <div className="sticky top-2 z-10 mb-3 flex items-center justify-between gap-2 bg-background px-2 before:absolute before:inset-x-0 before:-top-4 before:h-4 before:bg-background before:content-[''] dark:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))] dark:before:bg-[color-mix(in_srgb,var(--foreground)_2.5%,var(--background))]">
1284 <p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">1290 <p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">
1285 <span>1291 <span>
apps/web/src/components/chat/ModelListRow.tsx+2-3
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts";1import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts";
2import { memo } from "react";2import { memo } from "react";
3import { CheckIcon, StarIcon } from "lucide-react";3import { StarIcon } from "lucide-react";
4import {4import {
5 getDisplayModelName,5 getDisplayModelName,
6 getTriggerDisplayModelLabel,6 getTriggerDisplayModelLabel,
...@@ -51,7 +51,7 @@ export const ModelListRow = memo(function ModelListRow(props: {...@@ -51,7 +51,7 @@ export const ModelListRow = memo(function ModelListRow(props: {
51 contentClassName="flex w-full items-center gap-3"51 contentClassName="flex w-full items-center gap-3"
52 className={cn(52 className={cn(
53 "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2.5 transition-[background-color,box-shadow,color]",53 "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2.5 transition-[background-color,box-shadow,color]",
54 "data-highlighted:bg-muted/56 data-selected:bg-transparent data-selected:text-foreground data-selected:ring-0",54 "data-highlighted:bg-muted/56 data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0",
55 props.disabledReason &&55 props.disabledReason &&
56 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",56 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",
57 )}57 )}
...@@ -66,7 +66,6 @@ export const ModelListRow = memo(function ModelListRow(props: {...@@ -66,7 +66,6 @@ export const ModelListRow = memo(function ModelListRow(props: {
66 props.preferShortName ? { preferShortName: true } : undefined,66 props.preferShortName ? { preferShortName: true } : undefined,
67 )}67 )}
68 </div>68 </div>
69 {props.isSelected ? <CheckIcon className="size-3.5 shrink-0 text-blue-400" /> : null}
70 {props.showNewBadge ? (69 {props.showNewBadge ? (
71 <span70 <span
72 className="shrink-0 rounded border border-amber-500/35 bg-amber-500/15 px-0.5 py-px text-[10px] font-bold uppercase leading-none tracking-wide text-amber-800 dark:border-amber-400/30 dark:bg-amber-400/12 dark:text-amber-200"71 className="shrink-0 rounded border border-amber-500/35 bg-amber-500/15 px-0.5 py-px text-[10px] font-bold uppercase leading-none tracking-wide text-amber-800 dark:border-amber-400/30 dark:bg-amber-400/12 dark:text-amber-200"
apps/web/src/components/chat/ModelPickerContent.tsx+3-8
...@@ -521,7 +521,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -521,7 +521,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
521 return (521 return (
522 <TooltipProvider delay={0}>522 <TooltipProvider delay={0}>
523 <div523 <div
524 className="relative flex h-screen max-h-96 w-screen max-w-100 flex-row overflow-hidden rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5 dark:before:shadow-none"524 className="dropdown-glass model-picker-surface relative flex h-screen max-h-96 w-screen max-w-100 flex-row overflow-hidden rounded-xl text-popover-foreground"
525 data-model-picker-content="true"525 data-model-picker-content="true"
526 >526 >
527 {/* Sidebar */}527 {/* Sidebar */}
...@@ -568,15 +568,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -568,15 +568,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
568 handleModelSelect(slug, instanceId);568 handleModelSelect(slug, instanceId);
569 }}569 }}
570 >570 >
571 <div571 <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
572 className={cn(
573 "flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40",
574 showSidebar && "border-l",
575 )}
576 >
577 {/* Search bar */}572 {/* Search bar */}
578 <div className="px-4 pt-2.5">573 <div className="px-4 pt-2.5">
579 <div className="-translate-y-px border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring">574 <div className="-translate-y-px rounded-lg bg-accent px-2 py-1">
580 <ComboboxInput575 <ComboboxInput
581 ref={searchInputRef}576 ref={searchInputRef}
582 className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5"577 className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5"
apps/web/src/components/chat/ModelPickerSidebar.tsx+2-5
...@@ -97,10 +97,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -97,10 +97,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
97 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);97 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);
9898
99 return (99 return (
100 <div100 <div className="w-12 shrink-0 overflow-hidden bg-muted/60" data-model-picker-sidebar="true">
101 className="w-12 shrink-0 overflow-hidden border-r bg-muted/30"
102 data-model-picker-sidebar="true"
103 >
104 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">101 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
105 <div102 <div
106 ref={sidebarContentRef}103 ref={sidebarContentRef}
...@@ -118,7 +115,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -118,7 +115,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
118 ) : null}115 ) : null}
119 {/* Favorites section */}116 {/* Favorites section */}
120 {showFavorites ? (117 {showFavorites ? (
121 <div className="mb-1 border-b pb-1">118 <div className="mb-1 pb-1">
122 <div className="relative w-full">119 <div className="relative w-full">
123 <Tooltip>120 <Tooltip>
124 <TooltipTrigger121 <TooltipTrigger
apps/web/src/components/chat/ProviderStatusBanner.tsx+1-1
...@@ -27,7 +27,7 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({...@@ -27,7 +27,7 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
27 : `${providerName} provider has limited availability.`));27 : `${providerName} provider has limited availability.`));
2828
29 return (29 return (
30 <div className="mx-auto w-fit max-w-[calc(100%-2rem)] pt-3">30 <div className="pointer-events-auto mx-auto w-fit max-w-[calc(100%-2rem)] pt-3">
31 <div31 <div
32 className={cn(32 className={cn(
33 "inline-flex items-center gap-3 rounded-xl border px-3.5 py-3 text-card-foreground text-sm",33 "inline-flex items-center gap-3 rounded-xl border px-3.5 py-3 text-card-foreground text-sm",
apps/web/src/components/chat/TraitsPicker.tsx+1-7
...@@ -16,7 +16,7 @@ import {...@@ -16,7 +16,7 @@ import {
16} from "@t3tools/shared/model";16} from "@t3tools/shared/model";
17import { memo, useCallback, useState } from "react";17import { memo, useCallback, useState } from "react";
18import type { VariantProps } from "class-variance-authority";18import type { VariantProps } from "class-variance-authority";
19import { CheckIcon, ChevronDownIcon } from "lucide-react";19import { ChevronDownIcon } from "lucide-react";
20import { Button, buttonVariants } from "../ui/button";20import { Button, buttonVariants } from "../ui/button";
21import {21import {
22 Menu,22 Menu,
...@@ -337,9 +337,6 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({...@@ -337,9 +337,6 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
337 </>337 </>
338 ) : null}338 ) : null}
339 </span>339 </span>
340 {option.id === selectedValue ? (
341 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
342 ) : null}
343 </span>340 </span>
344 </MenuRadioItem>341 </MenuRadioItem>
345 ))}342 ))}
...@@ -370,9 +367,6 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({...@@ -370,9 +367,6 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
370 <MenuRadioItem key={value} value={value} hideIndicator>367 <MenuRadioItem key={value} value={value} hideIndicator>
371 <span className="flex w-full min-w-0 items-center justify-between gap-3">368 <span className="flex w-full min-w-0 items-center justify-between gap-3">
372 <span>{value === "on" ? "On" : "Off"}</span>369 <span>{value === "on" ? "On" : "Off"}</span>
373 {value === selectedValue ? (
374 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
375 ) : null}
376 </span>370 </span>
377 </MenuRadioItem>371 </MenuRadioItem>
378 ))}372 ))}
apps/web/src/components/settings/ConnectionsSettings.tsx+3-6
...@@ -373,7 +373,7 @@ function formatDesktopSshConnectionError(error: unknown): string {...@@ -373,7 +373,7 @@ function formatDesktopSshConnectionError(error: unknown): string {
373 return withoutTaggedErrorPrefix.trim() || fallback;373 return withoutTaggedErrorPrefix.trim() || fallback;
374}374}
375375
376const ENDPOINT_ROW_CLASSNAME = "border-t border-border/60 px-4 py-2.5 first:border-t-0 sm:px-5";376const ENDPOINT_ROW_CLASSNAME = "rounded-xl px-3 py-2.5 sm:px-4";
377377
378type AccessSectionPresentation = "current" | "endpoint-rail";378type AccessSectionPresentation = "current" | "endpoint-rail";
379379
...@@ -383,10 +383,7 @@ function accessRowClassName(_presentation: AccessSectionPresentation) {...@@ -383,10 +383,7 @@ function accessRowClassName(_presentation: AccessSectionPresentation) {
383383
384function endpointRowClassName(presentation: AccessSectionPresentation, isAvailable: boolean) {384function endpointRowClassName(presentation: AccessSectionPresentation, isAvailable: boolean) {
385 if (presentation === "endpoint-rail") {385 if (presentation === "endpoint-rail") {
386 return cn(386 return cn("relative rounded-xl px-3 py-3 sm:px-4", !isAvailable && "bg-muted/15");
387 "relative border-t border-border/60 px-4 py-3 first:border-t-0 sm:px-5",
388 !isAvailable && "bg-muted/20",
389 );
390 }387 }
391388
392 return cn(ENDPOINT_ROW_CLASSNAME, !isAvailable && "bg-muted/24");389 return cn(ENDPOINT_ROW_CLASSNAME, !isAvailable && "bg-muted/24");
...@@ -1524,7 +1521,7 @@ const DesktopSshHostRow = memo(function DesktopSshHostRow({...@@ -1524,7 +1521,7 @@ const DesktopSshHostRow = memo(function DesktopSshHostRow({
1524 const buttonLabel = connectingHostAlias === target.alias ? "Adding…" : "Add environment";1521 const buttonLabel = connectingHostAlias === target.alias ? "Adding…" : "Add environment";
15251522
1526 return (1523 return (
1527 <div className="border-t border-border/60 px-4 py-3 first:border-t-0 sm:px-5">1524 <div className="rounded-xl px-3 py-3 sm:px-4">
1528 <div className={ITEM_ROW_INNER_CLASSNAME}>1525 <div className={ITEM_ROW_INNER_CLASSNAME}>
1529 <div className="min-w-0 flex-1">1526 <div className="min-w-0 flex-1">
1530 <h3 className="truncate text-sm font-medium text-foreground">{target.alias}</h3>1527 <h3 className="truncate text-sm font-medium text-foreground">{target.alias}</h3>
apps/web/src/components/settings/ProviderInstanceCard.tsx+9-9
...@@ -531,7 +531,7 @@ export function ProviderInstanceCard({...@@ -531,7 +531,7 @@ export function ProviderInstanceCard({
531 const titleHeadNode = (531 const titleHeadNode = (
532 <>532 <>
533 {titleIconNode}533 {titleIconNode}
534 <h3 className="truncate text-[13px] font-semibold tracking-[-0.01em] text-foreground">534 <h3 className="truncate text-sm font-medium tracking-[-0.005em] text-foreground">
535 {displayName}535 {displayName}
536 </h3>536 </h3>
537 {String(instanceId) !== String(instance.driver) ? (537 {String(instanceId) !== String(instance.driver) ? (
...@@ -578,7 +578,7 @@ export function ProviderInstanceCard({...@@ -578,7 +578,7 @@ export function ProviderInstanceCard({
578 );578 );
579579
580 const authRowNode = (580 const authRowNode = (
581 <p className="flex min-w-0 flex-wrap items-center gap-x-1 text-xs text-muted-foreground/80">581 <p className="flex min-w-0 flex-wrap items-center gap-x-1 text-[13px] leading-[1.45] text-muted-foreground/80">
582 {hasAuthenticatedEmail ? (582 {hasAuthenticatedEmail ? (
583 <>583 <>
584 <span>Authenticated as</span>584 <span>Authenticated as</span>
...@@ -600,8 +600,8 @@ export function ProviderInstanceCard({...@@ -600,8 +600,8 @@ export function ProviderInstanceCard({
600 ) : null;600 ) : null;
601601
602 return (602 return (
603 <div className="border-t border-border/60 first:border-t-0">603 <div className="rounded-xl transition-colors hover:bg-muted/20">
604 <div className="px-4 py-3.5 sm:px-5">604 <div className="px-3 py-3 sm:px-4">
605 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">605 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
606 <div className="min-w-0 flex-1 space-y-1">606 <div className="min-w-0 flex-1 space-y-1">
607 <div className="flex min-w-0 flex-wrap items-center gap-2">607 <div className="flex min-w-0 flex-wrap items-center gap-2">
...@@ -729,8 +729,8 @@ export function ProviderInstanceCard({...@@ -729,8 +729,8 @@ export function ProviderInstanceCard({
729729
730 <Collapsible open={isExpanded} onOpenChange={onExpandedChange}>730 <Collapsible open={isExpanded} onOpenChange={onExpandedChange}>
731 <CollapsibleContent>731 <CollapsibleContent>
732 <div className="space-y-0">732 <div className="space-y-5 px-3 pb-4 pt-2 sm:px-4">
733 <div className="border-t border-border/60 px-4 py-3 sm:px-5">733 <div>
734 <label htmlFor={`provider-instance-${instanceId}-display-name`} className="block">734 <label htmlFor={`provider-instance-${instanceId}-display-name`} className="block">
735 <span className="text-xs font-medium text-foreground">Display name</span>735 <span className="text-xs font-medium text-foreground">Display name</span>
736 <DraftInput736 <DraftInput
...@@ -747,7 +747,7 @@ export function ProviderInstanceCard({...@@ -747,7 +747,7 @@ export function ProviderInstanceCard({
747 </label>747 </label>
748 </div>748 </div>
749749
750 <div className="border-t border-border/60 px-4 py-3 sm:px-5">750 <div>
751 <ProviderAccentColorPicker751 <ProviderAccentColorPicker
752 displayName={displayName}752 displayName={displayName}
753 value={accentColor}753 value={accentColor}
...@@ -757,7 +757,7 @@ export function ProviderInstanceCard({...@@ -757,7 +757,7 @@ export function ProviderInstanceCard({
757 />757 />
758 </div>758 </div>
759759
760 <div className="border-t border-border/60 px-4 py-3 sm:px-5">760 <div>
761 <ProviderEnvironmentSection761 <ProviderEnvironmentSection
762 environment={instance.environment ?? []}762 environment={instance.environment ?? []}
763 onChange={updateEnvironment}763 onChange={updateEnvironment}
...@@ -789,7 +789,7 @@ export function ProviderInstanceCard({...@@ -789,7 +789,7 @@ export function ProviderInstanceCard({
789 onModelOrderChange={onModelOrderChange}789 onModelOrderChange={onModelOrderChange}
790 />790 />
791 ) : (791 ) : (
792 <div className="border-t border-border/60 px-4 py-3 sm:px-5">792 <div>
793 <p className="text-xs text-muted-foreground">793 <p className="text-xs text-muted-foreground">
794 This instance uses a driver (794 This instance uses a driver (
795 <code className="text-foreground">{String(instance.driver)}</code>) that is not795 <code className="text-foreground">{String(instance.driver)}</code>) that is not
apps/web/src/components/settings/ProviderModelsSection.tsx+1-1
...@@ -185,7 +185,7 @@ export function ProviderModelsSection({...@@ -185,7 +185,7 @@ export function ProviderModelsSection({
185 };185 };
186186
187 return (187 return (
188 <div className="border-t border-border/60 px-4 py-3 sm:px-5">188 <div>
189 <div className="text-xs font-medium text-foreground">Models</div>189 <div className="text-xs font-medium text-foreground">Models</div>
190 <div className="mt-1 text-xs text-muted-foreground">190 <div className="mt-1 text-xs text-muted-foreground">
191 {models.length} model{models.length === 1 ? "" : "s"} available.191 {models.length} model{models.length === 1 ? "" : "s"} available.
apps/web/src/components/settings/ProviderSettingsForm.tsx+1-1
...@@ -167,7 +167,7 @@ function FieldFrame(props: {...@@ -167,7 +167,7 @@ function FieldFrame(props: {
167 readonly children: ReactNode;167 readonly children: ReactNode;
168}) {168}) {
169 if (props.variant === "card") {169 if (props.variant === "card") {
170 return <div className="border-t border-border/60 px-4 py-3 sm:px-5">{props.children}</div>;170 return <div>{props.children}</div>;
171 }171 }
172 return <div className="grid gap-1.5">{props.children}</div>;172 return <div className="grid gap-1.5">{props.children}</div>;
173}173}
apps/web/src/components/settings/SettingsSidebarNav.tsx+4-7
...@@ -18,7 +18,6 @@ import {...@@ -18,7 +18,6 @@ import {
18 SidebarMenu,18 SidebarMenu,
19 SidebarMenuButton,19 SidebarMenuButton,
20 SidebarMenuItem,20 SidebarMenuItem,
21 SidebarSeparator,
22 useSidebar,21 useSidebar,
23} from "../ui/sidebar";22} from "../ui/sidebar";
24import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn";23import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn";
...@@ -85,8 +84,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -85,8 +84,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
85 isActive={isActive}84 isActive={isActive}
86 className={85 className={
87 isActive86 isActive
88 ? "gap-2.5 px-2.5 py-2 text-left text-[13px] font-medium text-sidebar-foreground"87 ? "h-8 items-center gap-2 rounded-md bg-sidebar-row-active px-2 py-1.5 text-left text-sm font-medium text-sidebar-foreground"
89 : "gap-2.5 px-2.5 py-2 text-left text-[13px] text-sidebar-muted-foreground hover:text-sidebar-foreground"88 : "h-8 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
90 }89 }
91 onClick={() => handleSectionClick(item.to)}90 onClick={() => handleSectionClick(item.to)}
92 >91 >
...@@ -94,7 +93,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -94,7 +93,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
94 className={93 className={
95 isActive94 isActive
96 ? "size-4 shrink-0 text-sidebar-foreground"95 ? "size-4 shrink-0 text-sidebar-foreground"
97 : "size-4 shrink-0 text-sidebar-muted-foreground"96 : "size-4 shrink-0 text-sidebar-muted-foreground/60"
98 }97 }
99 />98 />
100 <span className="truncate">{item.label}</span>99 <span className="truncate">{item.label}</span>
...@@ -105,8 +104,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -105,8 +104,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
105 </SidebarMenu>104 </SidebarMenu>
106 </SidebarGroup>105 </SidebarGroup>
107 </SidebarContent>106 </SidebarContent>
108
109 <SidebarSeparator />
110 <SidebarFooter className="p-2">107 <SidebarFooter className="p-2">
111 <T3ConnectSidebarSignIn />108 <T3ConnectSidebarSignIn />
112 <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">109 <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">
...@@ -114,7 +111,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -114,7 +111,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
114 <SidebarMenuItem>111 <SidebarMenuItem>
115 <SidebarMenuButton112 <SidebarMenuButton
116 size="sm"113 size="sm"
117 className="gap-2 px-2 py-2 text-xs text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground"114 className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
118 onClick={handleBackClick}115 onClick={handleBackClick}
119 >116 >
120 <ArrowLeftIcon className="size-4" />117 <ArrowLeftIcon className="size-4" />
apps/web/src/components/settings/SourceControlSettings.tsx+6-6
...@@ -229,16 +229,16 @@ function DiscoveryItemRow({...@@ -229,16 +229,16 @@ function DiscoveryItemRow({
229 return (229 return (
230 <div230 <div
231 className={cn(231 className={cn(
232 "border-t border-border/60 first:border-t-0",232 "rounded-xl transition-colors hover:bg-muted/20",
233 isVcsNotReady(item) && "opacity-80",233 isVcsNotReady(item) && "opacity-80",
234 )}234 )}
235 >235 >
236 <div className="px-4 py-3.5 sm:px-5">236 <div className="px-3 py-3 sm:px-4">
237 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">237 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
238 <div className="min-w-0 flex-1 space-y-1">238 <div className="min-w-0 flex-1 space-y-1">
239 <div className="flex min-w-0 flex-wrap items-center gap-2">239 <div className="flex min-w-0 flex-wrap items-center gap-2">
240 <SourceControlItemMark item={item} />240 <SourceControlItemMark item={item} />
241 <span className="truncate text-[13px] font-semibold tracking-[-0.01em] text-foreground">241 <span className="truncate text-sm font-medium tracking-[-0.005em] text-foreground">
242 {item.label}242 {item.label}
243 </span>243 </span>
244 {version ? <code className="text-xs text-muted-foreground">{version}</code> : null}244 {version ? <code className="text-xs text-muted-foreground">{version}</code> : null}
...@@ -253,7 +253,7 @@ function DiscoveryItemRow({...@@ -253,7 +253,7 @@ function DiscoveryItemRow({
253 </Badge>253 </Badge>
254 ) : null}254 ) : null}
255 </div>255 </div>
256 <p className="flex min-w-0 flex-wrap items-center gap-x-1 text-xs text-muted-foreground/80">256 <p className="flex min-w-0 flex-wrap items-center gap-x-1 text-[13px] leading-[1.45] text-muted-foreground/80">
257 {itemSummary({ item, auth, authAccount })}257 {itemSummary({ item, auth, authAccount })}
258 </p>258 </p>
259 </div>259 </div>
...@@ -282,7 +282,7 @@ function DiscoveryItemRow({...@@ -282,7 +282,7 @@ function DiscoveryItemRow({
282 {hasDetails ? (282 {hasDetails ? (
283 <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>283 <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
284 <CollapsibleContent>284 <CollapsibleContent>
285 <div className="border-t border-border/60 px-4 py-3 sm:px-5">{children}</div>285 <div className="px-3 pb-4 pt-1 sm:px-4">{children}</div>
286 </CollapsibleContent>286 </CollapsibleContent>
287 </Collapsible>287 </Collapsible>
288 ) : null}288 ) : null}
...@@ -368,7 +368,7 @@ function SourceControlSectionSkeleton({...@@ -368,7 +368,7 @@ function SourceControlSectionSkeleton({
368 return (368 return (
369 <SettingsSection title={title} headerAction={headerAction}>369 <SettingsSection title={title} headerAction={headerAction}>
370 {SOURCE_CONTROL_SKELETON_ROWS.map((row) => (370 {SOURCE_CONTROL_SKELETON_ROWS.map((row) => (
371 <div key={row} className="border-t border-border/60 px-4 py-3.5 first:border-t-0 sm:px-5">371 <div key={row} className="rounded-xl px-3 py-3 sm:px-4">
372 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">372 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
373 <div className="min-w-0 flex-1 space-y-2">373 <div className="min-w-0 flex-1 space-y-2">
374 <div className="flex items-center gap-2">374 <div className="flex items-center gap-2">
apps/web/src/components/settings/itemRows.ts+2-2
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1/** Direct row in the card – same pattern as the Provider / ACP-agent list rows. */1/** Direct row in a settings section. Whitespace, rather than rules, separates peers. */
2export const ITEM_ROW_CLASSNAME = "border-t border-border/60 px-4 py-4 first:border-t-0 sm:px-5";2export const ITEM_ROW_CLASSNAME = "rounded-xl px-3 py-3 sm:px-4";
33
4export const ITEM_ROW_INNER_CLASSNAME =4export const ITEM_ROW_INNER_CLASSNAME =
5 "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between";5 "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between";
apps/web/src/components/settings/settingsLayout.tsx+14-21
...@@ -29,18 +29,15 @@ export function SettingsSection({...@@ -29,18 +29,15 @@ export function SettingsSection({
29 children: ReactNode;29 children: ReactNode;
30}) {30}) {
31 return (31 return (
32 <section {...sectionProps} className={cn("space-y-2.5", className)}>32 <section {...sectionProps} className={cn("space-y-3", className)}>
33 <div className="flex items-center justify-between px-1">33 <div className="flex min-h-8 items-center justify-between gap-4 px-3 sm:px-4">
34 <h2 className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-foreground/50">34 <h2 className="flex items-center gap-2 text-lg font-semibold tracking-[-0.025em] text-foreground">
35 <span className="inline-block h-px w-3 bg-border" aria-hidden />
36 {icon}35 {icon}
37 {title}36 {title}
38 </h2>37 </h2>
39 <div className="flex h-5 min-w-5 items-center justify-end">{headerAction}</div>38 <div className="flex min-h-7 min-w-7 items-center justify-end">{headerAction}</div>
40 </div>
41 <div className="relative overflow-visible rounded-2xl border bg-surface-raised text-card-foreground shadow-sm/4 not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:shadow-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]">
42 {children}
43 </div>39 </div>
40 <div className="relative space-y-1 overflow-visible text-foreground">{children}</div>
44 </section>41 </section>
45 );42 );
46}43}
...@@ -65,24 +62,20 @@ export function SettingsRow({...@@ -65,24 +62,20 @@ export function SettingsRow({
65 return (62 return (
66 <div63 <div
67 {...rowProps}64 {...rowProps}
68 className={cn(65 className={cn("rounded-xl px-3 sm:px-4", children ? "pt-3 pb-1" : "py-3", className)}
69 "border-t border-border/60 px-4 first:border-t-0 sm:px-5",
70 children ? "pt-3.5 pb-0" : "py-3.5",
71 className,
72 )}
73 >66 >
74 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">67 <div className="flex flex-col gap-3 sm:grid sm:grid-cols-[minmax(0,1fr)_minmax(10rem,auto)] sm:items-center sm:gap-8">
75 <div className="min-w-0 flex-1 space-y-1">68 <div className="min-w-0 flex-1 space-y-1">
76 <div className="flex min-h-5 items-center gap-1.5">69 <div className="flex min-h-5 items-center gap-1.5">
77 <h3 className="text-[13px] font-semibold tracking-[-0.01em] text-foreground">70 <h3 className="text-sm font-medium tracking-[-0.005em] text-foreground">{title}</h3>
78 {title}
79 </h3>
80 <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">71 <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">
81 {resetAction}72 {resetAction}
82 </span>73 </span>
83 </div>74 </div>
84 <p className="text-xs text-muted-foreground/80">{description}</p>75 <p className="max-w-xl text-[13px] leading-[1.45] text-muted-foreground/80">
85 {status ? <div className="pt-0.5 text-[11px] text-muted-foreground">{status}</div> : null}76 {description}
77 </p>
78 {status ? <div className="pt-0.5 text-xs text-muted-foreground">{status}</div> : null}
86 </div>79 </div>
87 {control ? (80 {control ? (
88 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">81 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
...@@ -127,8 +120,8 @@ export function SettingsPageContainer({...@@ -127,8 +120,8 @@ export function SettingsPageContainer({
127 className?: string;120 className?: string;
128}) {121}) {
129 return (122 return (
130 <div className="scrollbar-gutter-both flex-1 overflow-y-auto p-6 sm:p-8">123 <div className="scrollbar-gutter-both flex-1 overflow-y-auto px-4 py-7 sm:px-8 sm:py-10">
131 <div className={cn("mx-auto flex w-full max-w-3xl flex-col gap-8", className)}>124 <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}>
132 {children}125 {children}
133 </div>126 </div>
134 </div>127 </div>
apps/web/src/components/sidebar/SidebarChrome.tsx+2-2
...@@ -115,10 +115,10 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {...@@ -115,10 +115,10 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
115 <SidebarMenuItem>115 <SidebarMenuItem>
116 <SidebarMenuButton116 <SidebarMenuButton
117 size="sm"117 size="sm"
118 className="gap-2 px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground"118 className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
119 onClick={handleSettingsClick}119 onClick={handleSettingsClick}
120 >120 >
121 <SettingsIcon className="size-4 shrink-0" />121 <SettingsIcon className="size-4.5 shrink-0" />
122 <span>Settings</span>122 <span>Settings</span>
123 </SidebarMenuButton>123 </SidebarMenuButton>
124 </SidebarMenuItem>124 </SidebarMenuItem>
apps/web/src/components/ui/button.tsx+1-1
...@@ -40,7 +40,7 @@ const buttonVariants = cva(...@@ -40,7 +40,7 @@ const buttonVariants = cva(
40 "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground",40 "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground",
41 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",41 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
42 outline:42 outline:
43 "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64 [&_svg:not([class*='text-'])]:text-muted-foreground",43 "border-transparent bg-popover not-dark:bg-clip-padding text-foreground shadow-none dark:bg-input/32 [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64 [&_svg:not([class*='text-'])]:text-muted-foreground",
44 secondary:44 secondary:
45 "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",45 "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",
46 },46 },
apps/web/src/components/ui/combobox.tsx+5-9
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1"use client";1"use client";
22
3import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";3import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
4import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";4import { ChevronsUpDownIcon, XIcon } from "lucide-react";
5import * as React from "react";5import * as React from "react";
66
7import { cn } from "~/lib/utils";7import { cn } from "~/lib/utils";
...@@ -170,7 +170,7 @@ function ComboboxPopup({...@@ -170,7 +170,7 @@ function ComboboxPopup({
170 >170 >
171 <span171 <span
172 className={cn(172 className={cn(
173 "relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",173 "dropdown-glass relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg transition-[scale,opacity]",
174 className,174 className,
175 )}175 )}
176 >176 >
...@@ -191,7 +191,7 @@ function ComboboxItem({...@@ -191,7 +191,7 @@ function ComboboxItem({
191 className,191 className,
192 contentClassName,192 contentClassName,
193 children,193 children,
194 hideIndicator = false,194 hideIndicator: _hideIndicator = false,
195 ...props195 ...props
196}: ComboboxPrimitive.Item.Props & {196}: ComboboxPrimitive.Item.Props & {
197 contentClassName?: string;197 contentClassName?: string;
...@@ -200,19 +200,15 @@ function ComboboxItem({...@@ -200,19 +200,15 @@ function ComboboxItem({
200 return (200 return (
201 <ComboboxPrimitive.Item201 <ComboboxPrimitive.Item
202 className={cn(202 className={cn(
203 "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none hover:bg-accent data-disabled:pointer-events-none data-selected:bg-accent/50 data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",203 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center rounded-sm px-2 py-1 text-base outline-none hover:bg-accent data-disabled:pointer-events-none data-selected:bg-foreground/[0.08] data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
204 className,204 className,
205 )}205 )}
206 data-slot="combobox-item"206 data-slot="combobox-item"
207 {...props}207 {...props}
208 >208 >
209 <ComboboxPrimitive.ItemIndicator className={cn("col-start-1", hideIndicator && "hidden")}>
210 <CheckIcon />
211 </ComboboxPrimitive.ItemIndicator>
212 <div209 <div
213 className={cn(210 className={cn(
214 "[&_svg:not([class*='text-'])]:text-muted-foreground",211 "min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground",
215 hideIndicator ? "col-start-1 col-span-full" : "col-start-2",
216 contentClassName,212 contentClassName,
217 )}213 )}
218 data-slot="combobox-item-content"214 data-slot="combobox-item-content"
apps/web/src/components/ui/command.tsx+12-5
...@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B...@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B
30 return (30 return (
31 <CommandDialogPrimitive.Backdrop31 <CommandDialogPrimitive.Backdrop
32 className={cn(32 className={cn(
33 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",33 "fixed inset-0 z-50 bg-black/40 transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
34 className,34 className,
35 )}35 )}
36 data-slot="command-dialog-backdrop"36 data-slot="command-dialog-backdrop"
...@@ -66,7 +66,7 @@ function CommandDialogPopup({...@@ -66,7 +66,7 @@ function CommandDialogPopup({
66 <CommandDialogViewport>66 <CommandDialogViewport>
67 <CommandDialogPrimitive.Popup67 <CommandDialogPrimitive.Popup
68 className={cn(68 className={cn(
69 "pointer-events-auto -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-105 min-h-0 w-full min-w-0 max-w-xl scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:bg-muted/72 before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1 dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5 dark:before:shadow-none",69 "chat-composer-glass command-palette-glass pointer-events-auto -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-105 min-h-0 w-full min-w-0 max-w-xl scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl text-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1",
70 className,70 className,
71 )}71 )}
72 data-slot="command-dialog-popup"72 data-slot="command-dialog-popup"
...@@ -144,7 +144,7 @@ function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {...@@ -144,7 +144,7 @@ function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
144 return (144 return (
145 <div145 <div
146 className={cn(146 className={cn(
147 "-mx-px not-has-[+[data-slot=command-footer]]:-mb-px relative min-h-0 overflow-hidden rounded-t-xl not-has-[+[data-slot=command-footer]]:rounded-b-2xl border border-b-0 bg-popover bg-clip-padding shadow-xs/5 [clip-path:inset(0_1px)] not-has-[+[data-slot=command-footer]]:[clip-path:inset(0_1px_1px_1px_round_0_0_calc(var(--radius-2xl)-1px)_calc(var(--radius-2xl)-1px))] before:pointer-events-none before:absolute before:inset-0 before:rounded-t-[calc(var(--radius-xl)-1px)] **:data-[slot=scroll-area-scrollbar]:mt-2 [touch-action:pan-y] dark:border-white/5 dark:shadow-none",147 "relative min-h-0 overflow-hidden rounded-t-xl not-has-[+[data-slot=command-footer]]:rounded-b-2xl bg-transparent **:data-[slot=scroll-area-scrollbar]:mt-2 [touch-action:pan-y]",
148 className,148 className,
149 )}149 )}
150 {...props}150 {...props}
...@@ -171,7 +171,14 @@ function CommandCollection({ ...props }: React.ComponentProps<typeof Autocomplet...@@ -171,7 +171,14 @@ function CommandCollection({ ...props }: React.ComponentProps<typeof Autocomplet
171171
172function CommandItem({ className, ...props }: React.ComponentProps<typeof AutocompleteItem>) {172function CommandItem({ className, ...props }: React.ComponentProps<typeof AutocompleteItem>) {
173 return (173 return (
174 <AutocompleteItem className={cn("py-1.5", className)} data-slot="command-item" {...props} />174 <AutocompleteItem
175 className={cn(
176 "py-1.5 data-selected:bg-foreground/[0.06] data-highlighted:bg-foreground/[0.09] data-highlighted:text-foreground [&[data-highlighted][data-selected]]:bg-foreground/[0.09] [&[data-highlighted][data-selected]]:text-foreground",
177 className,
178 )}
179 data-slot="command-item"
180 {...props}
181 />
175 );182 );
176}183}
177184
...@@ -205,7 +212,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {...@@ -205,7 +212,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
205 return (212 return (
206 <div213 <div
207 className={cn(214 className={cn(
208 "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] border-t border-zinc-200/80 px-5 py-3 font-medium text-sm text-zinc-700 dark:border-white/8 dark:text-zinc-300 [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-zinc-100 [&_[data-slot=kbd]]:text-zinc-900 [&_[data-slot=kbd]]:ring-1 [&_[data-slot=kbd]]:ring-zinc-200/80 [&_[data-slot=kbd]]:dark:bg-zinc-800 [&_[data-slot=kbd]]:dark:text-zinc-100 [&_[data-slot=kbd]]:dark:ring-zinc-700",215 "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-5 py-3 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0",
209 className,216 className,
210 )}217 )}
211 data-slot="command-footer"218 data-slot="command-footer"
apps/web/src/components/ui/menu.tsx+4-22
...@@ -49,7 +49,7 @@ function MenuPopup({...@@ -49,7 +49,7 @@ function MenuPopup({
49 >49 >
50 <MenuPrimitive.Popup50 <MenuPrimitive.Popup
51 className={cn(51 className={cn(
52 "relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 outline-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",52 "dropdown-glass relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg outline-none focus:outline-none",
53 className,53 className,
54 )}54 )}
55 data-slot="menu-popup"55 data-slot="menu-popup"
...@@ -150,7 +150,7 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {...@@ -150,7 +150,7 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
150function MenuRadioItem({150function MenuRadioItem({
151 className,151 className,
152 children,152 children,
153 hideIndicator = false,153 hideIndicator: _hideIndicator = false,
154 ...props154 ...props
155}: MenuPrimitive.RadioItem.Props & {155}: MenuPrimitive.RadioItem.Props & {
156 hideIndicator?: boolean;156 hideIndicator?: boolean;
...@@ -158,31 +158,13 @@ function MenuRadioItem({...@@ -158,31 +158,13 @@ function MenuRadioItem({
158 return (158 return (
159 <MenuPrimitive.RadioItem159 <MenuPrimitive.RadioItem
160 className={cn(160 className={cn(
161 "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",161 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center rounded-sm px-2 py-1 text-base text-foreground outline-none data-checked:bg-foreground/[0.08] data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
162 hideIndicator ? "grid-cols-[1fr] ps-3 pe-3" : "grid-cols-[1rem_1fr] ps-2 pe-4",
163 className,162 className,
164 )}163 )}
165 data-slot="menu-radio-item"164 data-slot="menu-radio-item"
166 {...props}165 {...props}
167 >166 >
168 {hideIndicator ? null : (167 <span className="min-w-0 flex-1">{children}</span>
169 <MenuPrimitive.RadioItemIndicator className="col-start-1">
170 <svg
171 fill="none"
172 height="24"
173 stroke="currentColor"
174 strokeLinecap="round"
175 strokeLinejoin="round"
176 strokeWidth="2"
177 viewBox="0 0 24 24"
178 width="24"
179 xmlns="http://www.w3.org/2000/svg"
180 >
181 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
182 </svg>
183 </MenuPrimitive.RadioItemIndicator>
184 )}
185 <span className={hideIndicator ? "col-start-1" : "col-start-2"}>{children}</span>
186 </MenuPrimitive.RadioItem>168 </MenuPrimitive.RadioItem>
187 );169 );
188}170}
apps/web/src/components/ui/select.tsx+4-25
...@@ -150,7 +150,7 @@ function SelectPopup({...@@ -150,7 +150,7 @@ function SelectPopup({
150 </SelectPrimitive.ScrollUpArrow>150 </SelectPrimitive.ScrollUpArrow>
151 <div151 <div
152 className={cn(152 className={cn(
153 "relative h-full rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",153 "dropdown-glass relative h-full rounded-lg",
154 matchTriggerWidth && "min-w-(--anchor-width)",154 matchTriggerWidth && "min-w-(--anchor-width)",
155 popupClassName,155 popupClassName,
156 )}156 )}
...@@ -177,7 +177,7 @@ function SelectPopup({...@@ -177,7 +177,7 @@ function SelectPopup({
177function SelectItem({177function SelectItem({
178 className,178 className,
179 children,179 children,
180 hideIndicator = false,180 hideIndicator: _hideIndicator = false,
181 ...props181 ...props
182}: SelectPrimitive.Item.Props & {182}: SelectPrimitive.Item.Props & {
183 hideIndicator?: boolean;183 hideIndicator?: boolean;
...@@ -185,35 +185,14 @@ function SelectItem({...@@ -185,35 +185,14 @@ function SelectItem({
185 return (185 return (
186 <SelectPrimitive.Item186 <SelectPrimitive.Item
187 className={cn(187 className={cn(
188 "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",188 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center rounded-sm px-2 py-1 text-base outline-none data-selected:bg-foreground/[0.08] data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
189 hideIndicator ? "grid-cols-[1fr] ps-3 pe-3" : "grid-cols-[1rem_1fr] ps-2 pe-4",
190 className,189 className,
191 )}190 )}
192 data-slot="select-item"191 data-slot="select-item"
193 {...props}192 {...props}
194 >193 >
195 {hideIndicator ? null : (
196 <SelectPrimitive.ItemIndicator className="col-start-1" data-slot="select-item-indicator">
197 <svg
198 fill="none"
199 height="24"
200 stroke="currentColor"
201 strokeLinecap="round"
202 strokeLinejoin="round"
203 strokeWidth="2"
204 viewBox="0 0 24 24"
205 width="24"
206 xmlns="http://www.w3.org/1500/svg"
207 >
208 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
209 </svg>
210 </SelectPrimitive.ItemIndicator>
211 )}
212 <SelectPrimitive.ItemText194 <SelectPrimitive.ItemText
213 className={cn(195 className="min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground"
214 "min-w-0 [&_svg:not([class*='text-'])]:text-muted-foreground",
215 hideIndicator ? "col-start-1" : "col-start-2",
216 )}
217 data-slot="select-item-text"196 data-slot="select-item-text"
218 >197 >
219 {children}198 {children}
apps/web/src/index.css+183-75
...@@ -107,7 +107,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -107,7 +107,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
107}107}
108108
109@theme inline {109@theme inline {
110 --color-zinc-25: oklch(99.2% 0 0);
111 --animate-skeleton: skeleton 2s -1s infinite linear;110 --animate-skeleton: skeleton 2s -1s infinite linear;
112 /* Duty-cycled indicator animations: long holds with stepped ramps, so the111 /* Duty-cycled indicator animations: long holds with stepped ramps, so the
113 compositor updates discrete frames instead of every vsync. */112 compositor updates discrete frames instead of every vsync. */
...@@ -303,6 +302,46 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -303,6 +302,46 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
303 align-items: center;302 align-items: center;
304 }303 }
305304
305 /* Fade rows themselves as they pass beneath the top chrome. A mask remains
306 visible even when the header and timeline share the same background. */
307 .chat-timeline-scroll-fade {
308 --timeline-top-fade-height: 2.5rem;
309 -webkit-mask-image:
310 linear-gradient(
311 to bottom,
312 transparent 0%,
313 rgb(0 0 0 / 10%) 10%,
314 rgb(0 0 0 / 30%) 24%,
315 rgb(0 0 0 / 58%) 42%,
316 rgb(0 0 0 / 82%) 62%,
317 rgb(0 0 0 / 96%) 82%,
318 black 100%
319 ),
320 linear-gradient(black, black);
321 -webkit-mask-position: top, bottom;
322 -webkit-mask-repeat: no-repeat;
323 -webkit-mask-size:
324 100% var(--timeline-top-fade-height),
325 100% calc(100% - var(--timeline-top-fade-height));
326 mask-image:
327 linear-gradient(
328 to bottom,
329 transparent 0%,
330 rgb(0 0 0 / 10%) 10%,
331 rgb(0 0 0 / 30%) 24%,
332 rgb(0 0 0 / 58%) 42%,
333 rgb(0 0 0 / 82%) 62%,
334 rgb(0 0 0 / 96%) 82%,
335 black 100%
336 ),
337 linear-gradient(black, black);
338 mask-position: top, bottom;
339 mask-repeat: no-repeat;
340 mask-size:
341 100% var(--timeline-top-fade-height),
342 100% calc(100% - var(--timeline-top-fade-height));
343 }
344
306 .workspace-titlebar-controls {345 .workspace-titlebar-controls {
307 position: absolute;346 position: absolute;
308 top: var(--workspace-controls-top);347 top: var(--workspace-controls-top);
...@@ -323,50 +362,75 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -323,50 +362,75 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
323 }362 }
324363
325 .chat-composer-glass {364 .chat-composer-glass {
326 background: var(--surface-raised);365 background: var(--secondary);
366 -webkit-backdrop-filter: blur(16px);
367 backdrop-filter: blur(16px);
327 }368 }
328369
329 /* Mix from --background (not --card): the lower chrome is a full-width370 .chat-composer-context-strip {
330 strip over the main view, and a card-tinted wash reads as a lighter371 position: relative;
331 seam against it when no content is scrolled underneath. The backdrop372 isolation: isolate;
332 blur, not the tint, carries the frosted effect. */
333 .chat-composer-lower-chrome {
334 background: color-mix(in srgb, var(--background) 20%, transparent);
335 }373 }
336374
337 /* The translucent lower chrome lets the composer's shadow taper naturally375 .chat-composer-context-strip::before {
338 into the message canvas. Above the terminal drawer that same shadow reads376 position: absolute;
339 as a dirty light-to-black gradient, so end the workspace on an opaque377 z-index: -1;
340 surface and let the drawer's border define the boundary instead. */378 inset: 0;
341 .chat-composer-lower-chrome[data-terminal-open="true"] {379 border-radius: 0 0 16px 16px;
342 background: var(--background);380 background: color-mix(in srgb, var(--card) 20%, transparent);
381 box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%);
382 content: "";
383 -webkit-backdrop-filter: blur(16px);
384 backdrop-filter: blur(16px);
343 }385 }
344386
345 .chat-composer-glass {387 .dropdown-glass {
346 box-shadow:388 background: var(--secondary);
347 0 18px 48px -20px rgb(0 0 0 / 28%),389 -webkit-backdrop-filter: blur(16px);
348 0 4px 14px -7px rgb(0 0 0 / 22%);390 backdrop-filter: blur(16px);
391 box-shadow: 0 16px 40px -18px rgb(0 0 0 / 55%);
392 }
393
394 .command-palette-glass {
395 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
349 }396 }
350397
351 .chat-composer-shared-blur {398 .command-menu-surface {
399 background: var(--popover);
400 -webkit-backdrop-filter: none;
401 backdrop-filter: none;
402 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
403 }
404
405 .workflow-dialog-surface {
406 border-color: transparent;
407 background: color-mix(in srgb, var(--popover) 82%, transparent);
352 -webkit-backdrop-filter: blur(16px);408 -webkit-backdrop-filter: blur(16px);
353 backdrop-filter: blur(16px);409 backdrop-filter: blur(16px);
410 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
354 }411 }
355412
356 .dark .chat-composer-glass {413 .dark .dropdown-glass {
357 background: var(--surface-raised);414 box-shadow: 0 18px 44px -18px rgb(0 0 0 / 80%);
358 }415 }
359416
360 .dark .chat-composer-lower-chrome {417 .dark .model-picker-surface.model-picker-surface {
361 background: color-mix(in srgb, var(--background) 45%, transparent);418 background: color-mix(in srgb, var(--background) 90%, transparent);
362 }419 }
363420
364 .dark .chat-composer-lower-chrome[data-terminal-open="true"] {421 .dark .command-menu-surface,
365 background: var(--background);422 .dark .command-palette-glass,
423 .dark .workflow-dialog-surface {
424 box-shadow: none;
366 }425 }
367426
368 .dark .chat-composer-glass {427 .dark .workflow-dialog-surface {
369 box-shadow: none;428 background: color-mix(in srgb, var(--popover) 76%, transparent);
429 }
430
431 .dark .chat-composer-context-strip::before {
432 background: color-mix(in srgb, var(--card) 45%, transparent);
433 box-shadow: 0 14px 32px -18px rgb(0 0 0 / 75%);
370 }434 }
371435
372 .chat-composer-lower-chrome {436 .chat-composer-lower-chrome {
...@@ -376,6 +440,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -376,6 +440,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
376 }440 }
377441
378 @media (min-width: 40rem) {442 @media (min-width: 40rem) {
443 .chat-timeline-scroll-fade {
444 --timeline-top-fade-height: 3rem;
445 }
446
379 .chat-composer-horizontal-inset {447 .chat-composer-horizontal-inset {
380 padding-inline-start: calc(env(safe-area-inset-left) + 1.25rem);448 padding-inline-start: calc(env(safe-area-inset-left) + 1.25rem);
381 padding-inline-end: calc(env(safe-area-inset-right) + 1.25rem);449 padding-inline-end: calc(env(safe-area-inset-right) + 1.25rem);
...@@ -384,11 +452,19 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -384,11 +452,19 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
384452
385 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {453 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
386 .chat-composer-glass {454 .chat-composer-glass {
455 background: var(--secondary);
456 }
457
458 .chat-composer-context-strip::before {
387 background: var(--card);459 background: var(--card);
388 }460 }
389461
390 .chat-composer-lower-chrome {462 .dropdown-glass {
391 background: var(--background);463 background: color-mix(in srgb, var(--popover) 88%, black);
464 }
465
466 .workflow-dialog-surface {
467 background: var(--popover);
392 }468 }
393 }469 }
394}470}
...@@ -430,25 +506,25 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -430,25 +506,25 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
430:root {506:root {
431 color-scheme: light;507 color-scheme: light;
432 --radius: 0.625rem;508 --radius: 0.625rem;
433 --background: var(--color-zinc-25);509 --background: var(--color-white);
434 --app-chrome-background: var(--background);510 --app-chrome-background: var(--background);
435 --surface-raised: color-mix(in srgb, var(--card) 20%, transparent);511 --surface-raised: var(--secondary);
436 --foreground: var(--color-zinc-800);512 --foreground: var(--color-neutral-800);
437 --card: var(--color-white);513 --card: var(--color-white);
438 --card-foreground: var(--color-zinc-800);514 --card-foreground: var(--color-neutral-800);
439 --popover: var(--color-white);515 --popover: var(--color-white);
440 --popover-foreground: var(--color-zinc-800);516 --popover-foreground: var(--color-neutral-800);
441 --primary: oklch(0.488 0.217 264);517 --primary: oklch(0.488 0.217 264);
442 --primary-foreground: var(--color-white);518 --primary-foreground: var(--color-white);
443 --secondary: var(--color-zinc-50);519 --secondary: --alpha(var(--color-black) / 4%);
444 --secondary-foreground: var(--color-zinc-800);520 --secondary-foreground: var(--color-neutral-800);
445 --muted: var(--color-zinc-50);521 --muted: --alpha(var(--color-black) / 4%);
446 --muted-foreground: var(--color-zinc-500);522 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));
447 --accent: var(--color-zinc-100);523 --accent: --alpha(var(--color-black) / 4%);
448 --accent-foreground: var(--color-zinc-900);524 --accent-foreground: var(--color-neutral-800);
449 --destructive: var(--color-red-500);525 --destructive: var(--color-red-500);
450 --border: var(--color-zinc-200);526 --border: --alpha(var(--color-black) / 8%);
451 --input: var(--color-zinc-300);527 --input: --alpha(var(--color-black) / 10%);
452 --ring: oklch(0.488 0.217 264);528 --ring: oklch(0.488 0.217 264);
453 --destructive-foreground: var(--color-red-700);529 --destructive-foreground: var(--color-red-700);
454 --info: var(--color-blue-500);530 --info: var(--color-blue-500);
...@@ -470,22 +546,24 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -470,22 +546,24 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
470546
471 @variant dark {547 @variant dark {
472 color-scheme: dark;548 color-scheme: dark;
473 --background: var(--color-zinc-950);549 /* Keep the workspace in the same neutral-black family as sidebar v2.
550 Surfaces lift from this base instead of starting from a milky gray. */
551 --background: var(--color-neutral-950);
474 --app-chrome-background: var(--background);552 --app-chrome-background: var(--background);
475 --surface-raised: color-mix(in srgb, var(--card) 45%, transparent);553 --surface-raised: var(--secondary);
476 --foreground: var(--color-zinc-100);554 --foreground: var(--color-neutral-100);
477 --card: var(--color-zinc-900);555 --card: color-mix(in srgb, var(--background) 97%, var(--color-white));
478 --card-foreground: var(--color-zinc-100);556 --card-foreground: var(--color-neutral-100);
479 --popover: var(--color-zinc-900);557 --popover: color-mix(in srgb, var(--background) 94%, var(--color-white));
480 --popover-foreground: var(--color-zinc-100);558 --popover-foreground: var(--color-neutral-100);
481 --primary: oklch(0.588 0.217 264);559 --primary: oklch(0.588 0.217 264);
482 --primary-foreground: var(--color-white);560 --primary-foreground: var(--color-white);
483 --secondary: var(--color-zinc-900);561 --secondary: --alpha(var(--color-white) / 4%);
484 --secondary-foreground: var(--color-zinc-100);562 --secondary-foreground: var(--color-neutral-100);
485 --muted: var(--color-zinc-900);563 --muted: --alpha(var(--color-white) / 4%);
486 --muted-foreground: var(--color-zinc-400);564 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));
487 --accent: var(--color-zinc-900);565 --accent: --alpha(var(--color-white) / 4%);
488 --accent-foreground: var(--color-zinc-100);566 --accent-foreground: var(--color-neutral-100);
489 --destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));567 --destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
490 --border: --alpha(var(--color-white) / 6%);568 --border: --alpha(var(--color-white) / 6%);
491 --input: --alpha(var(--color-white) / 8%);569 --input: --alpha(var(--color-white) / 8%);
...@@ -500,27 +578,57 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -500,27 +578,57 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
500 }578 }
501}579}
502580
503[data-app-sidebar] {581/* Give both navigation implementations the same cool graphite surface. The
504 --sidebar: var(--color-zinc-50);582 version attribute remains useful for layout-specific styling, but color is
505 --sidebar-foreground: var(--color-zinc-800);583 shared so switching the beta does not switch visual systems. */
506 --sidebar-muted-foreground: var(--color-zinc-500);584[data-sidebar-version="v1"],
507 --sidebar-control-surface: var(--color-zinc-100);585[data-sidebar-version="v2"] {
508 --sidebar-row-hover: var(--color-zinc-25);586 --background: oklch(0.965 0.009 265);
509 --sidebar-row-active: var(--color-white);587 --foreground: oklch(0.27 0.025 265);
510 --sidebar-row-selected: var(--color-white);588 --card: oklch(0.945 0.014 265);
511 --sidebar-border: var(--color-zinc-200);589 --card-foreground: var(--foreground);
590 --accent: oklch(0.875 0.008 265);
591 --accent-foreground: oklch(0.24 0.012 265);
592 --muted: oklch(0.9 0.018 265);
593 --muted-foreground: oklch(0.49 0.03 265);
594 --border: oklch(0.72 0.025 265 / 28%);
595 --input: oklch(0.62 0.03 265 / 28%);
596 --sidebar: var(--card);
597 --sidebar-foreground: var(--foreground);
598 --sidebar-muted-foreground: var(--muted-foreground);
599 --sidebar-control-surface: var(--muted);
600 --sidebar-row-hover: color-mix(in srgb, var(--foreground) 6%, transparent);
601 --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent);
602 --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent);
603 --sidebar-border: var(--border);
512 --sidebar-stage-fade: var(--sidebar);604 --sidebar-stage-fade: var(--sidebar);
513}605 background-color: var(--card);
514606}
515.dark [data-app-sidebar] {607
516 --sidebar: var(--color-zinc-950);608.dark [data-sidebar-version="v1"],
517 --sidebar-foreground: var(--color-zinc-100);609.dark [data-sidebar-version="v2"] {
518 --sidebar-muted-foreground: var(--color-zinc-400);610 --background: #000;
519 --sidebar-control-surface: var(--color-zinc-800);611 --foreground: #f1f3f7;
520 --sidebar-row-hover: var(--color-zinc-900);612 --card: #000;
521 --sidebar-row-active: var(--color-zinc-900);613 --card-foreground: var(--foreground);
522 --sidebar-row-selected: var(--color-zinc-900);614 --accent: #191a1d;
523 --sidebar-border: var(--color-zinc-800);615 --accent-foreground: #f7f9ff;
616 --muted: #0a0a0a;
617 --muted-foreground: #a3a3a3;
618 --border: rgb(255 255 255 / 8%);
619 --input: rgb(255 255 255 / 18%);
620 --sidebar: var(--card);
621 --sidebar-foreground: var(--foreground);
622 --sidebar-muted-foreground: var(--muted-foreground);
623 --sidebar-control-surface: var(--muted);
624 --sidebar-row-hover: color-mix(in srgb, var(--foreground) 8%, transparent);
625 --sidebar-row-active: color-mix(in srgb, var(--foreground) 11%, transparent);
626 --sidebar-row-selected: color-mix(in srgb, var(--foreground) 7%, transparent);
627 --sidebar-border: var(--border);
628 /* The stage-channel header art must ramp to THIS panel's surface, not the
629 global chrome background, or the fade shows a seam (same rule as the
630 light palette above). */
631 --sidebar-stage-fade: var(--card);
524}632}
525633
526body {634body {
apps/web/src/routes/settings.tsx+3-3
...@@ -22,7 +22,7 @@ function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {...@@ -22,7 +22,7 @@ function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {
22 return (22 return (
23 <Button23 <Button
24 size="xs"24 size="xs"
25 variant="outline"25 variant="ghost"
26 disabled={changedSettingLabels.length === 0}26 disabled={changedSettingLabels.length === 0}
27 onClick={() => void restoreDefaults()}27 onClick={() => void restoreDefaults()}
28 >28 >
...@@ -68,7 +68,7 @@ function SettingsContentLayout() {...@@ -68,7 +68,7 @@ function SettingsContentLayout() {
68 {!isElectron && (68 {!isElectron && (
69 <header69 <header
70 className={cn(70 className={cn(
71 "border-b border-border px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",71 "px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
72 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,72 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
73 )}73 )}
74 >74 >
...@@ -86,7 +86,7 @@ function SettingsContentLayout() {...@@ -86,7 +86,7 @@ function SettingsContentLayout() {
86 {isElectron && (86 {isElectron && (
87 <div87 <div
88 className={cn(88 className={cn(
89 "drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",89 "drag-region flex h-[52px] shrink-0 items-center px-5 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
90 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,90 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
91 )}91 )}
92 >92 >