| author | |
| committer | |
| log | 07f8027d9a542f72b12db927caceebb2c9c72aa7 |
| tree | 22131031c7f6700663e4e90ab289250527501dc3 |
| parent | cf251c3bd00ff0e0cc90a78b20c043cf58f330ad |
| signature | Signed by PGP key B5690EEEBB952194 |
12 files changed, 167 insertions(+), 194 deletions(-)
apps/web/src/components/ChatView.tsx+6-15| ... | @@ -170,7 +170,6 @@ import { | ... | @@ -170,7 +170,6 @@ import { |
| 170 | WifiOffIcon, | 170 | WifiOffIcon, |
| 171 | } from "lucide-react"; | 171 | } from "lucide-react"; |
| 172 | import { cn, randomHex } from "~/lib/utils"; | 172 | import { cn, randomHex } from "~/lib/utils"; |
| 173 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 174 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 173 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 175 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; | 174 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; |
| 176 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; | 175 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; |
| ... | @@ -259,6 +258,7 @@ import { ChatHeader } from "./chat/ChatHeader"; | ... | @@ -259,6 +258,7 @@ import { ChatHeader } from "./chat/ChatHeader"; |
| 259 | import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls"; | 258 | import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls"; |
| 260 | import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; | 259 | import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; |
| 261 | import { NoActiveThreadState } from "./NoActiveThreadState"; | 260 | import { NoActiveThreadState } from "./NoActiveThreadState"; |
| 261 | import { WorkspacePageHeader } from "./WorkspacePageHeader"; | ||
| 262 | import { | 262 | import { |
| 263 | resolveEffectiveEnvMode, | 263 | resolveEffectiveEnvMode, |
| 264 | resolveLocalCheckoutBranchMismatch, | 264 | resolveLocalCheckoutBranchMismatch, |
| ... | @@ -6240,20 +6240,11 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -6240,20 +6240,11 @@ function ChatViewContent(props: ChatViewProps) { |
| 6240 | data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"} | 6240 | data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"} |
| 6241 | > | 6241 | > |
| 6242 | {/* Top bar */} | 6242 | {/* Top bar */} |
| 6243 | <header | 6243 | <WorkspacePageHeader |
| 6244 | data-chat-header | 6244 | data-chat-header |
| 6245 | className={cn( | 6245 | electron={isElectron} |
| 6246 | "bg-background transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none", | 6246 | reserveNativeControls={reserveTitleBarControlInset && !inlineRightPanelOwnsTitleBar} |
| 6247 | isElectron | 6247 | className="relative bg-background" |
| 6248 | ? cn( | ||
| 6249 | "drag-region relative flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 sm:px-5", | ||
| 6250 | reserveTitleBarControlInset && | ||
| 6251 | !inlineRightPanelOwnsTitleBar && | ||
| 6252 | "wco:pr-[var(--workspace-native-controls-inset)]", | ||
| 6253 | ) | ||
| 6254 | : "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]", | ||
| 6255 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 6256 | )} | ||
| 6257 | > | 6248 | > |
| 6258 | {!rightPanelOpen ? panelLayoutControls : null} | 6249 | {!rightPanelOpen ? panelLayoutControls : null} |
| 6259 | <ChatHeader | 6250 | <ChatHeader |
| ... | @@ -6284,7 +6275,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -6284,7 +6275,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 6284 | onUpdateProjectScript={updateProjectScript} | 6275 | onUpdateProjectScript={updateProjectScript} |
| 6285 | onDeleteProjectScript={deleteProjectScript} | 6276 | onDeleteProjectScript={deleteProjectScript} |
| 6286 | /> | 6277 | /> |
| 6287 | </header> | 6278 | </WorkspacePageHeader> |
| 6288 | 6279 | ||
| 6289 | <ThreadErrorBanner | 6280 | <ThreadErrorBanner |
| 6290 | error={visibleThreadError} | 6281 | error={visibleThreadError} |
apps/web/src/components/NoActiveThreadState.tsx+4-15| ... | @@ -1,26 +1,15 @@ | ... | @@ -1,26 +1,15 @@ |
| 1 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty"; | 1 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty"; |
| 2 | import { SidebarInset } from "./ui/sidebar"; | 2 | import { SidebarInset } from "./ui/sidebar"; |
| 3 | import { isElectron } from "../env"; | 3 | import { isElectron } from "../env"; |
| 4 | import { cn } from "~/lib/utils"; | 4 | import { WorkspacePageHeader } from "./WorkspacePageHeader"; |
| 5 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 6 | 5 | ||
| 7 | export function NoActiveThreadState() { | 6 | export function NoActiveThreadState() { |
| 8 | return ( | 7 | return ( |
| 9 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 8 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> |
| 10 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> | 9 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> |
| 11 | <header | 10 | <WorkspacePageHeader electron={isElectron} className="border-b border-border"> |
| 12 | className={cn( | ||
| 13 | "border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", | ||
| 14 | isElectron | ||
| 15 | ? "drag-region flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center" | ||
| 16 | : "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center", | ||
| 17 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 18 | )} | ||
| 19 | > | ||
| 20 | {isElectron ? ( | 11 | {isElectron ? ( |
| 21 | <span className="text-xs text-muted-foreground/50 wco:pr-[var(--workspace-native-controls-inset)]"> | 12 | <span className="text-xs text-muted-foreground/50">No active thread</span> |
| 22 | No active thread | ||
| 23 | </span> | ||
| 24 | ) : ( | 13 | ) : ( |
| 25 | <div className="flex items-center gap-2"> | 14 | <div className="flex items-center gap-2"> |
| 26 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> | 15 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> |
| ... | @@ -28,7 +17,7 @@ export function NoActiveThreadState() { | ... | @@ -28,7 +17,7 @@ export function NoActiveThreadState() { |
| 28 | </span> | 17 | </span> |
| 29 | </div> | 18 | </div> |
| 30 | )} | 19 | )} |
| 31 | </header> | 20 | </WorkspacePageHeader> |
| 32 | 21 | ||
| 33 | <Empty className="flex-1"> | 22 | <Empty className="flex-1"> |
| 34 | <div className="w-full max-w-lg px-8 py-12"> | 23 | <div className="w-full max-w-lg px-8 py-12"> |
apps/web/src/components/WorkspacePageHeader.tsx created+28| ... | @@ -0,0 +1,28 @@ | ||
| 1 | import type { ComponentPropsWithoutRef } from "react"; | ||
| 2 | |||
| 3 | import { cn } from "../lib/utils"; | ||
| 4 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../workspaceTitlebar"; | ||
| 5 | |||
| 6 | /** Shared workspace top-bar geometry. */ | ||
| 7 | export function WorkspacePageHeader({ | ||
| 8 | electron = false, | ||
| 9 | reserveNativeControls = electron, | ||
| 10 | className, | ||
| 11 | ...props | ||
| 12 | }: ComponentPropsWithoutRef<"header"> & { | ||
| 13 | readonly electron?: boolean; | ||
| 14 | readonly reserveNativeControls?: boolean; | ||
| 15 | }) { | ||
| 16 | return ( | ||
| 17 | <header | ||
| 18 | className={cn( | ||
| 19 | "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center gap-3 pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]", | ||
| 20 | electron && "drag-region", | ||
| 21 | reserveNativeControls && "wco:pr-[var(--workspace-native-controls-inset)]", | ||
| 22 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 23 | className, | ||
| 24 | )} | ||
| 25 | {...props} | ||
| 26 | /> | ||
| 27 | ); | ||
| 28 | } | ||
apps/web/src/components/settings/DiagnosticsSettings.tsx+1-1| ... | @@ -992,7 +992,7 @@ export function DiagnosticsSettingsPanel() { | ... | @@ -992,7 +992,7 @@ export function DiagnosticsSettingsPanel() { |
| 992 | : false; | 992 | : false; |
| 993 | 993 | ||
| 994 | return ( | 994 | return ( |
| 995 | <SettingsPageContainer className="max-w-6xl gap-10"> | 995 | <SettingsPageContainer width="expanded" className="gap-10"> |
| 996 | <ResourceTelemetryDiagnostics /> | 996 | <ResourceTelemetryDiagnostics /> |
| 997 | 997 | ||
| 998 | <SettingsSection | 998 | <SettingsSection |
apps/web/src/components/settings/KeybindingsSettings.tsx+1-1| ... | @@ -1195,7 +1195,7 @@ export function KeybindingsSettingsPanel() { | ... | @@ -1195,7 +1195,7 @@ export function KeybindingsSettingsPanel() { |
| 1195 | ); | 1195 | ); |
| 1196 | 1196 | ||
| 1197 | return ( | 1197 | return ( |
| 1198 | <SettingsPageContainer className="max-w-5xl"> | 1198 | <SettingsPageContainer width="wide"> |
| 1199 | <SettingsSection | 1199 | <SettingsSection |
| 1200 | {...searchableSetting("keybindings")} | 1200 | {...searchableSetting("keybindings")} |
| 1201 | headerAction={ | 1201 | headerAction={ |
apps/web/src/components/settings/ProjectSettingsPanel.tsx+4-22| ... | @@ -81,8 +81,6 @@ import { | ... | @@ -81,8 +81,6 @@ import { |
| 81 | type NewProjectScriptInput, | 81 | type NewProjectScriptInput, |
| 82 | type ProjectScriptEditorRequest, | 82 | type ProjectScriptEditorRequest, |
| 83 | } from "../projectScriptEditor"; | 83 | } from "../projectScriptEditor"; |
| 84 | import { cn } from "../../lib/utils"; | ||
| 85 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; | ||
| 86 | import { Button } from "../ui/button"; | 84 | import { Button } from "../ui/button"; |
| 87 | import { Input } from "../ui/input"; | 85 | import { Input } from "../ui/input"; |
| 88 | import { | 86 | import { |
| ... | @@ -103,6 +101,7 @@ import { | ... | @@ -103,6 +101,7 @@ import { |
| 103 | WorkspaceBreadcrumbItem, | 101 | WorkspaceBreadcrumbItem, |
| 104 | WorkspaceBreadcrumbSeparator, | 102 | WorkspaceBreadcrumbSeparator, |
| 105 | } from "../WorkspaceBreadcrumb"; | 103 | } from "../WorkspaceBreadcrumb"; |
| 104 | import { WorkspacePageHeader } from "../WorkspacePageHeader"; | ||
| 106 | import { | 105 | import { |
| 107 | SettingResetButton, | 106 | SettingResetButton, |
| 108 | SettingsPageContainer, | 107 | SettingsPageContainer, |
| ... | @@ -175,26 +174,9 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) { | ... | @@ -175,26 +174,9 @@ export function ProjectSettingsPage({ projectKey }: { projectKey: string }) { |
| 175 | return ( | 174 | return ( |
| 176 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"> | 175 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"> |
| 177 | <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground"> | 176 | <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground"> |
| 178 | {!isElectron && ( | 177 | <WorkspacePageHeader electron={isElectron}> |
| 179 | <header | 178 | <ProjectSettingsBreadcrumb projectKey={projectKey} /> |
| 180 | className={cn( | 179 | </WorkspacePageHeader> |
| 181 | "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", | ||
| 182 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 183 | )} | ||
| 184 | > | ||
| 185 | <ProjectSettingsBreadcrumb projectKey={projectKey} /> | ||
| 186 | </header> | ||
| 187 | )} | ||
| 188 | {isElectron && ( | ||
| 189 | <div | ||
| 190 | className={cn( | ||
| 191 | "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)]", | ||
| 192 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 193 | )} | ||
| 194 | > | ||
| 195 | <ProjectSettingsBreadcrumb projectKey={projectKey} /> | ||
| 196 | </div> | ||
| 197 | )} | ||
| 198 | <ProjectSettingsPanel projectKey={projectKey} /> | 180 | <ProjectSettingsPanel projectKey={projectKey} /> |
| 199 | </div> | 181 | </div> |
| 200 | </SidebarInset> | 182 | </SidebarInset> |
apps/web/src/components/settings/SettingsSidebarNav.tsx+5-22| ... | @@ -9,7 +9,6 @@ import { | ... | @@ -9,7 +9,6 @@ import { |
| 9 | } from "react"; | 9 | } from "react"; |
| 10 | import { | 10 | import { |
| 11 | ArchiveIcon, | 11 | ArchiveIcon, |
| 12 | ArrowLeftIcon, | ||
| 13 | BlocksIcon, | 12 | BlocksIcon, |
| 14 | BotIcon, | 13 | BotIcon, |
| 15 | GitBranchIcon, | 14 | GitBranchIcon, |
| ... | @@ -20,7 +19,7 @@ import { | ... | @@ -20,7 +19,7 @@ import { |
| 20 | Settings2Icon, | 19 | Settings2Icon, |
| 21 | XIcon, | 20 | XIcon, |
| 22 | } from "lucide-react"; | 21 | } from "lucide-react"; |
| 23 | import { useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router"; | 22 | import { useLocation, useNavigate } from "@tanstack/react-router"; |
| 24 | 23 | ||
| 25 | import { Button } from "../ui/button"; | 24 | import { Button } from "../ui/button"; |
| 26 | import { Input } from "../ui/input"; | 25 | import { Input } from "../ui/input"; |
| ... | @@ -35,6 +34,7 @@ import { | ... | @@ -35,6 +34,7 @@ import { |
| 35 | useSidebar, | 34 | useSidebar, |
| 36 | } from "../ui/sidebar"; | 35 | } from "../ui/sidebar"; |
| 37 | import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn"; | 36 | import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn"; |
| 37 | import { SidebarUtilityMenu } from "../sidebar/SidebarChrome"; | ||
| 38 | import { scrollToSettingsTarget } from "./settingsLayout"; | 38 | import { scrollToSettingsTarget } from "./settingsLayout"; |
| 39 | import { | 39 | import { |
| 40 | searchSettings, | 40 | searchSettings, |
| ... | @@ -74,7 +74,6 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) { | ... | @@ -74,7 +74,6 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) { |
| 74 | export function SettingsSidebarNav({ pathname }: { pathname: string }) { | 74 | export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 75 | const navigate = useNavigate(); | 75 | const navigate = useNavigate(); |
| 76 | const currentHash = useLocation({ select: (location) => location.hash }); | 76 | const currentHash = useLocation({ select: (location) => location.hash }); |
| 77 | const canGoBack = useCanGoBack(); | ||
| 78 | const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); | 77 | const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); |
| 79 | const searchInputRef = useRef<HTMLInputElement>(null); | 78 | const searchInputRef = useRef<HTMLInputElement>(null); |
| 80 | const [query, setQuery] = useState(""); | 79 | const [query, setQuery] = useState(""); |
| ... | @@ -178,17 +177,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -178,17 +177,6 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 178 | }, | 177 | }, |
| 179 | [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results], | 178 | [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results], |
| 180 | ); | 179 | ); |
| 181 | const handleBackClick = useCallback(() => { | ||
| 182 | if (isMobile) { | ||
| 183 | setOpenMobile(false); | ||
| 184 | } | ||
| 185 | if (canGoBack) { | ||
| 186 | window.history.back(); | ||
| 187 | return; | ||
| 188 | } | ||
| 189 | void navigate({ to: "/" }); | ||
| 190 | }, [canGoBack, isMobile, navigate, setOpenMobile]); | ||
| 191 | |||
| 192 | return ( | 180 | return ( |
| 193 | <> | 181 | <> |
| 194 | <SidebarContent className="overflow-x-hidden"> | 182 | <SidebarContent className="overflow-x-hidden"> |
| ... | @@ -298,14 +286,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -298,14 +286,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 298 | <SidebarFooter className="p-[var(--sidebar-content-inset)]"> | 286 | <SidebarFooter className="p-[var(--sidebar-content-inset)]"> |
| 299 | <T3ConnectSidebarSignIn /> | 287 | <T3ConnectSidebarSignIn /> |
| 300 | <div className="flex items-center gap-1"> | 288 | <div className="flex items-center gap-1"> |
| 301 | <SidebarMenu className="min-w-0 flex-1"> | 289 | <div className="min-w-0 flex-1"> |
| 302 | <SidebarMenuItem> | 290 | <SidebarUtilityMenu /> |
| 303 | <SidebarMenuButton onClick={handleBackClick}> | 291 | </div> |
| 304 | <ArrowLeftIcon /> | ||
| 305 | <span>Back</span> | ||
| 306 | </SidebarMenuButton> | ||
| 307 | </SidebarMenuItem> | ||
| 308 | </SidebarMenu> | ||
| 309 | <T3ConnectSidebarAvatar /> | 292 | <T3ConnectSidebarAvatar /> |
| 310 | </div> | 293 | </div> |
| 311 | </SidebarFooter> | 294 | </SidebarFooter> |
apps/web/src/components/settings/settingsLayout.tsx+18-2| ... | @@ -15,6 +15,14 @@ import { cn } from "../../lib/utils"; | ... | @@ -15,6 +15,14 @@ import { cn } from "../../lib/utils"; |
| 15 | import { Button } from "../ui/button"; | 15 | import { Button } from "../ui/button"; |
| 16 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 16 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 17 | 17 | ||
| 18 | type SettingsPageWidth = "readable" | "wide" | "expanded"; | ||
| 19 | |||
| 20 | const SETTINGS_PAGE_WIDTH_CLASS: Record<SettingsPageWidth, string> = { | ||
| 21 | readable: "max-w-4xl", | ||
| 22 | wide: "max-w-5xl", | ||
| 23 | expanded: "max-w-6xl", | ||
| 24 | }; | ||
| 25 | |||
| 18 | interface SettingsSearchTargetContextValue { | 26 | interface SettingsSearchTargetContextValue { |
| 19 | readonly targetId: string | null; | 27 | readonly targetId: string | null; |
| 20 | readonly onTargetHandled: () => void; | 28 | readonly onTargetHandled: () => void; |
| ... | @@ -233,9 +241,11 @@ export function SettingResetButton({ | ... | @@ -233,9 +241,11 @@ export function SettingResetButton({ |
| 233 | export function SettingsPageContainer({ | 241 | export function SettingsPageContainer({ |
| 234 | children, | 242 | children, |
| 235 | className, | 243 | className, |
| 244 | width = "readable", | ||
| 236 | }: { | 245 | }: { |
| 237 | children: ReactNode; | 246 | children: ReactNode; |
| 238 | className?: string; | 247 | className?: string; |
| 248 | width?: SettingsPageWidth; | ||
| 239 | }) { | 249 | }) { |
| 240 | const navigate = useNavigate(); | 250 | const navigate = useNavigate(); |
| 241 | const hash = useLocation({ select: (location) => location.hash }); | 251 | const hash = useLocation({ select: (location) => location.hash }); |
| ... | @@ -247,10 +257,16 @@ export function SettingsPageContainer({ | ... | @@ -247,10 +257,16 @@ export function SettingsPageContainer({ |
| 247 | return ( | 257 | return ( |
| 248 | <SettingsSearchTargetProvider targetId={targetId} onTargetHandled={clearTargetHash}> | 258 | <SettingsSearchTargetProvider targetId={targetId} onTargetHandled={clearTargetHash}> |
| 249 | <div | 259 | <div |
| 250 | className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto px-4 pt-10 pb-7 sm:px-8 sm:pt-12 sm:pb-10" | 260 | className="topbar-scroll-fade scrollbar-gutter-both flex-1 overflow-y-auto [--topbar-scroll-fade-height:1.5rem] sm:[--topbar-scroll-fade-height:1.5rem]" |
| 251 | data-settings-page-scroll | 261 | data-settings-page-scroll |
| 252 | > | 262 | > |
| 253 | <div className={cn("mx-auto flex w-full max-w-4xl flex-col gap-12", className)}> | 263 | <div |
| 264 | className={cn( | ||
| 265 | "mx-auto flex w-full flex-col gap-12 px-5 pt-6 pb-12 sm:px-6", | ||
| 266 | SETTINGS_PAGE_WIDTH_CLASS[width], | ||
| 267 | className, | ||
| 268 | )} | ||
| 269 | > | ||
| 254 | {children} | 270 | {children} |
| 255 | </div> | 271 | </div> |
| 256 | </div> | 272 | </div> |
apps/web/src/components/sidebar/SidebarChrome.tsx+78-68| ... | @@ -4,8 +4,9 @@ import { | ... | @@ -4,8 +4,9 @@ import { |
| 4 | GitPullRequestIcon, | 4 | GitPullRequestIcon, |
| 5 | SettingsIcon, | 5 | SettingsIcon, |
| 6 | } from "lucide-react"; | 6 | } from "lucide-react"; |
| 7 | import type { ReactNode } from "react"; | ||
| 7 | import { memo, useCallback } from "react"; | 8 | import { memo, useCallback } from "react"; |
| 8 | import { Link, useLocation, useNavigate } from "@tanstack/react-router"; | 9 | import { Link, useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router"; |
| 9 | 10 | ||
| 10 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; | 11 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; |
| 11 | import { cn } from "../../lib/utils"; | 12 | import { cn } from "../../lib/utils"; |
| ... | @@ -117,16 +118,44 @@ function T3Wordmark() { | ... | @@ -117,16 +118,44 @@ function T3Wordmark() { |
| 117 | ); | 118 | ); |
| 118 | } | 119 | } |
| 119 | 120 | ||
| 120 | export const SidebarChromeFooter = memo(function SidebarChromeFooter() { | 121 | function SidebarUtilityItem({ |
| 122 | icon, | ||
| 123 | label, | ||
| 124 | onClick, | ||
| 125 | }: { | ||
| 126 | icon: ReactNode; | ||
| 127 | label: string; | ||
| 128 | onClick: () => void; | ||
| 129 | }) { | ||
| 130 | return ( | ||
| 131 | <SidebarMenuItem className="shrink-0"> | ||
| 132 | <Tooltip> | ||
| 133 | <TooltipTrigger | ||
| 134 | render={ | ||
| 135 | <SidebarMenuButton aria-label={label} onClick={onClick} size="icon"> | ||
| 136 | {icon} | ||
| 137 | </SidebarMenuButton> | ||
| 138 | } | ||
| 139 | /> | ||
| 140 | <TooltipPopup side="top">{label}</TooltipPopup> | ||
| 141 | </Tooltip> | ||
| 142 | </SidebarMenuItem> | ||
| 143 | ); | ||
| 144 | } | ||
| 145 | |||
| 146 | export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { | ||
| 121 | const navigate = useNavigate(); | 147 | const navigate = useNavigate(); |
| 148 | const canGoBack = useCanGoBack(); | ||
| 122 | const { isMobile, setOpenMobile } = useSidebar(); | 149 | const { isMobile, setOpenMobile } = useSidebar(); |
| 123 | const currentFooterPage = useLocation({ | 150 | const currentFooterPage = useLocation({ |
| 124 | select: (location) => | 151 | select: (location) => |
| 125 | location.pathname === "/usage" | 152 | /^\/settings(?:\/|$)/.test(location.pathname) |
| 126 | ? "usage" | 153 | ? "settings" |
| 127 | : location.pathname === "/pull-requests" | 154 | : location.pathname === "/usage" |
| 128 | ? "pull-requests" | 155 | ? "usage" |
| 129 | : null, | 156 | : location.pathname === "/pull-requests" |
| 157 | ? "pull-requests" | ||
| 158 | : null, | ||
| 130 | }); | 159 | }); |
| 131 | const { environments } = useEnvironments(); | 160 | const { environments } = useEnvironments(); |
| 132 | // The page reads every connected server, so one of them offering pull requests is enough for | 161 | // The page reads every connected server, so one of them offering pull requests is enough for |
| ... | @@ -157,73 +186,54 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() { | ... | @@ -157,73 +186,54 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() { |
| 157 | 186 | ||
| 158 | const handleBackClick = useCallback(() => { | 187 | const handleBackClick = useCallback(() => { |
| 159 | closeMobileSidebar(); | 188 | closeMobileSidebar(); |
| 189 | if (canGoBack) { | ||
| 190 | window.history.back(); | ||
| 191 | return; | ||
| 192 | } | ||
| 160 | void navigate({ to: "/" }); | 193 | void navigate({ to: "/" }); |
| 161 | }, [closeMobileSidebar, navigate]); | 194 | }, [canGoBack, closeMobileSidebar, navigate]); |
| 162 | 195 | ||
| 196 | return ( | ||
| 197 | <SidebarMenu className="flex-row items-center"> | ||
| 198 | {currentFooterPage ? ( | ||
| 199 | <SidebarMenuItem className="min-w-0 flex-1"> | ||
| 200 | <SidebarMenuButton onClick={handleBackClick}> | ||
| 201 | <ArrowLeftIcon /> | ||
| 202 | <span>Back</span> | ||
| 203 | </SidebarMenuButton> | ||
| 204 | </SidebarMenuItem> | ||
| 205 | ) : ( | ||
| 206 | <> | ||
| 207 | <SidebarUtilityItem | ||
| 208 | icon={<SettingsIcon />} | ||
| 209 | label="Settings" | ||
| 210 | onClick={handleSettingsClick} | ||
| 211 | /> | ||
| 212 | {pullRequestsSupported ? ( | ||
| 213 | <SidebarUtilityItem | ||
| 214 | icon={<GitPullRequestIcon />} | ||
| 215 | label="Pull Requests" | ||
| 216 | onClick={handlePullRequestsClick} | ||
| 217 | /> | ||
| 218 | ) : null} | ||
| 219 | <SidebarUtilityItem | ||
| 220 | icon={<ChartNoAxesColumnIcon />} | ||
| 221 | label="Usage" | ||
| 222 | onClick={handleUsageClick} | ||
| 223 | /> | ||
| 224 | </> | ||
| 225 | )} | ||
| 226 | <SidebarUpdatePill /> | ||
| 227 | </SidebarMenu> | ||
| 228 | ); | ||
| 229 | }); | ||
| 230 | |||
| 231 | export const SidebarChromeFooter = memo(function SidebarChromeFooter() { | ||
| 163 | return ( | 232 | return ( |
| 164 | <SidebarFooter className="p-[var(--sidebar-content-inset)]"> | 233 | <SidebarFooter className="p-[var(--sidebar-content-inset)]"> |
| 165 | <SidebarProviderUpdatePill /> | 234 | <SidebarProviderUpdatePill /> |
| 166 | <SidebarUpdateArchitectureWarning /> | 235 | <SidebarUpdateArchitectureWarning /> |
| 167 | <SidebarMenu className="flex-row items-center"> | 236 | <SidebarUtilityMenu /> |
| 168 | {currentFooterPage ? ( | ||
| 169 | <SidebarMenuItem className="min-w-0 flex-1"> | ||
| 170 | <SidebarMenuButton onClick={handleBackClick}> | ||
| 171 | <ArrowLeftIcon /> | ||
| 172 | <span>Back</span> | ||
| 173 | </SidebarMenuButton> | ||
| 174 | </SidebarMenuItem> | ||
| 175 | ) : ( | ||
| 176 | <> | ||
| 177 | <SidebarMenuItem className="shrink-0"> | ||
| 178 | <Tooltip> | ||
| 179 | <TooltipTrigger | ||
| 180 | render={ | ||
| 181 | <SidebarMenuButton | ||
| 182 | aria-label="Settings" | ||
| 183 | onClick={handleSettingsClick} | ||
| 184 | size="icon" | ||
| 185 | > | ||
| 186 | <SettingsIcon /> | ||
| 187 | </SidebarMenuButton> | ||
| 188 | } | ||
| 189 | /> | ||
| 190 | <TooltipPopup side="top">Settings</TooltipPopup> | ||
| 191 | </Tooltip> | ||
| 192 | </SidebarMenuItem> | ||
| 193 | {pullRequestsSupported ? ( | ||
| 194 | <SidebarMenuItem className="shrink-0"> | ||
| 195 | <Tooltip> | ||
| 196 | <TooltipTrigger | ||
| 197 | render={ | ||
| 198 | <SidebarMenuButton | ||
| 199 | aria-label="Pull Requests" | ||
| 200 | onClick={handlePullRequestsClick} | ||
| 201 | size="icon" | ||
| 202 | > | ||
| 203 | <GitPullRequestIcon /> | ||
| 204 | </SidebarMenuButton> | ||
| 205 | } | ||
| 206 | /> | ||
| 207 | <TooltipPopup side="top">Pull Requests</TooltipPopup> | ||
| 208 | </Tooltip> | ||
| 209 | </SidebarMenuItem> | ||
| 210 | ) : null} | ||
| 211 | <SidebarMenuItem className="shrink-0"> | ||
| 212 | <Tooltip> | ||
| 213 | <TooltipTrigger | ||
| 214 | render={ | ||
| 215 | <SidebarMenuButton aria-label="Usage" onClick={handleUsageClick} size="icon"> | ||
| 216 | <ChartNoAxesColumnIcon /> | ||
| 217 | </SidebarMenuButton> | ||
| 218 | } | ||
| 219 | /> | ||
| 220 | <TooltipPopup side="top">Usage</TooltipPopup> | ||
| 221 | </Tooltip> | ||
| 222 | </SidebarMenuItem> | ||
| 223 | </> | ||
| 224 | )} | ||
| 225 | <SidebarUpdatePill /> | ||
| 226 | </SidebarMenu> | ||
| 227 | </SidebarFooter> | 237 | </SidebarFooter> |
| 228 | ); | 238 | ); |
| 229 | }); | 239 | }); |
apps/web/src/routes/-chatIndexTitlebar.test.ts+9-3| ... | @@ -15,9 +15,15 @@ describe("hosted static onboarding header", () => { | ... | @@ -15,9 +15,15 @@ describe("hosted static onboarding header", () => { |
| 15 | 15 | ||
| 16 | const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd); | 16 | const onboardingHeader = routeSource.slice(onboardingStart, onboardingEnd); |
| 17 | 17 | ||
| 18 | expect(onboardingHeader).toContain("h-[var(--workspace-topbar-height)]"); | 18 | expect(onboardingHeader).toContain("<WorkspacePageHeader"); |
| 19 | expect(onboardingHeader).toContain("min-h-[var(--workspace-topbar-height)]"); | ||
| 20 | expect(onboardingHeader).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS"); | ||
| 21 | expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/); | 19 | expect(onboardingHeader).not.toMatch(/(?:^|\s)(?:[\w-]+:)*py-/); |
| 20 | |||
| 21 | const headerSource = NodeFS.readFileSync( | ||
| 22 | new URL("../components/WorkspacePageHeader.tsx", import.meta.url), | ||
| 23 | "utf8", | ||
| 24 | ); | ||
| 25 | expect(headerSource).toContain("h-[var(--workspace-topbar-height)]"); | ||
| 26 | expect(headerSource).toContain("min-h-[var(--workspace-topbar-height)]"); | ||
| 27 | expect(headerSource).toContain("COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS"); | ||
| 22 | }); | 28 | }); |
| 23 | }); | 29 | }); |
apps/web/src/routes/_chat.index.tsx+3-9| ... | @@ -8,6 +8,7 @@ import { sortScopedProjectsForSidebar } from "../components/Sidebar.logic"; | ... | @@ -8,6 +8,7 @@ import { sortScopedProjectsForSidebar } from "../components/Sidebar.logic"; |
| 8 | import { Button } from "../components/ui/button"; | 8 | import { Button } from "../components/ui/button"; |
| 9 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty"; | 9 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty"; |
| 10 | import { SidebarInset } from "../components/ui/sidebar"; | 10 | import { SidebarInset } from "../components/ui/sidebar"; |
| 11 | import { WorkspacePageHeader } from "../components/WorkspacePageHeader"; | ||
| 11 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; | 12 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; |
| 12 | import { | 13 | import { |
| 13 | useAllEnvironmentShellsBootstrapped, | 14 | useAllEnvironmentShellsBootstrapped, |
| ... | @@ -17,8 +18,6 @@ import { | ... | @@ -17,8 +18,6 @@ import { |
| 17 | import { useEnvironments } from "../state/environments"; | 18 | import { useEnvironments } from "../state/environments"; |
| 18 | import { APP_DISPLAY_NAME } from "~/branding"; | 19 | import { APP_DISPLAY_NAME } from "~/branding"; |
| 19 | import { hasCloudPublicConfig } from "~/cloud/publicConfig"; | 20 | import { hasCloudPublicConfig } from "~/cloud/publicConfig"; |
| 20 | import { cn } from "~/lib/utils"; | ||
| 21 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 22 | 21 | ||
| 23 | function ChatIndexRouteView() { | 22 | function ChatIndexRouteView() { |
| 24 | const { authGateState } = Route.useRouteContext(); | 23 | const { authGateState } = Route.useRouteContext(); |
| ... | @@ -143,18 +142,13 @@ function HostedStaticOnboardingState() { | ... | @@ -143,18 +142,13 @@ function HostedStaticOnboardingState() { |
| 143 | return ( | 142 | return ( |
| 144 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 143 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> |
| 145 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> | 144 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> |
| 146 | <header | 145 | <WorkspacePageHeader className="border-b border-border"> |
| 147 | className={cn( | ||
| 148 | "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", | ||
| 149 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 150 | )} | ||
| 151 | > | ||
| 152 | <div className="flex items-center gap-2"> | 146 | <div className="flex items-center gap-2"> |
| 153 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> | 147 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> |
| 154 | {APP_DISPLAY_NAME} | 148 | {APP_DISPLAY_NAME} |
| 155 | </span> | 149 | </span> |
| 156 | </div> | 150 | </div> |
| 157 | </header> | 151 | </WorkspacePageHeader> |
| 158 | 152 | ||
| 159 | <Empty className="flex-1"> | 153 | <Empty className="flex-1"> |
| 160 | <div className="w-full max-w-xl rounded-3xl border border-border/55 bg-card/20 px-8 py-12 shadow-sm/5"> | 154 | <div className="w-full max-w-xl rounded-3xl border border-border/55 bg-card/20 px-8 py-12 shadow-sm/5"> |
apps/web/src/routes/settings.tsx+10-36| ... | @@ -13,9 +13,8 @@ import { useSettingsRestore } from "../components/settings/SettingsPanels"; | ... | @@ -13,9 +13,8 @@ import { useSettingsRestore } from "../components/settings/SettingsPanels"; |
| 13 | import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb"; | 13 | import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb"; |
| 14 | import { Button } from "../components/ui/button"; | 14 | import { Button } from "../components/ui/button"; |
| 15 | import { SidebarInset } from "../components/ui/sidebar"; | 15 | import { SidebarInset } from "../components/ui/sidebar"; |
| 16 | import { WorkspacePageHeader } from "../components/WorkspacePageHeader"; | ||
| 16 | import { isElectron } from "../env"; | 17 | import { isElectron } from "../env"; |
| 17 | import { cn } from "~/lib/utils"; | ||
| 18 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 19 | 18 | ||
| 20 | function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) { | 19 | function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) { |
| 21 | const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored); | 20 | const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored); |
| ... | @@ -72,41 +71,16 @@ function SettingsContentLayout() { | ... | @@ -72,41 +71,16 @@ function SettingsContentLayout() { |
| 72 | return ( | 71 | return ( |
| 73 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"> | 72 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"> |
| 74 | <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground"> | 73 | <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground"> |
| 75 | {!isElectron && ( | 74 | <WorkspacePageHeader electron={isElectron}> |
| 76 | <header | 75 | <div className="flex w-full items-center gap-3"> |
| 77 | className={cn( | 76 | <SettingsBreadcrumb pathname={location.pathname} /> |
| 78 | "flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", | 77 | {showRestoreDefaults ? ( |
| 79 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | 78 | <div className="ms-auto flex items-center gap-2"> |
| 80 | )} | 79 | <RestoreDefaultsButton onRestored={handleRestored} /> |
| 81 | > | 80 | </div> |
| 82 | <div className="flex w-full items-center gap-2"> | 81 | ) : null} |
| 83 | <SettingsBreadcrumb pathname={location.pathname} /> | ||
| 84 | {showRestoreDefaults ? ( | ||
| 85 | <div className="ms-auto flex items-center gap-2"> | ||
| 86 | <RestoreDefaultsButton onRestored={handleRestored} /> | ||
| 87 | </div> | ||
| 88 | ) : null} | ||
| 89 | </div> | ||
| 90 | </header> | ||
| 91 | )} | ||
| 92 | |||
| 93 | {isElectron && ( | ||
| 94 | <div | ||
| 95 | className={cn( | ||
| 96 | "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)]", | ||
| 97 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 98 | )} | ||
| 99 | > | ||
| 100 | <div className="flex w-full items-center gap-2"> | ||
| 101 | <SettingsBreadcrumb pathname={location.pathname} /> | ||
| 102 | {showRestoreDefaults ? ( | ||
| 103 | <div className="ms-auto flex items-center gap-2"> | ||
| 104 | <RestoreDefaultsButton onRestored={handleRestored} /> | ||
| 105 | </div> | ||
| 106 | ) : null} | ||
| 107 | </div> | ||
| 108 | </div> | 82 | </div> |
| 109 | )} | 83 | </WorkspacePageHeader> |
| 110 | 84 | ||
| 111 | <div key={restoreSignal} className="min-h-0 flex flex-1 flex-col"> | 85 | <div key={restoreSignal} className="min-h-0 flex flex-1 flex-col"> |
| 112 | <Outlet /> | 86 | <Outlet /> |