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