| author | |
| committer | |
| log | b2d17b7108766a989374d670ff12de39b6c4d033 |
| tree | c555eb0670a5cbff9244901e35f9e9be88a9c033 |
| parent | fb1034546f989c11568ef1c4b5801e6a5372a07d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: codex <codex@users.noreply.github.com>17 files changed, 247 insertions(+), 74 deletions(-)
apps/server/src/keybindings.test.ts+1| ... | @@ -198,6 +198,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { | ... | @@ -198,6 +198,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 198 | assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1"); | 198 | assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1"); |
| 199 | assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9"); | 199 | assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9"); |
| 200 | assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m"); | 200 | assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m"); |
| 201 | assert.equal(defaultsByCommand.get("sidebar.toggle"), "mod+b"); | ||
| 201 | assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b"); | 202 | assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b"); |
| 202 | assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d"); | 203 | assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d"); |
| 203 | assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1"); | 204 | assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1"); |
apps/web/src/components/AppSidebarLayout.tsx+54-3| ... | @@ -1,14 +1,64 @@ | ... | @@ -1,14 +1,64 @@ |
| 1 | import { useEffect, type ReactNode } from "react"; | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { useEffect, type CSSProperties, type ReactNode } from "react"; | ||
| 2 | import { useNavigate } from "@tanstack/react-router"; | 3 | import { useNavigate } from "@tanstack/react-router"; |
| 3 | 4 | ||
| 5 | import { isElectron } from "../env"; | ||
| 6 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; | ||
| 7 | import { isMacPlatform } from "../lib/utils"; | ||
| 8 | import { primaryServerKeybindingsAtom } from "../state/server"; | ||
| 4 | import ThreadSidebar from "./Sidebar"; | 9 | import ThreadSidebar from "./Sidebar"; |
| 5 | import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar"; | 10 | import { Sidebar, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "./ui/sidebar"; |
| 11 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | ||
| 6 | 12 | ||
| 7 | const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; | 13 | const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; |
| 8 | const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16; | 14 | const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16; |
| 9 | const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16; | 15 | const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16; |
| 16 | const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; | ||
| 17 | |||
| 18 | function SidebarControl() { | ||
| 19 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); | ||
| 20 | const { toggleSidebar } = useSidebar(); | ||
| 21 | const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); | ||
| 22 | |||
| 23 | useEffect(() => { | ||
| 24 | const onKeyDown = (event: KeyboardEvent) => { | ||
| 25 | if (event.defaultPrevented) return; | ||
| 26 | if (resolveShortcutCommand(event, keybindings) !== "sidebar.toggle") return; | ||
| 27 | |||
| 28 | event.preventDefault(); | ||
| 29 | event.stopPropagation(); | ||
| 30 | toggleSidebar(); | ||
| 31 | }; | ||
| 32 | |||
| 33 | window.addEventListener("keydown", onKeyDown); | ||
| 34 | return () => window.removeEventListener("keydown", onKeyDown); | ||
| 35 | }, [keybindings, toggleSidebar]); | ||
| 36 | |||
| 37 | return ( | ||
| 38 | <div | ||
| 39 | className="pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 flex h-[var(--workspace-topbar-height)] items-center" | ||
| 40 | data-sidebar-control="" | ||
| 41 | > | ||
| 42 | <Tooltip> | ||
| 43 | <TooltipTrigger | ||
| 44 | render={ | ||
| 45 | <SidebarTrigger className="pointer-events-auto" aria-label="Toggle main sidebar" /> | ||
| 46 | } | ||
| 47 | /> | ||
| 48 | <TooltipPopup side="bottom"> | ||
| 49 | Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""} | ||
| 50 | </TooltipPopup> | ||
| 51 | </Tooltip> | ||
| 52 | </div> | ||
| 53 | ); | ||
| 54 | } | ||
| 55 | |||
| 10 | export function AppSidebarLayout({ children }: { children: ReactNode }) { | 56 | export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 11 | const navigate = useNavigate(); | 57 | const navigate = useNavigate(); |
| 58 | const macosWindowControlsStyle = | ||
| 59 | isElectron && isMacPlatform(navigator.platform) | ||
| 60 | ? ({ "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } as CSSProperties) | ||
| 61 | : undefined; | ||
| 12 | 62 | ||
| 13 | useEffect(() => { | 63 | useEffect(() => { |
| 14 | const onMenuAction = window.desktopBridge?.onMenuAction; | 64 | const onMenuAction = window.desktopBridge?.onMenuAction; |
| ... | @@ -28,7 +78,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { | ... | @@ -28,7 +78,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 28 | }, [navigate]); | 78 | }, [navigate]); |
| 29 | 79 | ||
| 30 | return ( | 80 | return ( |
| 31 | <SidebarProvider className="h-dvh! min-h-0!" defaultOpen> | 81 | <SidebarProvider className="h-dvh! min-h-0!" defaultOpen style={macosWindowControlsStyle}> |
| 32 | <Sidebar | 82 | <Sidebar |
| 33 | side="left" | 83 | side="left" |
| 34 | collapsible="offcanvas" | 84 | collapsible="offcanvas" |
| ... | @@ -44,6 +94,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { | ... | @@ -44,6 +94,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 44 | <SidebarRail /> | 94 | <SidebarRail /> |
| 45 | </Sidebar> | 95 | </Sidebar> |
| 46 | {children} | 96 | {children} |
| 97 | <SidebarControl /> | ||
| 47 | </SidebarProvider> | 98 | </SidebarProvider> |
| 48 | ); | 99 | ); |
| 49 | } | 100 | } |
apps/web/src/components/ChatView.tsx+3-1| ... | @@ -128,6 +128,7 @@ import PlanSidebar from "./PlanSidebar"; | ... | @@ -128,6 +128,7 @@ import PlanSidebar from "./PlanSidebar"; |
| 128 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; | 128 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 129 | import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; | 129 | import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; |
| 130 | import { cn, randomHex } from "~/lib/utils"; | 130 | import { cn, randomHex } from "~/lib/utils"; |
| 131 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 131 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 132 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 132 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; | 133 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; |
| 133 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; | 134 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; |
| ... | @@ -4677,7 +4678,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4677,7 +4678,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 4677 | <header | 4678 | <header |
| 4678 | data-chat-header | 4679 | data-chat-header |
| 4679 | className={cn( | 4680 | className={cn( |
| 4680 | "border-b border-border", | 4681 | "border-b border-border transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none", |
| 4681 | isElectron | 4682 | isElectron |
| 4682 | ? cn( | 4683 | ? cn( |
| 4683 | "workspace-topbar drag-region relative px-3 sm:px-5", | 4684 | "workspace-topbar drag-region relative px-3 sm:px-5", |
| ... | @@ -4686,6 +4687,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4686,6 +4687,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 4686 | "wco:pr-[var(--workspace-native-controls-inset)]", | 4687 | "wco:pr-[var(--workspace-native-controls-inset)]", |
| 4687 | ) | 4688 | ) |
| 4688 | : "workspace-topbar 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)]", | 4689 | : "workspace-topbar 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)]", |
| 4690 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 4689 | )} | 4691 | )} |
| 4690 | > | 4692 | > |
| 4691 | {!rightPanelOpen ? panelLayoutControls : null} | 4693 | {!rightPanelOpen ? panelLayoutControls : null} |
apps/web/src/components/NoActiveThreadState.tsx+4-3| ... | @@ -1,7 +1,8 @@ | ... | @@ -1,7 +1,8 @@ |
| 1 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty"; | 1 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty"; |
| 2 | import { SidebarInset, SidebarTrigger } 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 { cn } from "~/lib/utils"; |
| 5 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 5 | 6 | ||
| 6 | export function NoActiveThreadState() { | 7 | export function NoActiveThreadState() { |
| 7 | return ( | 8 | return ( |
| ... | @@ -9,8 +10,9 @@ export function NoActiveThreadState() { | ... | @@ -9,8 +10,9 @@ export function NoActiveThreadState() { |
| 9 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> | 10 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> |
| 10 | <header | 11 | <header |
| 11 | className={cn( | 12 | className={cn( |
| 12 | "border-b border-border px-3 sm:px-5", | 13 | "border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", |
| 13 | isElectron ? "workspace-topbar drag-region" : "workspace-topbar", | 14 | isElectron ? "workspace-topbar drag-region" : "workspace-topbar", |
| 15 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 14 | )} | 16 | )} |
| 15 | > | 17 | > |
| 16 | {isElectron ? ( | 18 | {isElectron ? ( |
| ... | @@ -19,7 +21,6 @@ export function NoActiveThreadState() { | ... | @@ -19,7 +21,6 @@ export function NoActiveThreadState() { |
| 19 | </span> | 21 | </span> |
| 20 | ) : ( | 22 | ) : ( |
| 21 | <div className="flex items-center gap-2"> | 23 | <div className="flex items-center gap-2"> |
| 22 | <SidebarTrigger className="size-7 shrink-0 md:hidden" /> | ||
| 23 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> | 24 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> |
| 24 | No active thread | 25 | No active thread |
| 25 | </span> | 26 | </span> |
apps/web/src/components/Sidebar.tsx+53-53| ... | @@ -70,7 +70,7 @@ import { | ... | @@ -70,7 +70,7 @@ import { |
| 70 | type SidebarThreadSortOrder, | 70 | type SidebarThreadSortOrder, |
| 71 | } from "@t3tools/contracts/settings"; | 71 | } from "@t3tools/contracts/settings"; |
| 72 | import { isElectron } from "../env"; | 72 | import { isElectron } from "../env"; |
| 73 | import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; | 73 | import { APP_STAGE_LABEL } from "../branding"; |
| 74 | import { useOpenPrLink } from "../lib/openPullRequestLink"; | 74 | import { useOpenPrLink } from "../lib/openPullRequestLink"; |
| 75 | import { isTerminalFocused } from "../lib/terminalFocus"; | 75 | import { isTerminalFocused } from "../lib/terminalFocus"; |
| 76 | import { isMacPlatform } from "../lib/utils"; | 76 | import { isMacPlatform } from "../lib/utils"; |
| ... | @@ -187,12 +187,12 @@ import { | ... | @@ -187,12 +187,12 @@ import { |
| 187 | resolveProjectStatusIndicator, | 187 | resolveProjectStatusIndicator, |
| 188 | resolveSidebarNewThreadSeedContext, | 188 | resolveSidebarNewThreadSeedContext, |
| 189 | resolveSidebarNewThreadEnvMode, | 189 | resolveSidebarNewThreadEnvMode, |
| 190 | resolveSidebarStageBadgeLabel, | ||
| 190 | resolveThreadRowClassName, | 191 | resolveThreadRowClassName, |
| 191 | resolveThreadStatusPill, | 192 | resolveThreadStatusPill, |
| 192 | orderItemsByPreferredIds, | 193 | orderItemsByPreferredIds, |
| 193 | shouldClearThreadSelectionOnMouseDown, | 194 | shouldClearThreadSelectionOnMouseDown, |
| 194 | sortProjectsForSidebar, | 195 | sortProjectsForSidebar, |
| 195 | resolveSidebarStageBadgeLabel, | ||
| 196 | useThreadJumpHintVisibility, | 196 | useThreadJumpHintVisibility, |
| 197 | ThreadStatusPill, | 197 | ThreadStatusPill, |
| 198 | } from "./Sidebar.logic"; | 198 | } from "./Sidebar.logic"; |
| ... | @@ -2452,22 +2452,6 @@ const SidebarProjectListRow = memo(function SidebarProjectListRow(props: Sidebar | ... | @@ -2452,22 +2452,6 @@ const SidebarProjectListRow = memo(function SidebarProjectListRow(props: Sidebar |
| 2452 | ); | 2452 | ); |
| 2453 | }); | 2453 | }); |
| 2454 | 2454 | ||
| 2455 | function T3Wordmark() { | ||
| 2456 | return ( | ||
| 2457 | <svg | ||
| 2458 | aria-label="T3" | ||
| 2459 | className="h-2.5 w-auto shrink-0 text-foreground" | ||
| 2460 | viewBox="15.5309 37 94.3941 56.96" | ||
| 2461 | xmlns="http://www.w3.org/2000/svg" | ||
| 2462 | > | ||
| 2463 | <path | ||
| 2464 | d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" | ||
| 2465 | fill="currentColor" | ||
| 2466 | /> | ||
| 2467 | </svg> | ||
| 2468 | ); | ||
| 2469 | } | ||
| 2470 | |||
| 2471 | type SortableProjectHandleProps = Pick< | 2455 | type SortableProjectHandleProps = Pick< |
| 2472 | ReturnType<typeof useSortable>, | 2456 | ReturnType<typeof useSortable>, |
| 2473 | "attributes" | "listeners" | "setActivatorNodeRef" | 2457 | "attributes" | "listeners" | "setActivatorNodeRef" |
| ... | @@ -2664,48 +2648,64 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ | ... | @@ -2664,48 +2648,64 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 2664 | }: { | 2648 | }: { |
| 2665 | isElectron: boolean; | 2649 | isElectron: boolean; |
| 2666 | }) { | 2650 | }) { |
| 2651 | return isElectron ? ( | ||
| 2652 | <SidebarHeader className="@container/sidebar-header drag-region h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0"> | ||
| 2653 | <SidebarTrigger className="md:hidden" /> | ||
| 2654 | <SidebarBrand /> | ||
| 2655 | </SidebarHeader> | ||
| 2656 | ) : ( | ||
| 2657 | <SidebarHeader className="@container/sidebar-header h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0"> | ||
| 2658 | <SidebarTrigger className="md:hidden" /> | ||
| 2659 | <SidebarBrand /> | ||
| 2660 | </SidebarHeader> | ||
| 2661 | ); | ||
| 2662 | }); | ||
| 2663 | |||
| 2664 | function SidebarBrand() { | ||
| 2665 | const stageLabel = useSidebarStageLabel(); | ||
| 2666 | |||
| 2667 | return ( | ||
| 2668 | <Link | ||
| 2669 | aria-label="Go to threads" | ||
| 2670 | className="sidebar-brand ml-[var(--workspace-titlebar-content-left)] h-7 w-fit min-w-0 shrink-0 items-center gap-1 overflow-hidden rounded-md text-foreground outline-hidden ring-ring focus-visible:ring-2" | ||
| 2671 | to="/" | ||
| 2672 | > | ||
| 2673 | <T3Wordmark /> | ||
| 2674 | <span className="truncate text-sm font-medium tracking-tight text-muted-foreground"> | ||
| 2675 | Code | ||
| 2676 | </span> | ||
| 2677 | <span className="sidebar-brand-stage shrink-0 items-center whitespace-nowrap rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60"> | ||
| 2678 | {stageLabel} | ||
| 2679 | </span> | ||
| 2680 | </Link> | ||
| 2681 | ); | ||
| 2682 | } | ||
| 2683 | |||
| 2684 | function useSidebarStageLabel() { | ||
| 2667 | const primaryServerVersion = | 2685 | const primaryServerVersion = |
| 2668 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; | 2686 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; |
| 2669 | const stageBadgeLabel = resolveSidebarStageBadgeLabel({ | 2687 | |
| 2688 | return resolveSidebarStageBadgeLabel({ | ||
| 2670 | primaryServerVersion, | 2689 | primaryServerVersion, |
| 2671 | fallbackStageLabel: APP_STAGE_LABEL, | 2690 | fallbackStageLabel: APP_STAGE_LABEL, |
| 2672 | }); | 2691 | }); |
| 2673 | const wordmark = ( | 2692 | } |
| 2674 | <div className="flex items-center gap-2"> | ||
| 2675 | <SidebarTrigger className="shrink-0 md:hidden" /> | ||
| 2676 | <Tooltip> | ||
| 2677 | <TooltipTrigger | ||
| 2678 | render={ | ||
| 2679 | <Link | ||
| 2680 | aria-label="Go to threads" | ||
| 2681 | className="ml-1 flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md outline-hidden ring-ring transition-colors hover:text-foreground focus-visible:ring-2" | ||
| 2682 | to="/" | ||
| 2683 | > | ||
| 2684 | <T3Wordmark /> | ||
| 2685 | <span className="truncate text-sm font-medium tracking-tight text-muted-foreground"> | ||
| 2686 | Code | ||
| 2687 | </span> | ||
| 2688 | <span className="rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60"> | ||
| 2689 | {stageBadgeLabel} | ||
| 2690 | </span> | ||
| 2691 | </Link> | ||
| 2692 | } | ||
| 2693 | /> | ||
| 2694 | <TooltipPopup side="bottom" sideOffset={2}> | ||
| 2695 | Version {APP_VERSION} | ||
| 2696 | </TooltipPopup> | ||
| 2697 | </Tooltip> | ||
| 2698 | </div> | ||
| 2699 | ); | ||
| 2700 | 2693 | ||
| 2701 | return isElectron ? ( | 2694 | function T3Wordmark() { |
| 2702 | <SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px] wco:h-[env(titlebar-area-height)] wco:pl-[calc(env(titlebar-area-x)+1em)]"> | 2695 | return ( |
| 2703 | {wordmark} | 2696 | <svg |
| 2704 | </SidebarHeader> | 2697 | aria-label="T3" |
| 2705 | ) : ( | 2698 | className="h-2.5 w-auto shrink-0 text-foreground" |
| 2706 | <SidebarHeader className="gap-3 px-3 py-2 sm:gap-2.5 sm:px-4 sm:py-3">{wordmark}</SidebarHeader> | 2699 | viewBox="15.5309 37 94.3941 56.96" |
| 2700 | xmlns="http://www.w3.org/2000/svg" | ||
| 2701 | > | ||
| 2702 | <path | ||
| 2703 | d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" | ||
| 2704 | fill="currentColor" | ||
| 2705 | /> | ||
| 2706 | </svg> | ||
| 2707 | ); | 2707 | ); |
| 2708 | }); | 2708 | } |
| 2709 | 2709 | ||
| 2710 | const SidebarChromeFooter = memo(function SidebarChromeFooter() { | 2710 | const SidebarChromeFooter = memo(function SidebarChromeFooter() { |
| 2711 | const navigate = useNavigate(); | 2711 | const navigate = useNavigate(); |
apps/web/src/components/chat/ChatHeader.tsx-2| ... | @@ -14,7 +14,6 @@ import ProjectScriptsControl, { | ... | @@ -14,7 +14,6 @@ import ProjectScriptsControl, { |
| 14 | type NewProjectScriptInput, | 14 | type NewProjectScriptInput, |
| 15 | type ProjectScriptActionResult, | 15 | type ProjectScriptActionResult, |
| 16 | } from "../ProjectScriptsControl"; | 16 | } from "../ProjectScriptsControl"; |
| 17 | import { SidebarTrigger } from "../ui/sidebar"; | ||
| 18 | import { OpenInPicker } from "./OpenInPicker"; | 17 | import { OpenInPicker } from "./OpenInPicker"; |
| 19 | import { usePrimaryEnvironmentId } from "../../state/environments"; | 18 | import { usePrimaryEnvironmentId } from "../../state/environments"; |
| 20 | import { cn } from "~/lib/utils"; | 19 | import { cn } from "~/lib/utils"; |
| ... | @@ -80,7 +79,6 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -80,7 +79,6 @@ export const ChatHeader = memo(function ChatHeader({ |
| 80 | return ( | 79 | return ( |
| 81 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3"> | 80 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3"> |
| 82 | <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3"> | 81 | <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3"> |
| 83 | <SidebarTrigger className="size-7 shrink-0 md:hidden" /> | ||
| 84 | <Tooltip> | 82 | <Tooltip> |
| 85 | <TooltipTrigger | 83 | <TooltipTrigger |
| 86 | render={ | 84 | render={ |
apps/web/src/components/ui/sidebar.test.tsx+33| ... | @@ -6,7 +6,9 @@ import { | ... | @@ -6,7 +6,9 @@ import { |
| 6 | SidebarMenuButton, | 6 | SidebarMenuButton, |
| 7 | SidebarMenuSubButton, | 7 | SidebarMenuSubButton, |
| 8 | SidebarProvider, | 8 | SidebarProvider, |
| 9 | SidebarTrigger, | ||
| 9 | } from "./sidebar"; | 10 | } from "./sidebar"; |
| 11 | import { resolveSidebarState } from "./sidebarState"; | ||
| 10 | 12 | ||
| 11 | function renderSidebarButton(className?: string) { | 13 | function renderSidebarButton(className?: string) { |
| 12 | return renderToStaticMarkup( | 14 | return renderToStaticMarkup( |
| ... | @@ -17,6 +19,37 @@ function renderSidebarButton(className?: string) { | ... | @@ -17,6 +19,37 @@ function renderSidebarButton(className?: string) { |
| 17 | } | 19 | } |
| 18 | 20 | ||
| 19 | describe("sidebar interactive cursors", () => { | 21 | describe("sidebar interactive cursors", () => { |
| 22 | it("uses mobile sheet visibility for the shared responsive state", () => { | ||
| 23 | expect(resolveSidebarState({ isMobile: true, open: true, openMobile: false })).toBe( | ||
| 24 | "collapsed", | ||
| 25 | ); | ||
| 26 | expect(resolveSidebarState({ isMobile: true, open: false, openMobile: true })).toBe("expanded"); | ||
| 27 | expect(resolveSidebarState({ isMobile: false, open: true, openMobile: false })).toBe( | ||
| 28 | "expanded", | ||
| 29 | ); | ||
| 30 | }); | ||
| 31 | |||
| 32 | it("exposes collapsed state for shared titlebar inset styling", () => { | ||
| 33 | const html = renderToStaticMarkup( | ||
| 34 | <SidebarProvider defaultOpen={false}> | ||
| 35 | <div /> | ||
| 36 | </SidebarProvider>, | ||
| 37 | ); | ||
| 38 | |||
| 39 | expect(html).toContain('data-sidebar-state="collapsed"'); | ||
| 40 | }); | ||
| 41 | |||
| 42 | it("keeps the sidebar trigger interactive inside Electron drag regions", () => { | ||
| 43 | const html = renderToStaticMarkup( | ||
| 44 | <SidebarProvider> | ||
| 45 | <SidebarTrigger /> | ||
| 46 | </SidebarProvider>, | ||
| 47 | ); | ||
| 48 | |||
| 49 | expect(html).toContain("[-webkit-app-region:no-drag]"); | ||
| 50 | expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!"); | ||
| 51 | }); | ||
| 52 | |||
| 20 | it("uses a pointer cursor for menu buttons by default", () => { | 53 | it("uses a pointer cursor for menu buttons by default", () => { |
| 21 | const html = renderSidebarButton(); | 54 | const html = renderSidebarButton(); |
| 22 | 55 |
apps/web/src/components/ui/sidebar.tsx+18-5| ... | @@ -19,6 +19,7 @@ import { Skeleton } from "~/components/ui/skeleton"; | ... | @@ -19,6 +19,7 @@ import { Skeleton } from "~/components/ui/skeleton"; |
| 19 | import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; | 19 | import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; |
| 20 | import { useIsMobile } from "~/hooks/useMediaQuery"; | 20 | import { useIsMobile } from "~/hooks/useMediaQuery"; |
| 21 | import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage"; | 21 | import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage"; |
| 22 | import { resolveSidebarState, type ResponsiveSidebarState } from "./sidebarState"; | ||
| 22 | import * as Schema from "effect/Schema"; | 23 | import * as Schema from "effect/Schema"; |
| 23 | 24 | ||
| 24 | const SIDEBAR_COOKIE_NAME = "sidebar_state"; | 25 | const SIDEBAR_COOKIE_NAME = "sidebar_state"; |
| ... | @@ -29,7 +30,7 @@ const SIDEBAR_WIDTH_ICON = "3rem"; | ... | @@ -29,7 +30,7 @@ const SIDEBAR_WIDTH_ICON = "3rem"; |
| 29 | const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16; | 30 | const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16; |
| 30 | 31 | ||
| 31 | type SidebarContextProps = { | 32 | type SidebarContextProps = { |
| 32 | state: "expanded" | "collapsed"; | 33 | state: ResponsiveSidebarState; |
| 33 | open: boolean; | 34 | open: boolean; |
| 34 | setOpen: (open: boolean) => void; | 35 | setOpen: (open: boolean) => void; |
| 35 | openMobile: boolean; | 36 | openMobile: boolean; |
| ... | @@ -85,6 +86,11 @@ function useSidebar() { | ... | @@ -85,6 +86,11 @@ function useSidebar() { |
| 85 | return context; | 86 | return context; |
| 86 | } | 87 | } |
| 87 | 88 | ||
| 89 | function useSidebarVisibility() { | ||
| 90 | const { isMobile, open, openMobile } = useSidebar(); | ||
| 91 | return isMobile ? openMobile : open; | ||
| 92 | } | ||
| 93 | |||
| 88 | function SidebarProvider({ | 94 | function SidebarProvider({ |
| 89 | defaultOpen = true, | 95 | defaultOpen = true, |
| 90 | open: openProp, | 96 | open: openProp, |
| ... | @@ -132,7 +138,7 @@ function SidebarProvider({ | ... | @@ -132,7 +138,7 @@ function SidebarProvider({ |
| 132 | 138 | ||
| 133 | // We add a state so that we can do data-state="expanded" or "collapsed". | 139 | // We add a state so that we can do data-state="expanded" or "collapsed". |
| 134 | // This makes it easier to style the sidebar with Tailwind classes. | 140 | // This makes it easier to style the sidebar with Tailwind classes. |
| 135 | const state = open ? "expanded" : "collapsed"; | 141 | const state = resolveSidebarState({ isMobile, open, openMobile }); |
| 136 | 142 | ||
| 137 | const contextValue = React.useMemo<SidebarContextProps>( | 143 | const contextValue = React.useMemo<SidebarContextProps>( |
| 138 | () => ({ | 144 | () => ({ |
| ... | @@ -154,6 +160,7 @@ function SidebarProvider({ | ... | @@ -154,6 +160,7 @@ function SidebarProvider({ |
| 154 | "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", | 160 | "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", |
| 155 | className, | 161 | className, |
| 156 | )} | 162 | )} |
| 163 | data-sidebar-state={state} | ||
| 157 | data-slot="sidebar-wrapper" | 164 | data-slot="sidebar-wrapper" |
| 158 | style={ | 165 | style={ |
| 159 | { | 166 | { |
| ... | @@ -310,13 +317,18 @@ function Sidebar({ | ... | @@ -310,13 +317,18 @@ function Sidebar({ |
| 310 | } | 317 | } |
| 311 | 318 | ||
| 312 | function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) { | 319 | function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) { |
| 313 | const { toggleSidebar, openMobile } = useSidebar(); | 320 | const { toggleSidebar } = useSidebar(); |
| 321 | const isOpen = useSidebarVisibility(); | ||
| 314 | 322 | ||
| 315 | return ( | 323 | return ( |
| 316 | <Button | 324 | <Button |
| 317 | className={cn("size-7", className)} | 325 | className={cn( |
| 326 | "size-[var(--workspace-titlebar-control-size)]! [-webkit-app-region:no-drag]", | ||
| 327 | className, | ||
| 328 | )} | ||
| 318 | data-sidebar="trigger" | 329 | data-sidebar="trigger" |
| 319 | data-slot="sidebar-trigger" | 330 | data-slot="sidebar-trigger" |
| 331 | aria-pressed={isOpen} | ||
| 320 | onClick={(event) => { | 332 | onClick={(event) => { |
| 321 | onClick?.(event); | 333 | onClick?.(event); |
| 322 | toggleSidebar(); | 334 | toggleSidebar(); |
| ... | @@ -325,7 +337,7 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t | ... | @@ -325,7 +337,7 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t |
| 325 | variant="ghost" | 337 | variant="ghost" |
| 326 | {...props} | 338 | {...props} |
| 327 | > | 339 | > |
| 328 | {openMobile ? <PanelLeftCloseIcon /> : <PanelLeftIcon />} | 340 | {isOpen ? <PanelLeftCloseIcon /> : <PanelLeftIcon />} |
| 329 | <span className="sr-only">Toggle Sidebar</span> | 341 | <span className="sr-only">Toggle Sidebar</span> |
| 330 | </Button> | 342 | </Button> |
| 331 | ); | 343 | ); |
| ... | @@ -1004,4 +1016,5 @@ export { | ... | @@ -1004,4 +1016,5 @@ export { |
| 1004 | SidebarSeparator, | 1016 | SidebarSeparator, |
| 1005 | SidebarTrigger, | 1017 | SidebarTrigger, |
| 1006 | useSidebar, | 1018 | useSidebar, |
| 1019 | useSidebarVisibility, | ||
| 1007 | }; | 1020 | }; |
apps/web/src/components/ui/sidebarState.ts created+9| ... | @@ -0,0 +1,9 @@ | ||
| 1 | export type ResponsiveSidebarState = "expanded" | "collapsed"; | ||
| 2 | |||
| 3 | export function resolveSidebarState(input: { | ||
| 4 | isMobile: boolean; | ||
| 5 | open: boolean; | ||
| 6 | openMobile: boolean; | ||
| 7 | }): ResponsiveSidebarState { | ||
| 8 | return (input.isMobile ? input.openMobile : input.open) ? "expanded" : "collapsed"; | ||
| 9 | } | ||
apps/web/src/index.css+33| ... | @@ -7,13 +7,24 @@ | ... | @@ -7,13 +7,24 @@ |
| 7 | :root { | 7 | :root { |
| 8 | --workspace-topbar-height: 52px; | 8 | --workspace-topbar-height: 52px; |
| 9 | --workspace-controls-top: 0px; | 9 | --workspace-controls-top: 0px; |
| 10 | --workspace-controls-left: calc(env(safe-area-inset-left) + 0.75rem); | ||
| 10 | --workspace-controls-right: calc(env(safe-area-inset-right) + 0.75rem); | 11 | --workspace-controls-right: calc(env(safe-area-inset-right) + 0.75rem); |
| 11 | --workspace-native-controls-inset: 0px; | 12 | --workspace-native-controls-inset: 0px; |
| 13 | --workspace-titlebar-control-size: 1.75rem; | ||
| 14 | --workspace-titlebar-control-gap: 0.75rem; | ||
| 15 | } | ||
| 16 | |||
| 17 | [data-slot="sidebar-wrapper"] { | ||
| 18 | --workspace-titlebar-content-left: calc( | ||
| 19 | var(--workspace-controls-left) + var(--workspace-titlebar-control-size) + | ||
| 20 | var(--workspace-titlebar-control-gap) | ||
| 21 | ); | ||
| 12 | } | 22 | } |
| 13 | 23 | ||
| 14 | .wco { | 24 | .wco { |
| 15 | --workspace-topbar-height: env(titlebar-area-height, 52px); | 25 | --workspace-topbar-height: env(titlebar-area-height, 52px); |
| 16 | --workspace-controls-top: env(titlebar-area-y, 0px); | 26 | --workspace-controls-top: env(titlebar-area-y, 0px); |
| 27 | --workspace-controls-left: calc(env(titlebar-area-x, 0px) + 0.75rem); | ||
| 17 | --workspace-controls-right: calc( | 28 | --workspace-controls-right: calc( |
| 18 | 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem | 29 | 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem |
| 19 | ); | 30 | ); |
| ... | @@ -90,6 +101,28 @@ | ... | @@ -90,6 +101,28 @@ |
| 90 | } | 101 | } |
| 91 | 102 | ||
| 92 | @layer components { | 103 | @layer components { |
| 104 | .sidebar-brand { | ||
| 105 | display: none; | ||
| 106 | } | ||
| 107 | |||
| 108 | .sidebar-brand-stage { | ||
| 109 | display: none; | ||
| 110 | } | ||
| 111 | |||
| 112 | @media (min-width: 48rem) { | ||
| 113 | @container sidebar-header (min-width: 13.5rem) { | ||
| 114 | .sidebar-brand { | ||
| 115 | display: flex; | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | @container sidebar-header (min-width: 15.75rem) { | ||
| 120 | .sidebar-brand-stage { | ||
| 121 | display: inline-flex; | ||
| 122 | } | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 93 | .workspace-topbar { | 126 | .workspace-topbar { |
| 94 | display: flex; | 127 | display: flex; |
| 95 | height: var(--workspace-topbar-height); | 128 | height: var(--workspace-topbar-height); |
apps/web/src/keybindings.test.ts+5| ... | @@ -85,6 +85,7 @@ function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig { | ... | @@ -85,6 +85,7 @@ function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig { |
| 85 | } | 85 | } |
| 86 | 86 | ||
| 87 | const DEFAULT_BINDINGS = compile([ | 87 | const DEFAULT_BINDINGS = compile([ |
| 88 | { shortcut: modShortcut("b"), command: "sidebar.toggle" }, | ||
| 88 | { shortcut: modShortcut("j"), command: "terminal.toggle" }, | 89 | { shortcut: modShortcut("j"), command: "terminal.toggle" }, |
| 89 | { shortcut: modShortcut("b", { altKey: true }), command: "rightPanel.toggle" }, | 90 | { shortcut: modShortcut("b", { altKey: true }), command: "rightPanel.toggle" }, |
| 90 | { | 91 | { |
| ... | @@ -312,6 +313,10 @@ describe("shortcutLabelForCommand", () => { | ... | @@ -312,6 +313,10 @@ describe("shortcutLabelForCommand", () => { |
| 312 | }); | 313 | }); |
| 313 | 314 | ||
| 314 | it("returns effective labels for non-terminal commands", () => { | 315 | it("returns effective labels for non-terminal commands", () => { |
| 316 | assert.strictEqual( | ||
| 317 | shortcutLabelForCommand(DEFAULT_BINDINGS, "sidebar.toggle", "MacIntel"), | ||
| 318 | "⌘B", | ||
| 319 | ); | ||
| 315 | assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel"), "⇧⌘O"); | 320 | assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel"), "⇧⌘O"); |
| 316 | assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D"); | 321 | assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D"); |
| 317 | assert.strictEqual( | 322 | assert.strictEqual( |
apps/web/src/routes/_chat.index.tsx+9-3| ... | @@ -4,10 +4,12 @@ import { LinkIcon, PlusIcon } from "lucide-react"; | ... | @@ -4,10 +4,12 @@ import { LinkIcon, PlusIcon } from "lucide-react"; |
| 4 | import { NoActiveThreadState } from "../components/NoActiveThreadState"; | 4 | import { NoActiveThreadState } from "../components/NoActiveThreadState"; |
| 5 | import { Button } from "../components/ui/button"; | 5 | import { Button } from "../components/ui/button"; |
| 6 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty"; | 6 | import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty"; |
| 7 | import { SidebarInset, SidebarTrigger } from "../components/ui/sidebar"; | 7 | import { SidebarInset } from "../components/ui/sidebar"; |
| 8 | import { useEnvironments } from "../state/environments"; | 8 | import { useEnvironments } from "../state/environments"; |
| 9 | import { APP_DISPLAY_NAME } from "~/branding"; | 9 | import { APP_DISPLAY_NAME } from "~/branding"; |
| 10 | import { hasCloudPublicConfig } from "~/cloud/publicConfig"; | 10 | import { hasCloudPublicConfig } from "~/cloud/publicConfig"; |
| 11 | import { cn } from "~/lib/utils"; | ||
| 12 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 11 | 13 | ||
| 12 | function ChatIndexRouteView() { | 14 | function ChatIndexRouteView() { |
| 13 | const { authGateState } = Route.useRouteContext(); | 15 | const { authGateState } = Route.useRouteContext(); |
| ... | @@ -30,9 +32,13 @@ function HostedStaticOnboardingState() { | ... | @@ -30,9 +32,13 @@ function HostedStaticOnboardingState() { |
| 30 | return ( | 32 | return ( |
| 31 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 33 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> |
| 32 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> | 34 | <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background"> |
| 33 | <header className="border-b border-border px-3 py-2 sm:px-5 sm:py-3"> | 35 | <header |
| 36 | className={cn( | ||
| 37 | "border-b border-border px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5 sm:py-3", | ||
| 38 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 39 | )} | ||
| 40 | > | ||
| 34 | <div className="flex items-center gap-2"> | 41 | <div className="flex items-center gap-2"> |
| 35 | <SidebarTrigger className="size-7 shrink-0 md:hidden" /> | ||
| 36 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> | 42 | <span className="text-sm font-medium text-foreground md:text-muted-foreground/60"> |
| 37 | {APP_DISPLAY_NAME} | 43 | {APP_DISPLAY_NAME} |
| 38 | </span> | 44 | </span> |
apps/web/src/routes/settings.tsx+15-4| ... | @@ -11,8 +11,10 @@ import { useCallback, useEffect, useState } from "react"; | ... | @@ -11,8 +11,10 @@ import { useCallback, useEffect, useState } from "react"; |
| 11 | 11 | ||
| 12 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; | 12 | import { useSettingsRestore } from "../components/settings/SettingsPanels"; |
| 13 | import { Button } from "../components/ui/button"; | 13 | import { Button } from "../components/ui/button"; |
| 14 | import { SidebarInset, SidebarTrigger } from "../components/ui/sidebar"; | 14 | import { SidebarInset } from "../components/ui/sidebar"; |
| 15 | import { isElectron } from "../env"; | 15 | import { isElectron } from "../env"; |
| 16 | import { cn } from "~/lib/utils"; | ||
| 17 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | ||
| 16 | 18 | ||
| 17 | function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) { | 19 | function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) { |
| 18 | const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored); | 20 | const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored); |
| ... | @@ -64,9 +66,13 @@ function SettingsContentLayout() { | ... | @@ -64,9 +66,13 @@ function SettingsContentLayout() { |
| 64 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"> | 66 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate"> |
| 65 | <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground"> | 67 | <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground"> |
| 66 | {!isElectron && ( | 68 | {!isElectron && ( |
| 67 | <header className="border-b border-border px-3 py-2 sm:px-5"> | 69 | <header |
| 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", | ||
| 72 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 73 | )} | ||
| 74 | > | ||
| 68 | <div className="flex min-h-7 items-center gap-2 sm:min-h-6"> | 75 | <div className="flex min-h-7 items-center gap-2 sm:min-h-6"> |
| 69 | <SidebarTrigger className="size-7 shrink-0 md:hidden" /> | ||
| 70 | <span className="text-sm font-medium text-foreground">Settings</span> | 76 | <span className="text-sm font-medium text-foreground">Settings</span> |
| 71 | {showRestoreDefaults ? ( | 77 | {showRestoreDefaults ? ( |
| 72 | <div className="ms-auto flex items-center gap-2"> | 78 | <div className="ms-auto flex items-center gap-2"> |
| ... | @@ -78,7 +84,12 @@ function SettingsContentLayout() { | ... | @@ -78,7 +84,12 @@ function SettingsContentLayout() { |
| 78 | )} | 84 | )} |
| 79 | 85 | ||
| 80 | {isElectron && ( | 86 | {isElectron && ( |
| 81 | <div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5 wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]"> | 87 | <div |
| 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)]", | ||
| 90 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, | ||
| 91 | )} | ||
| 92 | > | ||
| 82 | <span className="text-xs font-medium tracking-wide text-muted-foreground/70"> | 93 | <span className="text-xs font-medium tracking-wide text-muted-foreground/70"> |
| 83 | Settings | 94 | Settings |
| 84 | </span> | 95 | </span> |
apps/web/src/workspaceTitlebar.ts created+2| ... | @@ -0,0 +1,2 @@ | ||
| 1 | export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS = | ||
| 2 | "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)]"; | ||
packages/contracts/src/keybindings.test.ts+6| ... | @@ -29,6 +29,12 @@ it.effect("parses keybinding rules", () => | ... | @@ -29,6 +29,12 @@ it.effect("parses keybinding rules", () => |
| 29 | }); | 29 | }); |
| 30 | assert.strictEqual(parsed.command, "terminal.toggle"); | 30 | assert.strictEqual(parsed.command, "terminal.toggle"); |
| 31 | 31 | ||
| 32 | const parsedSidebarToggle = yield* decode(KeybindingRule, { | ||
| 33 | key: "mod+b", | ||
| 34 | command: "sidebar.toggle", | ||
| 35 | }); | ||
| 36 | assert.strictEqual(parsedSidebarToggle.command, "sidebar.toggle"); | ||
| 37 | |||
| 32 | const parsedRightPanelToggle = yield* decode(KeybindingRule, { | 38 | const parsedRightPanelToggle = yield* decode(KeybindingRule, { |
| 33 | key: "mod+alt+b", | 39 | key: "mod+alt+b", |
| 34 | command: "rightPanel.toggle", | 40 | command: "rightPanel.toggle", |
packages/contracts/src/keybindings.ts+1| ... | @@ -48,6 +48,7 @@ export const MODEL_PICKER_KEYBINDING_COMMANDS = [ | ... | @@ -48,6 +48,7 @@ export const MODEL_PICKER_KEYBINDING_COMMANDS = [ |
| 48 | export type ModelPickerKeybindingCommand = (typeof MODEL_PICKER_KEYBINDING_COMMANDS)[number]; | 48 | export type ModelPickerKeybindingCommand = (typeof MODEL_PICKER_KEYBINDING_COMMANDS)[number]; |
| 49 | 49 | ||
| 50 | const STATIC_KEYBINDING_COMMANDS = [ | 50 | const STATIC_KEYBINDING_COMMANDS = [ |
| 51 | "sidebar.toggle", | ||
| 51 | "terminal.toggle", | 52 | "terminal.toggle", |
| 52 | "terminal.split", | 53 | "terminal.split", |
| 53 | "terminal.splitVertical", | 54 | "terminal.splitVertical", |
packages/shared/src/keybindings.ts+1| ... | @@ -19,6 +19,7 @@ type WhenToken = | ... | @@ -19,6 +19,7 @@ type WhenToken = |
| 19 | | { type: "rparen" }; | 19 | | { type: "rparen" }; |
| 20 | 20 | ||
| 21 | export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ | 21 | export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ |
| 22 | { key: "mod+b", command: "sidebar.toggle" }, | ||
| 22 | { key: "mod+j", command: "terminal.toggle" }, | 23 | { key: "mod+j", command: "terminal.toggle" }, |
| 23 | { key: "mod+alt+b", command: "rightPanel.toggle" }, | 24 | { key: "mod+alt+b", command: "rightPanel.toggle" }, |
| 24 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | 25 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |