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 }) {
106106 const pathname = useLocation({ select: (location) => location.pathname });
107107 const isOnSettings = pathname === "/settings" || pathname.startsWith("/settings/");
108108 const useSidebarV2 = sidebarV2Enabled && !isOnSettings;
109 const useSidebarV2Theme = useSidebarV2 || isOnSettings;
109110 const isMacosDesktop = isElectron && isMacPlatform(navigator.platform);
110111 const [sidebarWidth, setSidebarWidth] = useState(readInitialThreadSidebarWidth);
111112 const sidebarMaximumWidth = resolveThreadSidebarMaximumWidth(window.innerWidth);
......@@ -165,7 +166,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
165166 side="left"
166167 collapsible="offcanvas"
167168 data-app-sidebar=""
168 data-sidebar-version={useSidebarV2 ? "v2" : "v1"}
169 data-sidebar-version={useSidebarV2Theme ? "v2" : "v1"}
169170 className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
170171 resizable={{
171172 maxWidth: sidebarMaximumWidth,
apps/web/src/components/BranchToolbar.tsx+1-1
......@@ -248,7 +248,7 @@ export const BranchToolbar = memo(function BranchToolbar({
248248 if (!hasActiveThread || !activeProject) return null;
249249
250250 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">
252252 {isMobile ? (
253253 <MobileRunContextSelector
254254 envLocked={envLocked}
apps/web/src/components/ChatView.tsx+13-21
......@@ -2320,6 +2320,11 @@ function ChatViewContent(props: ChatViewProps) {
23202320 const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider);
23212321 return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null;
23222322 }, [activeProviderInstanceId, providerStatuses, selectedProvider]);
2323 const hasTimelineTopBanner =
2324 Boolean(threadError) ||
2325 (activeProviderStatus !== null &&
2326 activeProviderStatus.status !== "ready" &&
2327 activeProviderStatus.status !== "disabled");
23232328 const activeProjectCwd = activeProject?.workspaceRoot ?? null;
23242329 const activeThreadWorktreePath = activeThread?.worktreePath ?? null;
23252330 const activeWorkspaceRoot = activeThreadWorktreePath ?? activeProjectCwd ?? undefined;
......@@ -5412,7 +5417,7 @@ function ChatViewContent(props: ChatViewProps) {
54125417 <header
54135418 data-chat-header
54145419 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",
54165421 isElectron
54175422 ? cn(
54185423 "workspace-topbar drag-region relative px-3 sm:px-5",
......@@ -5448,8 +5453,6 @@ function ChatViewContent(props: ChatViewProps) {
54485453 />
54495454 </header>
54505455
5451 {/* Error banner */}
5452 <ProviderStatusBanner status={activeProviderStatus} />
54535456 <ThreadErrorBanner
54545457 error={threadError}
54555458 onDismiss={() => setThreadError(activeThread.id, null)}
......@@ -5458,6 +5461,10 @@ function ChatViewContent(props: ChatViewProps) {
54585461 <div className="flex min-h-0 min-w-0 flex-1">
54595462 {/* Chat column */}
54605463 <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>
54615468 {/* Messages Wrapper */}
54625469 <div className="relative flex min-h-0 flex-1 flex-col">
54635470 {/* Messages — LegendList handles virtualization and scrolling internally */}
......@@ -5494,6 +5501,7 @@ function ChatViewContent(props: ChatViewProps) {
54945501 onIsAtEndChange={onIsAtEndChange}
54955502 onManualNavigation={cancelTimelineLiveFollowForUserNavigation}
54965503 hideEmptyPlaceholder={isDraftHeroState}
5504 topFadeEnabled={!hasTimelineTopBanner}
54975505 />
54985506
54995507 {/* scroll to end pill — shown when user has scrolled away from the live edge */}
......@@ -5526,17 +5534,6 @@ function ChatViewContent(props: ChatViewProps) {
55265534 : "pointer-events-none absolute inset-x-0 bottom-0 z-20 pt-1.5 sm:pt-2"
55275535 }
55285536 >
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}
55405537 <div
55415538 ref={attachDraftHeroTransitionGroupRef}
55425539 className="chat-composer-horizontal-inset w-full"
......@@ -5574,7 +5571,7 @@ function ChatViewContent(props: ChatViewProps) {
55745571 >
55755572 <div
55765573 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"
55785575 >
55795576 <ChatComposer
55805577 composerRef={composerRef}
......@@ -5658,12 +5655,7 @@ function ChatViewContent(props: ChatViewProps) {
56585655 >
56595656 <div
56605657 data-terminal-open={terminalUiState.terminalOpen ? "true" : undefined}
5661 className={cn(
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 )}
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)]"
56675659 >
56685660 {isGitRepo && (
56695661 <div className="pointer-events-auto">
apps/web/src/components/CommandPalette.tsx+1-1
......@@ -1948,7 +1948,7 @@ function OpenCommandPaletteDialog(props: {
19481948 <Button
19491949 variant="ghost"
19501950 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"
19521952 disabled={isPickingProjectFolder}
19531953 onClick={() => {
19541954 void handleOpenProjectFromFileManager();
apps/web/src/components/DiffPanel.tsx+24-12
......@@ -521,27 +521,37 @@ export default function DiffPanel({
521521 <ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />
522522 </DropdownMenuTrigger>
523523 <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 >
525532 <span>Working tree</span>
526 {selectedTurnId === null && selectedGitScope === "unstaged" && (
527 <CheckIcon className="ml-auto" />
528 )}
529533 </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 >
531542 <span>Branch changes</span>
532 {selectedTurnId === null && selectedGitScope === "branch" && (
533 <CheckIcon className="ml-auto" />
534 )}
535543 </DropdownMenuItem>
536544 <DropdownMenuItem
545 className={
546 selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId
547 ? "bg-foreground/[0.08]"
548 : undefined
549 }
537550 onClick={() => {
538551 if (latestTurn) selectTurn(latestTurn.turnId);
539552 }}
540553 >
541554 <span>Latest turn</span>
542 {selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId && (
543 <CheckIcon className="ml-auto" />
544 )}
545555 </DropdownMenuItem>
546556 <DropdownMenuSub>
547557 <DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger>
......@@ -554,13 +564,15 @@ export default function DiffPanel({
554564 return (
555565 <DropdownMenuItem
556566 key={summary.turnId}
567 className={
568 summary.turnId === selectedTurn?.turnId ? "bg-foreground/[0.08]" : undefined
569 }
557570 onClick={() => selectTurn(summary.turnId)}
558571 >
559572 <span>Turn {turnCount}</span>
560573 <span className="ml-auto text-xs tabular-nums text-muted-foreground">
561574 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
562575 </span>
563 {summary.turnId === selectedTurn?.turnId && <CheckIcon className="ml-1" />}
564576 </DropdownMenuItem>
565577 );
566578 })}
apps/web/src/components/GitActionsControl.tsx+9-9
......@@ -544,9 +544,9 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
544544
545545 return (
546546 <Dialog open={props.open} onOpenChange={handleOpenChange}>
547 <DialogPopup className="max-w-xl overflow-hidden">
548 <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl">
549 <DialogHeader className="border-b border-border/70 bg-background">
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-transparent">
549 <DialogHeader className="border-b border-border/70 bg-foreground/[0.025]">
550550 <DialogTitle>Publish repository</DialogTitle>
551551 <DialogDescription>
552552 Pick where to host it, then point us at a repo to push to.
......@@ -913,7 +913,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
913913 </AnimatedHeight>
914914 </DialogPanel>
915915
916 <DialogFooter>
916 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025]">
917917 {publishWizardStep === 2 ? (
918918 <Button size="sm" onClick={() => handleOpenChange(false)}>
919919 Done
......@@ -1736,7 +1736,7 @@ export default function GitActionsControl({
17361736 >
17371737 <ChevronDownIcon aria-hidden="true" className="size-4" />
17381738 </MenuTrigger>
1739 <MenuPopup align="end" className="w-full">
1739 <MenuPopup align="end" className="command-menu-surface w-full">
17401740 {gitActionMenuItems.map((item) => {
17411741 const disabledReason = getMenuActionDisabledReason({
17421742 item,
......@@ -1825,7 +1825,7 @@ export default function GitActionsControl({
18251825 }
18261826 }}
18271827 >
1828 <DialogPopup>
1828 <DialogPopup className="workflow-dialog-surface before:hidden">
18291829 <DialogHeader>
18301830 <DialogTitle>{COMMIT_DIALOG_TITLE}</DialogTitle>
18311831 <DialogDescription>{COMMIT_DIALOG_DESCRIPTION}</DialogDescription>
......@@ -1955,7 +1955,7 @@ export default function GitActionsControl({
19551955 />
19561956 </div>
19571957 </DialogPanel>
1958 <DialogFooter>
1958 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025]">
19591959 <Button
19601960 variant="outline"
19611961 size="sm"
......@@ -1998,14 +1998,14 @@ export default function GitActionsControl({
19981998 }
19991999 }}
20002000 >
2001 <DialogPopup className="max-w-xl">
2001 <DialogPopup className="workflow-dialog-surface max-w-xl before:hidden">
20022002 <DialogHeader>
20032003 <DialogTitle>
20042004 {pendingDefaultBranchActionCopy?.title ?? "Run action on default refName?"}
20052005 </DialogTitle>
20062006 <DialogDescription>{pendingDefaultBranchActionCopy?.description}</DialogDescription>
20072007 </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">
20092009 <Button
20102010 className="w-full sm:mr-auto sm:w-auto"
20112011 variant="outline"
apps/web/src/components/ProjectScriptsControl.tsx+4-4
......@@ -276,7 +276,7 @@ export default function ProjectScriptsControl({
276276 >
277277 <ChevronDownIcon className="size-4" />
278278 </MenuTrigger>
279 <MenuPopup align="end">
279 <MenuPopup align="end" className="command-menu-surface">
280280 {scripts.map((script) => {
281281 const shortcutLabel = shortcutLabelForCommand(
282282 keybindings,
......@@ -364,7 +364,7 @@ export default function ProjectScriptsControl({
364364 }}
365365 open={dialogOpen}
366366 >
367 <DialogPopup>
367 <DialogPopup className="workflow-dialog-surface before:hidden">
368368 <DialogHeader>
369369 <DialogTitle>{isEditing ? "Edit Action" : "Add Action"}</DialogTitle>
370370 <DialogDescription>
......@@ -389,7 +389,7 @@ export default function ProjectScriptsControl({
389389 >
390390 <ScriptIcon icon={icon} className="size-4.5" />
391391 </PopoverTrigger>
392 <PopoverPopup align="start">
392 <PopoverPopup align="start" className="command-menu-surface">
393393 <div className="grid grid-cols-3 gap-2">
394394 {SCRIPT_ICONS.map((entry) => {
395395 const isSelected = entry.id === icon;
......@@ -480,7 +480,7 @@ export default function ProjectScriptsControl({
480480 {validationError && <p className="text-sm text-destructive">{validationError}</p>}
481481 </form>
482482 </DialogPanel>
483 <DialogFooter>
483 <DialogFooter className="dark:border-white/5 dark:bg-white/[0.025]">
484484 {isEditing && (
485485 <Button
486486 type="button"
apps/web/src/components/Sidebar.logic.test.ts+10-12
......@@ -834,26 +834,24 @@ describe("resolveThreadStatusPill", () => {
834834});
835835
836836describe("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", () => {
838838 const className = resolveThreadRowClassName({ isActive: true, isSelected: true });
839 expect(className).toContain("bg-primary/22");
840 expect(className).toContain("hover:bg-primary/26");
841 expect(className).toContain("dark:bg-primary/30");
842 expect(className).not.toContain("bg-accent/85");
839 expect(className).toContain("bg-sidebar-row-active");
840 expect(className).toContain("text-sidebar-foreground");
841 expect(className).not.toContain("bg-primary");
843842 });
844843
845844 it("uses selected hover colors for selected threads", () => {
846845 const className = resolveThreadRowClassName({ isActive: false, isSelected: true });
847 expect(className).toContain("bg-primary/15");
848 expect(className).toContain("hover:bg-primary/19");
849 expect(className).toContain("dark:bg-primary/22");
850 expect(className).not.toContain("hover:bg-accent");
846 expect(className).toContain("bg-sidebar-row-selected");
847 expect(className).toContain("hover:bg-sidebar-row-active");
848 expect(className).not.toContain("bg-primary");
851849 });
852850
853 it("keeps the accent palette for active-only threads", () => {
851 it("uses the active sidebar surface for active-only threads", () => {
854852 const className = resolveThreadRowClassName({ isActive: true, isSelected: false });
855 expect(className).toContain("bg-accent/85");
856 expect(className).toContain("hover:bg-accent");
853 expect(className).toContain("bg-sidebar-row-active");
854 expect(className).toContain("hover:bg-sidebar-row-active");
857855 });
858856});
859857
apps/web/src/components/Sidebar.logic.ts+8-5
......@@ -391,30 +391,33 @@ export function resolveThreadRowClassName(input: {
391391 isSelected: boolean;
392392}): string {
393393 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
396396 if (input.isSelected && input.isActive) {
397397 return cn(
398398 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",
400400 );
401401 }
402402
403403 if (input.isSelected) {
404404 return cn(
405405 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",
407407 );
408408 }
409409
410410 if (input.isActive) {
411411 return cn(
412412 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",
414414 );
415415 }
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 );
418421}
419422
420423// ── Sidebar v2 status model ─────────────────────────────────────────
apps/web/src/components/Sidebar.tsx+12-14
......@@ -714,7 +714,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
714714 {renamingThreadKey === threadKey ? (
715715 <input
716716 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"
718718 value={renamingTitle}
719719 onChange={handleRenameInputChange}
720720 onKeyDown={handleRenameInputKeyDown}
......@@ -727,7 +727,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
727727 <TooltipTrigger
728728 render={
729729 <span
730 className="min-w-0 flex-1 truncate text-xs"
730 className="min-w-0 flex-1 truncate text-sm"
731731 data-testid={`thread-title-${thread.id}`}
732732 >
733733 {thread.title}
......@@ -982,13 +982,13 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
982982 return (
983983 <SidebarMenuSub
984984 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"
986986 >
987987 {shouldShowThreadPanel && showEmptyThreadState ? (
988988 <SidebarMenuSubItem className="w-full" data-thread-selection-safe>
989989 <div
990990 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"
992992 >
993993 <span>No threads yet</span>
994994 </div>
......@@ -1034,7 +1034,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
10341034 render={showMoreButtonRender}
10351035 data-thread-selection-safe
10361036 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"
10381038 onClick={() => {
10391039 expandThreadListForProject(projectKey);
10401040 }}
......@@ -1052,7 +1052,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
10521052 render={showLessButtonRender}
10531053 data-thread-selection-safe
10541054 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"
10561056 onClick={() => {
10571057 collapseThreadListForProject(projectKey);
10581058 }}
......@@ -2252,7 +2252,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
22522252 <SidebarMenuButton
22532253 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
22542254 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 ${
22562256 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"
22572257 }`}
22582258 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
......@@ -2292,7 +2292,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
22922292 )}
22932293 <ProjectFavicon environmentId={project.environmentId} cwd={project.workspaceRoot} />
22942294 <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">
22962296 {project.displayName}
22972297 </span>
22982298 {project.groupedProjectCount > 1 ? (
......@@ -2899,13 +2899,13 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
28992899 render={
29002900 <SidebarMenuButton
29012901 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"
29032903 data-testid="command-palette-trigger"
29042904 />
29052905 }
29062906 >
2907 <SearchIcon className="size-3.5 text-muted-foreground/70" />
2908 <span className="flex-1 truncate text-left text-xs">Search</span>
2907 <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />
2908 <span className="flex-1 truncate text-left text-sm font-medium">Search</span>
29092909 {commandPaletteShortcutLabel ? (
29102910 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">
29112911 {commandPaletteShortcutLabel}
......@@ -2941,9 +2941,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
29412941 <LocalSecondaryStatus />
29422942 <SidebarGroup className="px-2 py-2">
29432943 <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">
2945 Projects
2946 </span>
2944 <span className="text-xs font-medium text-sidebar-muted-foreground/80">Projects</span>
29472945 <div className="flex items-center gap-1">
29482946 <ProjectSortMenu
29492947 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 @@
11import { useAtomValue } from "@effect/atom-react";
2import { useId } from "react";
23
34import { APP_STAGE_LABEL } from "../branding";
45import { resolveServerBackedAppStageLabel } from "../branding.logic";
......@@ -47,6 +48,10 @@ export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVar
4748 return variant === "nightly" ? <NightlySkyArt /> : <DevBlueprintArt />;
4849}
4950
51export function StageBackdropButtonArt({ variant }: { variant: SidebarStageBackdropVariant }) {
52 return variant === "nightly" ? <NightlySkyArt compact /> : <DevBlueprintArt compact />;
53}
54
5055const NIGHTLY_STARS: ReadonlyArray<{
5156 cx: number;
5257 cy: number;
......@@ -78,18 +83,26 @@ const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [
7883 { x: 246, y: 26 },
7984];
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
8295 return (
8396 <svg
8497 className="h-full w-full"
8598 fill="none"
8699 preserveAspectRatio="xMinYMin slice"
87 viewBox={STAGE_BACKDROP_VIEW_BOX}
100 viewBox={compact ? "96 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
88101 xmlns="http://www.w3.org/2000/svg"
89102 >
90103 <defs>
91104 <linearGradient
92 id="stage-night-sky"
105 id={skyId}
93106 x1="24"
94107 y1="0"
95108 x2="264"
......@@ -102,7 +115,7 @@ function NightlySkyArt() {
102115 <stop offset="1" stopColor="#32155B" />
103116 </linearGradient>
104117 <radialGradient
105 id="stage-night-glow"
118 id={glowId}
106119 cx="0"
107120 cy="0"
108121 r="1"
......@@ -113,29 +126,15 @@ function NightlySkyArt() {
113126 <stop offset="0.5" stopColor="#283075" stopOpacity="0.16" />
114127 <stop offset="1" stopColor="#111635" stopOpacity="0" />
115128 </radialGradient>
116 <linearGradient
117 id="stage-night-cloud"
118 x1="0"
119 y1="60"
120 x2="288"
121 y2="96"
122 gradientUnits="userSpaceOnUse"
123 >
129 <linearGradient id={cloudId} x1="0" y1="60" x2="288" y2="96" gradientUnits="userSpaceOnUse">
124130 <stop stopColor="#4EA4FF" stopOpacity="0.5" />
125131 <stop offset="0.52" stopColor="#696FEA" stopOpacity="0.62" />
126132 <stop offset="1" stopColor="#A85BEA" stopOpacity="0.5" />
127133 </linearGradient>
128 <filter
129 id="stage-night-soft"
130 x="-24"
131 y="-24"
132 width="336"
133 height="144"
134 filterUnits="userSpaceOnUse"
135 >
134 <filter id={softId} x="-24" y="-24" width="336" height="144" filterUnits="userSpaceOnUse">
136135 <feGaussianBlur stdDeviation="4" />
137136 </filter>
138 <pattern id="stage-night-stars" width="288" height="96" patternUnits="userSpaceOnUse">
137 <pattern id={starsId} width="288" height="96" patternUnits="userSpaceOnUse">
139138 <g fill="#E4EAFF">
140139 {NIGHTLY_STARS.map((star) => (
141140 <circle
......@@ -156,25 +155,25 @@ function NightlySkyArt() {
156155 ))}
157156 </g>
158157 </pattern>
159 <pattern id="stage-night-glows" width="640" height="96" patternUnits="userSpaceOnUse">
160 <rect width="640" height="96" fill="url(#stage-night-glow)" />
158 <pattern id={glowsId} width="640" height="96" patternUnits="userSpaceOnUse">
159 <rect width="640" height="96" fill={`url(#${glowId})`} />
161160 </pattern>
162161 </defs>
163162
164 <rect width="100%" height="96" fill="url(#stage-night-sky)" />
165 <rect width="100%" height="96" fill="url(#stage-night-glows)" />
166 <rect width="100%" height="96" fill="url(#stage-night-stars)" />
163 <rect width="100%" height="96" fill={`url(#${skyId})`} />
164 <rect width="100%" height="96" fill={`url(#${glowsId})`} />
165 <rect width="100%" height="96" fill={`url(#${starsId})`} />
167166
168 <g filter="url(#stage-night-soft)">
167 <g filter={`url(#${softId})`}>
169168 <path
170169 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})`}
172171 />
173172 </g>
174 <g filter="url(#stage-night-soft)">
173 <g filter={`url(#${softId})`}>
175174 <path
176175 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})`}
178177 fillOpacity="0.8"
179178 />
180179 </g>
......@@ -182,18 +181,29 @@ function NightlySkyArt() {
182181 );
183182}
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
186196 return (
187197 <svg
188198 className="stage-blueprint h-full w-full"
189199 fill="none"
190200 preserveAspectRatio="xMinYMin slice"
191 viewBox={STAGE_BACKDROP_VIEW_BOX}
201 viewBox={compact ? "64 0 8192 96" : STAGE_BACKDROP_VIEW_BOX}
192202 xmlns="http://www.w3.org/2000/svg"
193203 >
194204 <defs>
195205 <linearGradient
196 id="stage-bp-paper"
206 id={paperId}
197207 x1="60"
198208 y1="0"
199209 x2="220"
......@@ -206,7 +216,7 @@ function DevBlueprintArt() {
206216 <stop offset="1" style={{ stopColor: "var(--stage-bp-top)" }} />
207217 </linearGradient>
208218 <radialGradient
209 id="stage-bp-glow"
219 id={glowId}
210220 cx="0"
211221 cy="0"
212222 r="1"
......@@ -218,7 +228,7 @@ function DevBlueprintArt() {
218228 <stop offset="1" stopColor="#276AF1" stopOpacity="0" />
219229 </radialGradient>
220230 <radialGradient
221 id="stage-bp-glow-celeste"
231 id={celesteGlowId}
222232 cx="0"
223233 cy="0"
224234 r="1"
......@@ -230,7 +240,7 @@ function DevBlueprintArt() {
230240 <stop offset="1" stopColor="#277EF1" stopOpacity="0" />
231241 </radialGradient>
232242 <radialGradient
233 id="stage-bp-glow-violet"
243 id={violetGlowId}
234244 cx="0"
235245 cy="0"
236246 r="1"
......@@ -241,13 +251,13 @@ function DevBlueprintArt() {
241251 <stop offset="0.52" stopColor="#7C8BFF" stopOpacity="0.14" />
242252 <stop offset="1" stopColor="#3155DF" stopOpacity="0" />
243253 </radialGradient>
244 <pattern id="stage-bp-grid-minor" width="8" height="8" patternUnits="userSpaceOnUse">
254 <pattern id={minorGridId} width="8" height="8" patternUnits="userSpaceOnUse">
245255 <path d="M8 0H0V8" stroke="#EAF6FF" strokeOpacity="0.14" strokeWidth="0.5" />
246256 </pattern>
247 <pattern id="stage-bp-grid-major" width="32" height="32" patternUnits="userSpaceOnUse">
257 <pattern id={majorGridId} width="32" height="32" patternUnits="userSpaceOnUse">
248258 <path d="M32 0H0V32" stroke="#EAF6FF" strokeOpacity="0.26" strokeWidth="0.6" />
249259 </pattern>
250 <pattern id="stage-bp-ruler" width="32" height="6" patternUnits="userSpaceOnUse">
260 <pattern id={rulerId} width="32" height="6" patternUnits="userSpaceOnUse">
251261 <path
252262 d="M4 0V2.5M12 0V2.5M20 0V4M28 0V2.5"
253263 stroke="#DDF7FF"
......@@ -255,12 +265,12 @@ function DevBlueprintArt() {
255265 strokeWidth="0.5"
256266 />
257267 </pattern>
258 <pattern id="stage-bp-glows" width="768" height="96" patternUnits="userSpaceOnUse">
259 <rect width="768" height="96" fill="url(#stage-bp-glow)" />
260 <rect width="768" height="96" fill="url(#stage-bp-glow-celeste)" />
261 <rect width="768" height="96" fill="url(#stage-bp-glow-violet)" />
268 <pattern id={glowsId} width="768" height="96" patternUnits="userSpaceOnUse">
269 <rect width="768" height="96" fill={`url(#${glowId})`} />
270 <rect width="768" height="96" fill={`url(#${celesteGlowId})`} />
271 <rect width="768" height="96" fill={`url(#${violetGlowId})`} />
262272 </pattern>
263 <pattern id="stage-bp-annotations" width="768" height="96" patternUnits="userSpaceOnUse">
273 <pattern id={annotationsId} width="768" height="96" patternUnits="userSpaceOnUse">
264274 <g stroke="#DDF7FF" strokeLinecap="round" strokeOpacity="0.6" strokeWidth="0.7">
265275 <path d="M180 64H264" strokeDasharray="5 4" />
266276 <path d="M180 61V67M264 61V67" />
......@@ -309,12 +319,12 @@ function DevBlueprintArt() {
309319 </pattern>
310320 </defs>
311321
312 <rect width="100%" height="96" fill="url(#stage-bp-paper)" />
313 <rect width="100%" height="96" fill="url(#stage-bp-glows)" />
314 <rect width="100%" height="96" fill="url(#stage-bp-grid-minor)" />
315 <rect width="100%" height="96" fill="url(#stage-bp-grid-major)" />
316 <rect width="100%" height="6" fill="url(#stage-bp-ruler)" />
317 <rect width="100%" height="96" fill="url(#stage-bp-annotations)" />
322 <rect width="100%" height="96" fill={`url(#${paperId})`} />
323 <rect width="100%" height="96" fill={`url(#${glowsId})`} />
324 <rect width="100%" height="96" fill={`url(#${minorGridId})`} />
325 <rect width="100%" height="96" fill={`url(#${majorGridId})`} />
326 <rect width="100%" height="6" fill={`url(#${rulerId})`} />
327 <rect width="100%" height="96" fill={`url(#${annotationsId})`} />
318328 </svg>
319329 );
320330}
apps/web/src/components/SidebarV2.tsx+120-68
......@@ -1,7 +1,10 @@
11import { autoAnimate } from "@formkit/auto-animate";
22import { useAtomValue } from "@effect/atom-react";
33import { 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";
58import {
69 scopeProjectRef,
710 scopeThreadRef,
......@@ -11,9 +14,9 @@ import type { ScopedThreadRef } from "@t3tools/contracts";
1114import {
1215 CheckIcon,
1316 ChevronDownIcon,
17 CircleAlertIcon,
1418 CircleCheckIcon,
1519 CircleDashedIcon,
16 CircleAlertIcon,
1720 FolderIcon,
1821 FolderPlusIcon,
1922 GitBranchIcon,
......@@ -22,6 +25,7 @@ import {
2225 SearchIcon,
2326 ServerIcon,
2427 SquarePenIcon,
28 Trash2Icon,
2529 Undo2Icon,
2630} from "lucide-react";
2731import {
......@@ -69,6 +73,7 @@ import { useProjects, useThreadShells } from "../state/entities";
6973import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server";
7074import { vcsEnvironment } from "../state/vcs";
7175import { threadEnvironment } from "../state/threads";
76import { projectEnvironment } from "../state/projects";
7277import { useEnvironmentQuery } from "../state/query";
7378import { useAtomCommand } from "../state/use-atom-command";
7479import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes";
......@@ -94,15 +99,10 @@ import { stackedThreadToast, toastManager } from "./ui/toast";
9499import { CommandDialogTrigger } from "./ui/command";
95100import { Kbd } from "./ui/kbd";
96101import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
97import {
98 SidebarContent,
99 SidebarGroup,
100 SidebarMenuButton,
101 SidebarSeparator,
102 useSidebar,
103} from "./ui/sidebar";
102import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar";
104103import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome";
105104import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
105import { useComposerDraftStore } from "../composerDraftStore";
106106
107107// Settled-tail paging: recent history is the common lookup; the deep tail
108108// stays behind an explicit Show more.
......@@ -127,7 +127,6 @@ function SidebarV2ThreadTooltip({
127127 driverKind,
128128 modelInstanceId,
129129 modelLabel,
130 status,
131130 branchMismatch,
132131}: {
133132 thread: SidebarThreadSummary;
......@@ -137,11 +136,6 @@ function SidebarV2ThreadTooltip({
137136 driverKind: ProviderInstanceEntry["driverKind"] | null;
138137 modelInstanceId: string;
139138 modelLabel: string;
140 status: {
141 label: string;
142 className: string;
143 icon: "working" | "done" | null;
144 } | null;
145139 branchMismatch: {
146140 threadBranch: string;
147141 currentBranch: string;
......@@ -152,58 +146,31 @@ function SidebarV2ThreadTooltip({
152146 side="right"
153147 align="start"
154148 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"
156150 >
157 <div className="flex w-full flex-col gap-3 p-2">
158 <div className="flex min-w-0 items-start gap-3">
159 <div className="min-w-0 flex-1 text-sm font-medium text-foreground">{thread.title}</div>
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">
151 <div className="flex max-w-80 flex-col gap-2 p-2">
152 <div className="whitespace-nowrap text-sm font-medium text-foreground">{thread.title}</div>
153 <div className="grid gap-1.5 text-xs text-muted-foreground">
181154 {projectTitle ? (
182 <div className="flex min-w-0 items-start gap-2">
155 <div className="flex min-w-0 items-center gap-2">
183156 <ProjectFavicon
184157 environmentId={thread.environmentId}
185158 cwd={projectCwd ?? ""}
186159 className="size-4 shrink-0"
187160 />
188 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">
189 {projectTitle}
190 </div>
161 <div className="min-w-0 wrap-break-word text-foreground/90">{projectTitle}</div>
191162 </div>
192163 ) : null}
193164 {environmentLabel ? (
194 <div className="flex min-w-0 items-start gap-2">
165 <div className="flex min-w-0 items-center gap-2">
195166 <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" />
196 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">
197 {environmentLabel}
198 </div>
167 <div className="min-w-0 wrap-break-word text-foreground/90">{environmentLabel}</div>
199168 </div>
200169 ) : null}
201170 {thread.branch ? (
202 <div className="flex min-w-0 items-start gap-2">
171 <div className="flex min-w-0 items-center gap-2">
203172 <GitBranchIcon className="size-4 shrink-0 stroke-muted-foreground" />
204 <div className="min-w-0 flex-1 wrap-break-word text-foreground/90">
205 {thread.branch}
206 </div>
173 <div className="min-w-0 wrap-break-word text-foreground/90">{thread.branch}</div>
207174 </div>
208175 ) : null}
209176 {branchMismatch ? (
......@@ -215,19 +182,19 @@ function SidebarV2ThreadTooltip({
215182 </div>
216183 ) : null}
217184 {driverKind ? (
218 <div className="flex min-w-0 items-start gap-2">
185 <div className="flex min-w-0 items-center gap-2">
219186 <ProviderInstanceIcon
220187 driverKind={driverKind}
221188 displayName={thread.session?.providerName ?? modelInstanceId}
222189 iconClassName="size-4 shrink-0"
223190 />
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>
225192 </div>
226193 ) : null}
227194 {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">
229196 <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>
231198 </div>
232199 ) : null}
233200 </div>
......@@ -383,7 +350,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
383350 driverKind={driverKind}
384351 modelInstanceId={modelInstanceId}
385352 modelLabel={modelLabel}
386 status={topStatus}
387353 branchMismatch={branchMismatch}
388354 />
389355 );
......@@ -474,10 +440,11 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
474440 // content; surface is reserved for interaction (hover, multi-select, route).
475441 const rowSurfaceClassName = cn(
476442 "group/v2-row relative w-full cursor-pointer overflow-hidden rounded-md text-left outline-none select-none",
443 variant === "card" && "backdrop-blur-[16px]",
477444 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"
479446 : 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"
481448 : shouldRecede
482449 ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
483450 : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover",
......@@ -758,6 +725,9 @@ export default function SidebarV2() {
758725 const updateThreadMetadata = useAtomCommand(threadEnvironment.updateMetadata, {
759726 reportFailure: false,
760727 });
728 const deleteProject = useAtomCommand(projectEnvironment.delete, {
729 reportFailure: false,
730 });
761731 const newThreadContext = useHandleNewThread();
762732 const openAddProjectCommandPalette = useCallback(
763733 () => openCommandPalette({ open: "add-project" }),
......@@ -872,6 +842,77 @@ export default function SidebarV2() {
872842 clearSelection();
873843 }, [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
875916 // Settled threads stay in the live shell stream (settled ≠ archived), so
876917 // the partition works directly off live shells: no archived-snapshot
877918 // merging, no optimistic holds. Archived threads remain hidden here —
......@@ -1542,10 +1583,10 @@ export default function SidebarV2() {
15421583 <MenuRadioItem
15431584 value="all"
15441585 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"
15461587 >
1547 <FolderIcon className="size-3.5" />
1548 <span className="min-w-0 truncate">All projects</span>
1588 <FolderIcon className="size-4 shrink-0" />
1589 <span className="min-w-0 truncate text-sm">All projects</span>
15491590 </MenuRadioItem>
15501591 {projects.map((project) => {
15511592 const scopeKey = `${project.environmentId}:${project.id}`;
......@@ -1554,14 +1595,28 @@ export default function SidebarV2() {
15541595 key={scopeKey}
15551596 value={scopeKey}
15561597 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"
15581599 >
15591600 <ProjectFavicon
15601601 environmentId={project.environmentId}
15611602 cwd={project.workspaceRoot}
1562 className="size-3.5"
1603 className="size-4 shrink-0"
15631604 />
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>
15651620 </MenuRadioItem>
15661621 );
15671622 })}
......@@ -1718,9 +1773,6 @@ export default function SidebarV2() {
17181773 ) : null}
17191774 </SidebarGroup>
17201775 </SidebarContent>
1721 <div className="shrink-0 px-2">
1722 <SidebarSeparator className="mx-0 w-full bg-sidebar-border/80" />
1723 </div>
17241776 <SidebarChromeFooter />
17251777 </>
17261778 );
apps/web/src/components/chat/ChangedFilesTree.tsx+1-1
......@@ -41,7 +41,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
4141 const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]);
4242
4343 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">
4545 <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))]">
4646 <p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">
4747 <span>
apps/web/src/components/chat/ChatComposer.tsx+2-12
......@@ -99,7 +99,6 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
9999import { toastManager } from "../ui/toast";
100100import {
101101 BotIcon,
102 CheckIcon,
103102 CircleAlertIcon,
104103 ListTodoIcon,
105104 PencilRulerIcon,
......@@ -280,7 +279,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
280279 {runtimeModeOptions.map((mode) => {
281280 const option = runtimeModeConfig[mode];
282281 const OptionIcon = option.icon;
283 const isSelected = props.runtimeMode === mode;
284282 return (
285283 <SelectItem key={mode} value={mode} hideIndicator className="min-w-64 py-2">
286284 <div className="flex min-w-0 items-center gap-3">
......@@ -293,12 +291,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
293291 {option.description}
294292 </span>
295293 </div>
296 <CheckIcon
297 className={cn(
298 "size-4 text-blue-400",
299 isSelected ? "opacity-100" : "opacity-0",
300 )}
301 />
302294 </div>
303295 </SelectItem>
304296 );
......@@ -2162,10 +2154,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
21622154 ref={composerSurfaceRef}
21632155 data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}
21642156 className={cn(
2165 "chat-composer-glass rounded-[20px] border transition-[background-color] duration-200 has-focus-visible:border-foreground/40",
2166 isDragOverComposer
2167 ? "border-primary/70 bg-accent/45"
2168 : "border-black/12 dark:border-transparent dark:inset-ring-1 dark:inset-ring-white/5",
2157 "chat-composer-glass rounded-[20px] transition-[background-color] duration-200",
2158 isDragOverComposer ? "bg-accent/45 ring-1 ring-primary/70" : null,
21692159 projectSelectionRequired ? "opacity-75" : null,
21702160 composerProviderState.composerSurfaceClassName,
21712161 )}
apps/web/src/components/chat/ComposerPrimaryActions.tsx+13-1
......@@ -1,6 +1,7 @@
11import { memo, type PointerEventHandler } from "react";
22import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react";
33import { cn } from "~/lib/utils";
4import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop";
45import { Button } from "../ui/button";
56import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu";
67import { Spinner } from "../ui/spinner";
......@@ -71,6 +72,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
7172 const pointerFocusProps = preserveComposerFocusOnPointerDown
7273 ? { onPointerDown: preventPointerFocus }
7374 : undefined;
75 const stageBackdropVariant = useSidebarStageBackdropVariant();
7476
7577 if (pendingAction) {
7678 return (
......@@ -196,7 +198,12 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
196198 return (
197199 <button
198200 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 )}
200207 {...pointerFocusProps}
201208 disabled={isSendBusy || isConnecting || isEnvironmentUnavailable || !hasSendableContent}
202209 aria-label={
......@@ -211,6 +218,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
211218 : "Send message"
212219 }
213220 >
221 {stageBackdropVariant ? (
222 <span className="absolute inset-0 -z-10" aria-hidden="true">
223 <StageBackdropButtonArt variant={stageBackdropVariant} />
224 </span>
225 ) : null}
214226 {isConnecting || isSendBusy ? (
215227 <Spinner className="size-3.5" aria-hidden="true" />
216228 ) : (
apps/web/src/components/chat/ContextWindowMeter.tsx+12-5
......@@ -25,7 +25,9 @@ export function ContextWindowMeter(props: {
2525 const totalProcessedTokens = usage.totalProcessedTokens ?? null;
2626 const showTotalProcessed = totalProcessedTokens !== null && totalProcessedTokens > 0;
2727 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
3032 return (
3133 <Popover>
......@@ -37,7 +39,7 @@ export function ContextWindowMeter(props: {
3739 <button
3840 type="button"
3941 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",
4143 "hover:bg-accent data-[pressed]:bg-accent",
4244 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
4345 )}
......@@ -47,7 +49,7 @@ export function ContextWindowMeter(props: {
4749 : `Context window ${formatContextWindowTokens(usage.usedTokens)} tokens used`
4850 }
4951 >
50 <span className="relative flex size-4 items-center justify-center">
52 <span className="relative flex size-5 items-center justify-center">
5153 <svg
5254 viewBox="0 0 24 24"
5355 className="-rotate-90 absolute inset-0 size-full transform-gpu"
......@@ -58,7 +60,7 @@ export function ContextWindowMeter(props: {
5860 cy="12"
5961 r={radius}
6062 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)"
6264 strokeWidth="3"
6365 />
6466 <circle
......@@ -78,7 +80,12 @@ export function ContextWindowMeter(props: {
7880 </button>
7981 }
8082 />
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 >
8289 <div className="flex flex-col gap-2 p-3">
8390 <div className="flex items-center justify-between gap-3">
8491 <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) {
219219}
220220
221221describe("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
222239 it("keeps assistant changed-files headers sticky below the thread header", async () => {
223240 const { MessagesTimeline } = await import("./MessagesTimeline");
224241 const assistantMessageId = MessageId.make("message-assistant-with-files");
apps/web/src/components/chat/MessagesTimeline.tsx+11-5
......@@ -150,6 +150,7 @@ interface TimelineRowActivityState {
150150const TimelineRowCtx = createContext<TimelineRowSharedState>(null!);
151151const TimelineRowActivityCtx = createContext<TimelineRowActivityState>(null!);
152152const TIMELINE_LIST_HEADER = <div className="h-3 sm:h-4" />;
153const TIMELINE_LIST_FADE_HEADER = <div className="h-10 sm:h-12" />;
153154const TIMELINE_LIST_FOOTER = <div className="h-3 sm:h-4" />;
154155const EMPTY_TIMELINE_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
155156
......@@ -185,6 +186,7 @@ interface MessagesTimelineProps {
185186 onIsAtEndChange: (isAtEnd: boolean) => void;
186187 onManualNavigation: () => void;
187188 hideEmptyPlaceholder?: boolean;
189 topFadeEnabled?: boolean;
188190}
189191
190192// ---------------------------------------------------------------------------
......@@ -219,6 +221,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
219221 onIsAtEndChange,
220222 onManualNavigation,
221223 hideEmptyPlaceholder = false,
224 topFadeEnabled = false,
222225}: MessagesTimelineProps) {
223226 const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());
224227 const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());
......@@ -510,8 +513,11 @@ export const MessagesTimeline = memo(function MessagesTimeline({
510513 size: false,
511514 }}
512515 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"
514 ListHeaderComponent={TIMELINE_LIST_HEADER}
516 className={cn(
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}
515521 ListFooterComponent={TIMELINE_LIST_FOOTER}
516522 />
517523 <TimelineMinimap
......@@ -797,7 +803,7 @@ function TimelineMinimap({
797803 transform: `translateY(${activeTooltipTranslate})`,
798804 }}
799805 >
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">
801807 <span className="block max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5">
802808 {activeItem.userText ?? "User message"}
803809 </span>
......@@ -886,7 +892,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
886892
887893 return (
888894 <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">
890896 {regularImages.length > 0 && (
891897 <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
892898 {regularImages.map((image: NonNullable<TimelineMessage["attachments"]>[number]) => (
......@@ -1279,7 +1285,7 @@ function AssistantChangedFilesSectionInner({
12791285 const summaryStat = summarizeTurnDiffStats(checkpointFiles);
12801286
12811287 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">
12831289 <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))]">
12841290 <p className="flex items-center gap-1 whitespace-nowrap font-medium text-foreground text-xs leading-4">
12851291 <span>
apps/web/src/components/chat/ModelListRow.tsx+2-3
......@@ -1,6 +1,6 @@
11import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts";
22import { memo } from "react";
3import { CheckIcon, StarIcon } from "lucide-react";
3import { StarIcon } from "lucide-react";
44import {
55 getDisplayModelName,
66 getTriggerDisplayModelLabel,
......@@ -51,7 +51,7 @@ export const ModelListRow = memo(function ModelListRow(props: {
5151 contentClassName="flex w-full items-center gap-3"
5252 className={cn(
5353 "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",
5555 props.disabledReason &&
5656 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",
5757 )}
......@@ -66,7 +66,6 @@ export const ModelListRow = memo(function ModelListRow(props: {
6666 props.preferShortName ? { preferShortName: true } : undefined,
6767 )}
6868 </div>
69 {props.isSelected ? <CheckIcon className="size-3.5 shrink-0 text-blue-400" /> : null}
7069 {props.showNewBadge ? (
7170 <span
7271 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: {
521521 return (
522522 <TooltipProvider delay={0}>
523523 <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"
525525 data-model-picker-content="true"
526526 >
527527 {/* Sidebar */}
......@@ -568,15 +568,10 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
568568 handleModelSelect(slug, instanceId);
569569 }}
570570 >
571 <div
572 className={cn(
573 "flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40",
574 showSidebar && "border-l",
575 )}
576 >
571 <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
577572 {/* Search bar */}
578573 <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">
580575 <ComboboxInput
581576 ref={searchInputRef}
582577 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: {
9797 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);
9898
9999 return (
100 <div
101 className="w-12 shrink-0 overflow-hidden border-r bg-muted/30"
102 data-model-picker-sidebar="true"
103 >
100 <div className="w-12 shrink-0 overflow-hidden bg-muted/60" data-model-picker-sidebar="true">
104101 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
105102 <div
106103 ref={sidebarContentRef}
......@@ -118,7 +115,7 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
118115 ) : null}
119116 {/* Favorites section */}
120117 {showFavorites ? (
121 <div className="mb-1 border-b pb-1">
118 <div className="mb-1 pb-1">
122119 <div className="relative w-full">
123120 <Tooltip>
124121 <TooltipTrigger
apps/web/src/components/chat/ProviderStatusBanner.tsx+1-1
......@@ -27,7 +27,7 @@ export const ProviderStatusBanner = memo(function ProviderStatusBanner({
2727 : `${providerName} provider has limited availability.`));
2828
2929 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">
3131 <div
3232 className={cn(
3333 "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 {
1616} from "@t3tools/shared/model";
1717import { memo, useCallback, useState } from "react";
1818import type { VariantProps } from "class-variance-authority";
19import { CheckIcon, ChevronDownIcon } from "lucide-react";
19import { ChevronDownIcon } from "lucide-react";
2020import { Button, buttonVariants } from "../ui/button";
2121import {
2222 Menu,
......@@ -337,9 +337,6 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
337337 </>
338338 ) : null}
339339 </span>
340 {option.id === selectedValue ? (
341 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
342 ) : null}
343340 </span>
344341 </MenuRadioItem>
345342 ))}
......@@ -370,9 +367,6 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
370367 <MenuRadioItem key={value} value={value} hideIndicator>
371368 <span className="flex w-full min-w-0 items-center justify-between gap-3">
372369 <span>{value === "on" ? "On" : "Off"}</span>
373 {value === selectedValue ? (
374 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
375 ) : null}
376370 </span>
377371 </MenuRadioItem>
378372 ))}
apps/web/src/components/settings/ConnectionsSettings.tsx+3-6
......@@ -373,7 +373,7 @@ function formatDesktopSshConnectionError(error: unknown): string {
373373 return withoutTaggedErrorPrefix.trim() || fallback;
374374}
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
378378type AccessSectionPresentation = "current" | "endpoint-rail";
379379
......@@ -383,10 +383,7 @@ function accessRowClassName(_presentation: AccessSectionPresentation) {
383383
384384function endpointRowClassName(presentation: AccessSectionPresentation, isAvailable: boolean) {
385385 if (presentation === "endpoint-rail") {
386 return cn(
387 "relative border-t border-border/60 px-4 py-3 first:border-t-0 sm:px-5",
388 !isAvailable && "bg-muted/20",
389 );
386 return cn("relative rounded-xl px-3 py-3 sm:px-4", !isAvailable && "bg-muted/15");
390387 }
391388
392389 return cn(ENDPOINT_ROW_CLASSNAME, !isAvailable && "bg-muted/24");
......@@ -1524,7 +1521,7 @@ const DesktopSshHostRow = memo(function DesktopSshHostRow({
15241521 const buttonLabel = connectingHostAlias === target.alias ? "Adding…" : "Add environment";
15251522
15261523 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">
15281525 <div className={ITEM_ROW_INNER_CLASSNAME}>
15291526 <div className="min-w-0 flex-1">
15301527 <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({
531531 const titleHeadNode = (
532532 <>
533533 {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">
535535 {displayName}
536536 </h3>
537537 {String(instanceId) !== String(instance.driver) ? (
......@@ -578,7 +578,7 @@ export function ProviderInstanceCard({
578578 );
579579
580580 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">
582582 {hasAuthenticatedEmail ? (
583583 <>
584584 <span>Authenticated as</span>
......@@ -600,8 +600,8 @@ export function ProviderInstanceCard({
600600 ) : null;
601601
602602 return (
603 <div className="border-t border-border/60 first:border-t-0">
604 <div className="px-4 py-3.5 sm:px-5">
603 <div className="rounded-xl transition-colors hover:bg-muted/20">
604 <div className="px-3 py-3 sm:px-4">
605605 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
606606 <div className="min-w-0 flex-1 space-y-1">
607607 <div className="flex min-w-0 flex-wrap items-center gap-2">
......@@ -729,8 +729,8 @@ export function ProviderInstanceCard({
729729
730730 <Collapsible open={isExpanded} onOpenChange={onExpandedChange}>
731731 <CollapsibleContent>
732 <div className="space-y-0">
733 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
732 <div className="space-y-5 px-3 pb-4 pt-2 sm:px-4">
733 <div>
734734 <label htmlFor={`provider-instance-${instanceId}-display-name`} className="block">
735735 <span className="text-xs font-medium text-foreground">Display name</span>
736736 <DraftInput
......@@ -747,7 +747,7 @@ export function ProviderInstanceCard({
747747 </label>
748748 </div>
749749
750 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
750 <div>
751751 <ProviderAccentColorPicker
752752 displayName={displayName}
753753 value={accentColor}
......@@ -757,7 +757,7 @@ export function ProviderInstanceCard({
757757 />
758758 </div>
759759
760 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
760 <div>
761761 <ProviderEnvironmentSection
762762 environment={instance.environment ?? []}
763763 onChange={updateEnvironment}
......@@ -789,7 +789,7 @@ export function ProviderInstanceCard({
789789 onModelOrderChange={onModelOrderChange}
790790 />
791791 ) : (
792 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
792 <div>
793793 <p className="text-xs text-muted-foreground">
794794 This instance uses a driver (
795795 <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({
185185 };
186186
187187 return (
188 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
188 <div>
189189 <div className="text-xs font-medium text-foreground">Models</div>
190190 <div className="mt-1 text-xs text-muted-foreground">
191191 {models.length} model{models.length === 1 ? "" : "s"} available.
apps/web/src/components/settings/ProviderSettingsForm.tsx+1-1
......@@ -167,7 +167,7 @@ function FieldFrame(props: {
167167 readonly children: ReactNode;
168168}) {
169169 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>;
171171 }
172172 return <div className="grid gap-1.5">{props.children}</div>;
173173}
apps/web/src/components/settings/SettingsSidebarNav.tsx+4-7
......@@ -18,7 +18,6 @@ import {
1818 SidebarMenu,
1919 SidebarMenuButton,
2020 SidebarMenuItem,
21 SidebarSeparator,
2221 useSidebar,
2322} from "../ui/sidebar";
2423import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn";
......@@ -85,8 +84,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
8584 isActive={isActive}
8685 className={
8786 isActive
88 ? "gap-2.5 px-2.5 py-2 text-left text-[13px] 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"
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"
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"
9089 }
9190 onClick={() => handleSectionClick(item.to)}
9291 >
......@@ -94,7 +93,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
9493 className={
9594 isActive
9695 ? "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"
9897 }
9998 />
10099 <span className="truncate">{item.label}</span>
......@@ -105,8 +104,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
105104 </SidebarMenu>
106105 </SidebarGroup>
107106 </SidebarContent>
108
109 <SidebarSeparator />
110107 <SidebarFooter className="p-2">
111108 <T3ConnectSidebarSignIn />
112109 <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">
......@@ -114,7 +111,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
114111 <SidebarMenuItem>
115112 <SidebarMenuButton
116113 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"
118115 onClick={handleBackClick}
119116 >
120117 <ArrowLeftIcon className="size-4" />
apps/web/src/components/settings/SourceControlSettings.tsx+6-6
......@@ -229,16 +229,16 @@ function DiscoveryItemRow({
229229 return (
230230 <div
231231 className={cn(
232 "border-t border-border/60 first:border-t-0",
232 "rounded-xl transition-colors hover:bg-muted/20",
233233 isVcsNotReady(item) && "opacity-80",
234234 )}
235235 >
236 <div className="px-4 py-3.5 sm:px-5">
236 <div className="px-3 py-3 sm:px-4">
237237 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
238238 <div className="min-w-0 flex-1 space-y-1">
239239 <div className="flex min-w-0 flex-wrap items-center gap-2">
240240 <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">
242242 {item.label}
243243 </span>
244244 {version ? <code className="text-xs text-muted-foreground">{version}</code> : null}
......@@ -253,7 +253,7 @@ function DiscoveryItemRow({
253253 </Badge>
254254 ) : null}
255255 </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">
257257 {itemSummary({ item, auth, authAccount })}
258258 </p>
259259 </div>
......@@ -282,7 +282,7 @@ function DiscoveryItemRow({
282282 {hasDetails ? (
283283 <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
284284 <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>
286286 </CollapsibleContent>
287287 </Collapsible>
288288 ) : null}
......@@ -368,7 +368,7 @@ function SourceControlSectionSkeleton({
368368 return (
369369 <SettingsSection title={title} headerAction={headerAction}>
370370 {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">
372372 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
373373 <div className="min-w-0 flex-1 space-y-2">
374374 <div className="flex items-center gap-2">
apps/web/src/components/settings/itemRows.ts+2-2
......@@ -1,5 +1,5 @@
1/** Direct row in the card – same pattern as the Provider / ACP-agent list rows. */
2export const ITEM_ROW_CLASSNAME = "border-t border-border/60 px-4 py-4 first:border-t-0 sm:px-5";
1/** Direct row in a settings section. Whitespace, rather than rules, separates peers. */
2export const ITEM_ROW_CLASSNAME = "rounded-xl px-3 py-3 sm:px-4";
33
44export const ITEM_ROW_INNER_CLASSNAME =
55 "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({
2929 children: ReactNode;
3030}) {
3131 return (
32 <section {...sectionProps} className={cn("space-y-2.5", className)}>
33 <div className="flex items-center justify-between px-1">
34 <h2 className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-foreground/50">
35 <span className="inline-block h-px w-3 bg-border" aria-hidden />
32 <section {...sectionProps} className={cn("space-y-3", className)}>
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-lg font-semibold tracking-[-0.025em] text-foreground">
3635 {icon}
3736 {title}
3837 </h2>
39 <div className="flex h-5 min-w-5 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}
38 <div className="flex min-h-7 min-w-7 items-center justify-end">{headerAction}</div>
4339 </div>
40 <div className="relative space-y-1 overflow-visible text-foreground">{children}</div>
4441 </section>
4542 );
4643}
......@@ -65,24 +62,20 @@ export function SettingsRow({
6562 return (
6663 <div
6764 {...rowProps}
68 className={cn(
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 )}
65 className={cn("rounded-xl px-3 sm:px-4", children ? "pt-3 pb-1" : "py-3", className)}
7366 >
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">
7568 <div className="min-w-0 flex-1 space-y-1">
7669 <div className="flex min-h-5 items-center gap-1.5">
77 <h3 className="text-[13px] font-semibold tracking-[-0.01em] text-foreground">
78 {title}
79 </h3>
70 <h3 className="text-sm font-medium tracking-[-0.005em] text-foreground">{title}</h3>
8071 <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">
8172 {resetAction}
8273 </span>
8374 </div>
84 <p className="text-xs text-muted-foreground/80">{description}</p>
85 {status ? <div className="pt-0.5 text-[11px] text-muted-foreground">{status}</div> : null}
75 <p className="max-w-xl text-[13px] leading-[1.45] text-muted-foreground/80">
76 {description}
77 </p>
78 {status ? <div className="pt-0.5 text-xs text-muted-foreground">{status}</div> : null}
8679 </div>
8780 {control ? (
8881 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
......@@ -127,8 +120,8 @@ export function SettingsPageContainer({
127120 className?: string;
128121}) {
129122 return (
130 <div className="scrollbar-gutter-both flex-1 overflow-y-auto p-6 sm:p-8">
131 <div className={cn("mx-auto flex w-full max-w-3xl flex-col gap-8", className)}>
123 <div className="scrollbar-gutter-both flex-1 overflow-y-auto px-4 py-7 sm:px-8 sm:py-10">
124 <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}>
132125 {children}
133126 </div>
134127 </div>
apps/web/src/components/sidebar/SidebarChrome.tsx+2-2
......@@ -115,10 +115,10 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
115115 <SidebarMenuItem>
116116 <SidebarMenuButton
117117 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"
119119 onClick={handleSettingsClick}
120120 >
121 <SettingsIcon className="size-4 shrink-0" />
121 <SettingsIcon className="size-4.5 shrink-0" />
122122 <span>Settings</span>
123123 </SidebarMenuButton>
124124 </SidebarMenuItem>
apps/web/src/components/ui/button.tsx+1-1
......@@ -40,7 +40,7 @@ const buttonVariants = cva(
4040 "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [&_svg:not([class*='text-'])]:text-muted-foreground",
4141 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
4242 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",
4444 secondary:
4545 "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",
4646 },
apps/web/src/components/ui/combobox.tsx+5-9
......@@ -1,7 +1,7 @@
11"use client";
22
33import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
4import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
4import { ChevronsUpDownIcon, XIcon } from "lucide-react";
55import * as React from "react";
66
77import { cn } from "~/lib/utils";
......@@ -170,7 +170,7 @@ function ComboboxPopup({
170170 >
171171 <span
172172 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]",
174174 className,
175175 )}
176176 >
......@@ -191,7 +191,7 @@ function ComboboxItem({
191191 className,
192192 contentClassName,
193193 children,
194 hideIndicator = false,
194 hideIndicator: _hideIndicator = false,
195195 ...props
196196}: ComboboxPrimitive.Item.Props & {
197197 contentClassName?: string;
......@@ -200,19 +200,15 @@ function ComboboxItem({
200200 return (
201201 <ComboboxPrimitive.Item
202202 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",
204204 className,
205205 )}
206206 data-slot="combobox-item"
207207 {...props}
208208 >
209 <ComboboxPrimitive.ItemIndicator className={cn("col-start-1", hideIndicator && "hidden")}>
210 <CheckIcon />
211 </ComboboxPrimitive.ItemIndicator>
212209 <div
213210 className={cn(
214 "[&_svg:not([class*='text-'])]:text-muted-foreground",
215 hideIndicator ? "col-start-1 col-span-full" : "col-start-2",
211 "min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground",
216212 contentClassName,
217213 )}
218214 data-slot="combobox-item-content"
apps/web/src/components/ui/command.tsx+12-5
......@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B
3030 return (
3131 <CommandDialogPrimitive.Backdrop
3232 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",
3434 className,
3535 )}
3636 data-slot="command-dialog-backdrop"
......@@ -66,7 +66,7 @@ function CommandDialogPopup({
6666 <CommandDialogViewport>
6767 <CommandDialogPrimitive.Popup
6868 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",
7070 className,
7171 )}
7272 data-slot="command-dialog-popup"
......@@ -144,7 +144,7 @@ function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
144144 return (
145145 <div
146146 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]",
148148 className,
149149 )}
150150 {...props}
......@@ -171,7 +171,14 @@ function CommandCollection({ ...props }: React.ComponentProps<typeof Autocomplet
171171
172172function CommandItem({ className, ...props }: React.ComponentProps<typeof AutocompleteItem>) {
173173 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 />
175182 );
176183}
177184
......@@ -205,7 +212,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
205212 return (
206213 <div
207214 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",
209216 className,
210217 )}
211218 data-slot="command-footer"
apps/web/src/components/ui/menu.tsx+4-22
......@@ -49,7 +49,7 @@ function MenuPopup({
4949 >
5050 <MenuPrimitive.Popup
5151 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",
5353 className,
5454 )}
5555 data-slot="menu-popup"
......@@ -150,7 +150,7 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
150150function MenuRadioItem({
151151 className,
152152 children,
153 hideIndicator = false,
153 hideIndicator: _hideIndicator = false,
154154 ...props
155155}: MenuPrimitive.RadioItem.Props & {
156156 hideIndicator?: boolean;
......@@ -158,31 +158,13 @@ function MenuRadioItem({
158158 return (
159159 <MenuPrimitive.RadioItem
160160 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",
162 hideIndicator ? "grid-cols-[1fr] ps-3 pe-3" : "grid-cols-[1rem_1fr] ps-2 pe-4",
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",
163162 className,
164163 )}
165164 data-slot="menu-radio-item"
166165 {...props}
167166 >
168 {hideIndicator ? null : (
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>
167 <span className="min-w-0 flex-1">{children}</span>
186168 </MenuPrimitive.RadioItem>
187169 );
188170}
apps/web/src/components/ui/select.tsx+4-25
......@@ -150,7 +150,7 @@ function SelectPopup({
150150 </SelectPrimitive.ScrollUpArrow>
151151 <div
152152 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",
154154 matchTriggerWidth && "min-w-(--anchor-width)",
155155 popupClassName,
156156 )}
......@@ -177,7 +177,7 @@ function SelectPopup({
177177function SelectItem({
178178 className,
179179 children,
180 hideIndicator = false,
180 hideIndicator: _hideIndicator = false,
181181 ...props
182182}: SelectPrimitive.Item.Props & {
183183 hideIndicator?: boolean;
......@@ -185,35 +185,14 @@ function SelectItem({
185185 return (
186186 <SelectPrimitive.Item
187187 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",
189 hideIndicator ? "grid-cols-[1fr] ps-3 pe-3" : "grid-cols-[1rem_1fr] ps-2 pe-4",
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",
190189 className,
191190 )}
192191 data-slot="select-item"
193192 {...props}
194193 >
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 )}
212194 <SelectPrimitive.ItemText
213 className={cn(
214 "min-w-0 [&_svg:not([class*='text-'])]:text-muted-foreground",
215 hideIndicator ? "col-start-1" : "col-start-2",
216 )}
195 className="min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground"
217196 data-slot="select-item-text"
218197 >
219198 {children}
apps/web/src/index.css+183-75
......@@ -107,7 +107,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
107107}
108108
109109@theme inline {
110 --color-zinc-25: oklch(99.2% 0 0);
111110 --animate-skeleton: skeleton 2s -1s infinite linear;
112111 /* Duty-cycled indicator animations: long holds with stepped ramps, so the
113112 compositor updates discrete frames instead of every vsync. */
......@@ -303,6 +302,46 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
303302 align-items: center;
304303 }
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
306345 .workspace-titlebar-controls {
307346 position: absolute;
308347 top: var(--workspace-controls-top);
......@@ -323,50 +362,75 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
323362 }
324363
325364 .chat-composer-glass {
326 background: var(--surface-raised);
365 background: var(--secondary);
366 -webkit-backdrop-filter: blur(16px);
367 backdrop-filter: blur(16px);
327368 }
328369
329 /* Mix from --background (not --card): the lower chrome is a full-width
330 strip over the main view, and a card-tinted wash reads as a lighter
331 seam against it when no content is scrolled underneath. The backdrop
332 blur, not the tint, carries the frosted effect. */
333 .chat-composer-lower-chrome {
334 background: color-mix(in srgb, var(--background) 20%, transparent);
370 .chat-composer-context-strip {
371 position: relative;
372 isolation: isolate;
335373 }
336374
337 /* The translucent lower chrome lets the composer's shadow taper naturally
338 into the message canvas. Above the terminal drawer that same shadow reads
339 as a dirty light-to-black gradient, so end the workspace on an opaque
340 surface and let the drawer's border define the boundary instead. */
341 .chat-composer-lower-chrome[data-terminal-open="true"] {
342 background: var(--background);
375 .chat-composer-context-strip::before {
376 position: absolute;
377 z-index: -1;
378 inset: 0;
379 border-radius: 0 0 16px 16px;
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);
343385 }
344386
345 .chat-composer-glass {
346 box-shadow:
347 0 18px 48px -20px rgb(0 0 0 / 28%),
348 0 4px 14px -7px rgb(0 0 0 / 22%);
387 .dropdown-glass {
388 background: var(--secondary);
389 -webkit-backdrop-filter: blur(16px);
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%);
349396 }
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);
352408 -webkit-backdrop-filter: blur(16px);
353409 backdrop-filter: blur(16px);
410 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
354411 }
355412
356 .dark .chat-composer-glass {
357 background: var(--surface-raised);
413 .dark .dropdown-glass {
414 box-shadow: 0 18px 44px -18px rgb(0 0 0 / 80%);
358415 }
359416
360 .dark .chat-composer-lower-chrome {
361 background: color-mix(in srgb, var(--background) 45%, transparent);
417 .dark .model-picker-surface.model-picker-surface {
418 background: color-mix(in srgb, var(--background) 90%, transparent);
362419 }
363420
364 .dark .chat-composer-lower-chrome[data-terminal-open="true"] {
365 background: var(--background);
421 .dark .command-menu-surface,
422 .dark .command-palette-glass,
423 .dark .workflow-dialog-surface {
424 box-shadow: none;
366425 }
367426
368 .dark .chat-composer-glass {
369 box-shadow: none;
427 .dark .workflow-dialog-surface {
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%);
370434 }
371435
372436 .chat-composer-lower-chrome {
......@@ -376,6 +440,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
376440 }
377441
378442 @media (min-width: 40rem) {
443 .chat-timeline-scroll-fade {
444 --timeline-top-fade-height: 3rem;
445 }
446
379447 .chat-composer-horizontal-inset {
380448 padding-inline-start: calc(env(safe-area-inset-left) + 1.25rem);
381449 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
384452
385453 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
386454 .chat-composer-glass {
455 background: var(--secondary);
456 }
457
458 .chat-composer-context-strip::before {
387459 background: var(--card);
388460 }
389461
390 .chat-composer-lower-chrome {
391 background: var(--background);
462 .dropdown-glass {
463 background: color-mix(in srgb, var(--popover) 88%, black);
464 }
465
466 .workflow-dialog-surface {
467 background: var(--popover);
392468 }
393469 }
394470}
......@@ -430,25 +506,25 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
430506:root {
431507 color-scheme: light;
432508 --radius: 0.625rem;
433 --background: var(--color-zinc-25);
509 --background: var(--color-white);
434510 --app-chrome-background: var(--background);
435 --surface-raised: color-mix(in srgb, var(--card) 20%, transparent);
436 --foreground: var(--color-zinc-800);
511 --surface-raised: var(--secondary);
512 --foreground: var(--color-neutral-800);
437513 --card: var(--color-white);
438 --card-foreground: var(--color-zinc-800);
514 --card-foreground: var(--color-neutral-800);
439515 --popover: var(--color-white);
440 --popover-foreground: var(--color-zinc-800);
516 --popover-foreground: var(--color-neutral-800);
441517 --primary: oklch(0.488 0.217 264);
442518 --primary-foreground: var(--color-white);
443 --secondary: var(--color-zinc-50);
444 --secondary-foreground: var(--color-zinc-800);
445 --muted: var(--color-zinc-50);
446 --muted-foreground: var(--color-zinc-500);
447 --accent: var(--color-zinc-100);
448 --accent-foreground: var(--color-zinc-900);
519 --secondary: --alpha(var(--color-black) / 4%);
520 --secondary-foreground: var(--color-neutral-800);
521 --muted: --alpha(var(--color-black) / 4%);
522 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));
523 --accent: --alpha(var(--color-black) / 4%);
524 --accent-foreground: var(--color-neutral-800);
449525 --destructive: var(--color-red-500);
450 --border: var(--color-zinc-200);
451 --input: var(--color-zinc-300);
526 --border: --alpha(var(--color-black) / 8%);
527 --input: --alpha(var(--color-black) / 10%);
452528 --ring: oklch(0.488 0.217 264);
453529 --destructive-foreground: var(--color-red-700);
454530 --info: var(--color-blue-500);
......@@ -470,22 +546,24 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
470546
471547 @variant dark {
472548 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);
474552 --app-chrome-background: var(--background);
475 --surface-raised: color-mix(in srgb, var(--card) 45%, transparent);
476 --foreground: var(--color-zinc-100);
477 --card: var(--color-zinc-900);
478 --card-foreground: var(--color-zinc-100);
479 --popover: var(--color-zinc-900);
480 --popover-foreground: var(--color-zinc-100);
553 --surface-raised: var(--secondary);
554 --foreground: var(--color-neutral-100);
555 --card: color-mix(in srgb, var(--background) 97%, var(--color-white));
556 --card-foreground: var(--color-neutral-100);
557 --popover: color-mix(in srgb, var(--background) 94%, var(--color-white));
558 --popover-foreground: var(--color-neutral-100);
481559 --primary: oklch(0.588 0.217 264);
482560 --primary-foreground: var(--color-white);
483 --secondary: var(--color-zinc-900);
484 --secondary-foreground: var(--color-zinc-100);
485 --muted: var(--color-zinc-900);
486 --muted-foreground: var(--color-zinc-400);
487 --accent: var(--color-zinc-900);
488 --accent-foreground: var(--color-zinc-100);
561 --secondary: --alpha(var(--color-white) / 4%);
562 --secondary-foreground: var(--color-neutral-100);
563 --muted: --alpha(var(--color-white) / 4%);
564 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-white));
565 --accent: --alpha(var(--color-white) / 4%);
566 --accent-foreground: var(--color-neutral-100);
489567 --destructive: color-mix(in srgb, var(--color-red-500) 90%, var(--color-white));
490568 --border: --alpha(var(--color-white) / 6%);
491569 --input: --alpha(var(--color-white) / 8%);
......@@ -500,27 +578,57 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
500578 }
501579}
502580
503[data-app-sidebar] {
504 --sidebar: var(--color-zinc-50);
505 --sidebar-foreground: var(--color-zinc-800);
506 --sidebar-muted-foreground: var(--color-zinc-500);
507 --sidebar-control-surface: var(--color-zinc-100);
508 --sidebar-row-hover: var(--color-zinc-25);
509 --sidebar-row-active: var(--color-white);
510 --sidebar-row-selected: var(--color-white);
511 --sidebar-border: var(--color-zinc-200);
581/* Give both navigation implementations the same cool graphite surface. The
582 version attribute remains useful for layout-specific styling, but color is
583 shared so switching the beta does not switch visual systems. */
584[data-sidebar-version="v1"],
585[data-sidebar-version="v2"] {
586 --background: oklch(0.965 0.009 265);
587 --foreground: oklch(0.27 0.025 265);
588 --card: oklch(0.945 0.014 265);
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);
512604 --sidebar-stage-fade: var(--sidebar);
513}
514
515.dark [data-app-sidebar] {
516 --sidebar: var(--color-zinc-950);
517 --sidebar-foreground: var(--color-zinc-100);
518 --sidebar-muted-foreground: var(--color-zinc-400);
519 --sidebar-control-surface: var(--color-zinc-800);
520 --sidebar-row-hover: var(--color-zinc-900);
521 --sidebar-row-active: var(--color-zinc-900);
522 --sidebar-row-selected: var(--color-zinc-900);
523 --sidebar-border: var(--color-zinc-800);
605 background-color: var(--card);
606}
607
608.dark [data-sidebar-version="v1"],
609.dark [data-sidebar-version="v2"] {
610 --background: #000;
611 --foreground: #f1f3f7;
612 --card: #000;
613 --card-foreground: var(--foreground);
614 --accent: #191a1d;
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);
524632}
525633
526634body {
apps/web/src/routes/settings.tsx+3-3
......@@ -22,7 +22,7 @@ function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {
2222 return (
2323 <Button
2424 size="xs"
25 variant="outline"
25 variant="ghost"
2626 disabled={changedSettingLabels.length === 0}
2727 onClick={() => void restoreDefaults()}
2828 >
......@@ -68,7 +68,7 @@ function SettingsContentLayout() {
6868 {!isElectron && (
6969 <header
7070 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",
7272 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
7373 )}
7474 >
......@@ -86,7 +86,7 @@ function SettingsContentLayout() {
8686 {isElectron && (
8787 <div
8888 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)]",
9090 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
9191 )}
9292 >