diff options
| author | Adam <[email protected]> | 2026-01-12 10:11:29 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-15 07:29:13 -0600 |
| commit | 679270d9e0731c2b3e2c059d83907cb4086d90e2 (patch) | |
| tree | 4e8d70a281b92f86a4b916ed45a5410ecbba0289 /packages/app/src/pages/layout.tsx | |
| parent | 9f66a45970d1edf12ae9b3e7a22d77711b5e51c3 (diff) | |
| download | opencode-679270d9e0731c2b3e2c059d83907cb4086d90e2.tar.gz opencode-679270d9e0731c2b3e2c059d83907cb4086d90e2.zip | |
feat(app): new layout
Diffstat (limited to 'packages/app/src/pages/layout.tsx')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 523 |
1 files changed, 351 insertions, 172 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 535f4aef2..cc5396656 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -23,6 +23,8 @@ import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { HoverCard } from "@opencode-ai/ui/hover-card" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { Spinner } from "@opencode-ai/ui/spinner" @@ -55,6 +57,8 @@ import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { navStart } from "@/utils/perf" import { DialogSelectDirectory } from "@/components/dialog-select-directory" +import { DialogEditProject } from "@/components/dialog-edit-project" +import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" export default function Layout(props: ParentProps) { @@ -814,20 +818,24 @@ export default function Layout(props: ParentProps) { const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" return ( - <div class={`relative size-10 shrink-0 ${props.class ?? ""}`}> - <Avatar - fallback={name()} - src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url} - {...getAvatarColors(props.project.icon?.color)} - class="size-full rounded-lg" - style={ - notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined - } - /> + <div class={`relative size-8 shrink-0 rounded-sm ${props.class ?? ""}`}> + <div class="size-full rounded-sm overflow-clip"> + <Avatar + fallback={name()} + src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url} + {...getAvatarColors(props.project.icon?.color)} + class="size-full rounded-sm" + style={ + notifications().length > 0 && props.notify + ? { "-webkit-mask-image": mask, "mask-image": mask } + : undefined + } + /> + </div> <Show when={notifications().length > 0 && props.notify}> <div classList={{ - "absolute -top-0.5 -right-0.5 size-2 rounded-full": true, + "absolute -top-px -right-px size-2 rounded-full z-10": true, "bg-icon-critical-base": hasError(), "bg-text-interactive-base": !hasError(), }} @@ -837,7 +845,7 @@ export default function Layout(props: ParentProps) { ) } - const SessionItem = (props: { session: Session; slug: string; mobile?: boolean }): JSX.Element => { + const SessionItem = (props: { session: Session; slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => { const notification = useNotification() const notifications = createMemo(() => notification.session.unseen(props.session.id)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) @@ -859,47 +867,62 @@ export default function Layout(props: ParentProps) { return status?.type === "busy" || status?.type === "retry" }) + const tint = createMemo(() => { + const messages = sessionStore.message[props.session.id] + if (!messages) return undefined + const user = messages + .slice() + .reverse() + .find((m) => m.role === "user") + if (!user?.agent) return undefined + + const agent = sessionStore.agent.find((a) => a.name === user.agent) + return agent?.color + }) + return ( <div data-session-id={props.session.id} - class="group/session relative w-full rounded-md cursor-default transition-colors + class="group/session relative w-full rounded-md cursor-default transition-colors px-3 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover" > <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}> <A href={`${props.slug}/session/${props.session.id}`} - class="flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1 transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7" + class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} onMouseEnter={() => prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} > - <span - classList={{ - "text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true, - "animate-pulse": isWorking(), - }} - > - {props.session.title} - </span> - <div class="shrink-0 flex items-center gap-2"> - <Switch> - <Match when={isWorking()}> - <Spinner class="size-2.5" /> - </Match> - <Match when={hasPermissions()}> - <div class="size-1.5 rounded-full bg-surface-warning-strong" /> - </Match> - <Match when={hasError()}> - <div class="size-1.5 rounded-full bg-text-diff-delete-base" /> - </Match> - <Match when={notifications().length > 0}> - <div class="size-1.5 rounded-full bg-text-interactive-base" /> - </Match> - </Switch> + <div class="flex items-center gap-1 w-full"> + <div + class="shrink-0 size-6 flex items-center justify-center" + style={{ color: tint() ?? "var(--icon-interactive-base)" }} + > + <Switch> + <Match when={isWorking()}> + <Spinner class="size-[15px]" /> + </Match> + <Match when={hasPermissions()}> + <div class="size-1.5 rounded-full bg-surface-warning-strong" /> + </Match> + <Match when={hasError()}> + <div class="size-1.5 rounded-full bg-text-diff-delete-base" /> + </Match> + <Match when={notifications().length > 0}> + <div class="size-1.5 rounded-full bg-text-interactive-base" /> + </Match> + </Switch> + </div> + <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"> + {props.session.title} + </span> <Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show> </div> </A> </Tooltip> - <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1"> + <div + class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`} + > <TooltipKeybind placement={props.mobile ? "bottom" : "right"} title="Archive session" @@ -914,26 +937,81 @@ export default function Layout(props: ParentProps) { const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => { const sortable = createSortable(props.project.worktree) - const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) const selected = createMemo(() => { const current = params.dir ? base64Decode(params.dir) : "" return props.project.worktree === current || props.project.sandboxes?.includes(current) }) + const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2)) + const label = (directory: string) => { + const [data] = globalSync.child(directory) + const kind = directory === props.project.worktree ? "local" : "sandbox" + const name = data.vcs?.branch ?? getFilename(directory) + return `${kind} : ${name}` + } + + const sessions = (directory: string) => { + const [data] = globalSync.child(directory) + return data.session + .filter((session) => session.directory === data.path.directory) + .filter((session) => !session.parentID) + .toSorted(sortSessions) + .slice(0, 2) + } + + const trigger = ( + <button + type="button" + classList={{ + "flex items-center justify-center size-10 p-1 rounded-md border transition-colors cursor-default": true, + "bg-surface-base-hover border-icon-strong-base": selected(), + "bg-transparent border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": !selected(), + }} + onClick={() => navigateToProject(props.project.worktree)} + > + <ProjectIcon project={props.project} notify /> + </button> + ) + return ( // @ts-ignore <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}> - <Tooltip placement={props.mobile ? "bottom" : "right"} value={name()}> - <Button - variant="ghost" - size="large" - class="flex items-center justify-center p-0 size-12 rounded-xl" - data-selected={selected()} - onClick={() => navigateToProject(props.project.worktree)} - > - <ProjectIcon project={props.project} notify /> - </Button> - </Tooltip> + <HoverCard openDelay={0} closeDelay={0} placement="right-start" gutter={10} trigger={trigger}> + <div class="-m-3 flex flex-col w-72"> + <div class="px-3 py-2 text-12-medium text-text-strong">Recent sessions</div> + <div class="px-2 pb-2 flex flex-col gap-2"> + <For each={workspaces()}> + {(directory) => ( + <div class="flex flex-col gap-1"> + <div class="px-2 py-0.5 flex items-center gap-1 min-w-0"> + <div class="shrink-0 size-6 flex items-center justify-center"> + <Icon name="branch" size="small" class="text-icon-base" /> + </div> + <span class="truncate text-14-medium text-text-strong">{label(directory)}</span> + </div> + <For each={sessions(directory)}> + {(session) => ( + <SessionItem session={session} slug={base64Encode(directory)} dense mobile={props.mobile} /> + )} + </For> + </div> + )} + </For> + </div> + <div class="px-2 py-2 border-t border-border-weak-base"> + <Button + variant="ghost" + class="flex w-full text-left justify-start text-text-base px-2" + onClick={() => { + layout.sidebar.open() + navigateToProject(props.project.worktree) + }} + > + View all sessions + </Button> + </div> + </div> + </HoverCard> </div> ) } @@ -967,7 +1045,7 @@ export default function Layout(props: ParentProps) { return ( <Show when={label()}> {(value) => ( - <div class="bg-background-base rounded-md px-2 py-1 text-12-medium text-text-strong">{value()}</div> + <div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div> )} </Show> ) @@ -1003,39 +1081,59 @@ export default function Layout(props: ParentProps) { <Collapsible variant="ghost" open={open()} - class="gap-1.5 shrink-0" + class="shrink-0" onOpenChange={(value) => setStore("workspaceExpanded", props.directory, value)} > - <Collapsible.Trigger class="group/trigger flex items-center justify-between w-full px-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover"> - <div class="flex items-center gap-2 min-w-0"> - <Icon - name="chevron-right" - size="small" - class="text-text-subtle transition-transform duration-50 group-data-[expanded]/trigger:rotate-90" - /> - <span class="truncate text-12-medium text-text-strong">{title()}</span> + <div class="px-2 py-1"> + <div class="group/trigger relative"> + <Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover"> + <div class="flex items-center gap-1 min-w-0"> + <div class="flex items-center justify-center shrink-0 size-6"> + <Icon name="branch" size="small" /> + </div> + <span class="truncate text-14-medium text-text-strong">{title()}</span> + <Icon + name={open() ? "chevron-down" : "chevron-right"} + size="small" + class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/trigger:opacity-100 group-focus-within/trigger:opacity-100" + /> + </div> + </Collapsible.Trigger> + <div class="absolute right-1 top-1/2 -translate-y-1/2 hidden items-center gap-0.5 pointer-events-none group-hover/trigger:flex group-focus-within/trigger:flex"> + <Tooltip class="pointer-events-auto" value="More options" placement="top"> + <IconButton icon="dot-grid" variant="ghost" class="size-6 rounded-md" /> + </Tooltip> + <Tooltip class="pointer-events-auto" value="New session" placement="top"> + <IconButton + icon="plus-small" + variant="ghost" + class="size-6 rounded-md" + onClick={() => navigate(`/${slug()}/session`)} + /> + </Tooltip> + </div> </div> - </Collapsible.Trigger> + </div> <Collapsible.Content> - <nav class="flex flex-col gap-1 pl-2"> - <For each={sessions()}> - {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />} - </For> + <nav class="flex flex-col gap-1 px-2"> <Button as={A} href={`${slug()}/session`} variant="ghost" size="large" - icon="plus-small" - class="flex w-full text-left justify-start text-text-base rounded-md px-3" + icon="edit" + class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3" > New session </Button> + <For each={sessions()}> + {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />} + </For> <Show when={hasMore()}> <div class="relative w-full py-1"> <Button variant="ghost" - class="flex w-full text-left justify-start text-12-medium opacity-50 px-3.5" + class="flex w-full text-left justify-start text-12-medium text-text-weak px-10" size="large" onClick={loadMore} > @@ -1050,9 +1148,53 @@ export default function Layout(props: ParentProps) { ) } + const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => { + const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree) + const slug = createMemo(() => base64Encode(props.project.worktree)) + const sessions = createMemo(() => + workspaceStore.session + .filter((session) => session.directory === workspaceStore.path.directory) + .filter((session) => !session.parentID) + .toSorted(sortSessions), + ) + const hasMore = createMemo(() => workspaceStore.session.length >= workspaceStore.limit) + const loadMore = async () => { + setWorkspaceStore("limit", (limit) => limit + 5) + await globalSync.project.loadSessions(props.project.worktree) + } + + return ( + <div + ref={(el) => { + if (!props.mobile) scrollContainerRef = el + }} + class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar" + > + <nav class="flex flex-col gap-1 px-2"> + <For each={sessions()}> + {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />} + </For> + <Show when={hasMore()}> + <div class="relative w-full py-1"> + <Button + variant="ghost" + class="flex w-full text-left justify-start text-12-medium text-text-weak px-10" + size="large" + onClick={loadMore} + > + Load more + </Button> + </div> + </Show> + </nav> + </div> + ) + } + const SidebarContent = (sidebarProps: { mobile?: boolean }) => { const expanded = () => sidebarProps.mobile || layout.sidebar.opened() + const sync = useGlobalSync() const project = createMemo(() => currentProject()) const projectName = createMemo(() => { const current = project() @@ -1091,9 +1233,11 @@ export default function Layout(props: ParentProps) { navigate(`/${base64Encode(created.directory)}/session`) } + const homedir = createMemo(() => sync.data.path.home) + return ( <div class="flex h-full w-full overflow-hidden"> - <div class="w-16 shrink-0 bg-background-base border-r border-border-weak-base flex flex-col items-center overflow-hidden"> + <div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden"> <div class="flex-1 min-h-0 w-full"> <DragDropProvider onDragStart={handleDragStart} @@ -1103,7 +1247,7 @@ export default function Layout(props: ParentProps) { > <DragDropSensors /> <ConstrainDragXAxis /> - <div class="h-full w-full flex flex-col items-center gap-2 px-2 py-3 overflow-y-auto no-scrollbar"> + <div class="h-full w-full flex flex-col items-center gap-3 px-3 py-2 overflow-y-auto no-scrollbar"> <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}> <For each={layout.projects.list()}> {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />} @@ -1120,7 +1264,7 @@ export default function Layout(props: ParentProps) { </div> } > - <IconButton icon="plus" variant="ghost" class="size-12 rounded-xl" onClick={chooseProject} /> + <IconButton icon="plus" variant="ghost" size="large" onClick={chooseProject} /> </Tooltip> </div> <DragOverlay> @@ -1128,12 +1272,25 @@ export default function Layout(props: ParentProps) { </DragOverlay> </DragDropProvider> </div> + <div class="shrink-0 w-full pt-3 pb-3 flex flex-col items-center gap-2"> + <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Settings"> + <IconButton icon="settings-gear" variant="ghost" size="large" onClick={command.show} /> + </Tooltip> + <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Help"> + <IconButton + icon="help" + variant="ghost" + size="large" + onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")} + /> + </Tooltip> + </div> </div> <Show when={expanded()}> <div classList={{ - "flex flex-col min-h-0 bg-background-base border-r border-border-weak-base": true, + "flex flex-col min-h-0 bg-background-stronger border border-border-weak-base rounded-tl-sm": true, "flex-1 min-w-0": sidebarProps.mobile, }} style={{ width: sidebarProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }} @@ -1141,69 +1298,125 @@ export default function Layout(props: ParentProps) { <Show when={project()}> {(p) => ( <> - <div class="shrink-0 h-12 flex items-center justify-between px-3 border-b border-border-weak-base"> - <div class="min-w-0 truncate text-14-medium text-text-strong">{projectName()}</div> - <Button variant="ghost" size="large" icon="plus-small" onClick={createWorkspace}> - New workspace - </Button> + <div class="shrink-0 px-2 py-1"> + <div class="flex items-start justify-between gap-2 p-2"> + <div class="flex flex-col min-w-0"> + <span class="text-16-medium text-text-strong truncate">{projectName()}</span> + <Tooltip placement="right" value={project()?.worktree} class="shrink-0"> + <span class="text-12-regular text-text-base truncate"> + {project()?.worktree.replace(homedir(), "~")} + </span> + </Tooltip> + </div> + <DropdownMenu> + <DropdownMenu.Trigger + as={IconButton} + icon="dot-grid" + variant="ghost" + class="shrink-0 size-6 rounded-md" + /> + <DropdownMenu.Portal> + <DropdownMenu.Content> + <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p()} />)}> + <DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Item onSelect={() => closeProject(p().worktree)}> + <DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Separator /> + <DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces()}> + <DropdownMenu.ItemLabel> + {layout.sidebar.workspaces() ? "Disable workspaces" : "Enable workspaces"} + </DropdownMenu.ItemLabel> + </DropdownMenu.Item> + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenu> + </div> </div> - <div class="flex-1 min-h-0"> - <DragDropProvider - onDragStart={handleWorkspaceDragStart} - onDragEnd={handleWorkspaceDragEnd} - onDragOver={handleWorkspaceDragOver} - collisionDetector={closestCenter} - > - <DragDropSensors /> - <ConstrainDragXAxis /> - <div - ref={(el) => { - if (!sidebarProps.mobile) scrollContainerRef = el - }} - class="h-full w-full flex flex-col gap-2 px-2 py-2 overflow-y-auto no-scrollbar" - > - <SortableProvider ids={workspaces()}> - <For each={workspaces()}> - {(directory) => ( - <SortableWorkspace directory={directory} project={p()} mobile={sidebarProps.mobile} /> - )} - </For> - </SortableProvider> + <Show + when={layout.sidebar.workspaces()} + fallback={ + <> + <div class="py-4 px-3"> + <Button + size="large" + icon="plus-small" + class="w-full" + onClick={() => { + navigate(`/${base64Encode(p().worktree)}/session`) + layout.mobileSidebar.hide() + }} + > + New session + </Button> + </div> + <div class="flex-1 min-h-0"> + <LocalWorkspace project={p()} mobile={sidebarProps.mobile} /> + </div> + </> + } + > + <> + <div class="py-4 px-3"> + <Button size="large" icon="plus-small" class="w-full" onClick={createWorkspace}> + New workspace + </Button> </div> - <DragOverlay> - <WorkspaceDragOverlay /> - </DragOverlay> - </DragDropProvider> - </div> + <div class="flex-1 min-h-0"> + <DragDropProvider + onDragStart={handleWorkspaceDragStart} + onDragEnd={handleWorkspaceDragEnd} + onDragOver={handleWorkspaceDragOver} + collisionDetector={closestCenter} + > + <DragDropSensors /> + <ConstrainDragXAxis /> + <div + ref={(el) => { + if (!sidebarProps.mobile) scrollContainerRef = el + }} + class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar" + > + <SortableProvider ids={workspaces()}> + <For each={workspaces()}> + {(directory) => ( + <SortableWorkspace directory={directory} project={p()} mobile={sidebarProps.mobile} /> + )} + </For> + </SortableProvider> + </div> + <DragOverlay> + <WorkspaceDragOverlay /> + </DragOverlay> + </DragDropProvider> + </div> + </> + </Show> </> )} </Show> <Show when={!project()}> - <div class="p-3 text-12-regular text-text-weak">Open a project to see workspaces.</div> + <div class="p-3 text-12-regular text-text-weak">Open a project to see sessions.</div> </Show> - <Show when={providers.all().length > 0}> - <div class="shrink-0 px-2 py-3 border-t border-border-weak-base flex flex-col gap-1.5"> - <Button - class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2" - variant="ghost" - size="large" - icon="plus" - onClick={connectProvider} - > - Connect provider - </Button> - <Button - as={"a"} - href="https://opencode.ai/desktop-feedback" - target="_blank" - class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2" - variant="ghost" - size="large" - icon="bubble-5" - > - Share feedback - </Button> + <Show when={providers.all().length > 0 && providers.paid().length === 0}> + <div class="shrink-0 px-2 py-3 border-t border-border-weak-base"> + <div class="rounded-md bg-background-base shadow-xs-border-base"> + <div class="p-3 flex flex-col gap-2"> + <div class="text-12-medium text-text-strong">Getting started</div> + <div class="text-text-base">OpenCode includes free models so you can start immediately.</div> + <div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div> + </div> + <Button + class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3" + size="large" + icon="plus" + onClick={connectProvider} + > + Connect provider + </Button> + </div> </div> </Show> </div> @@ -1212,50 +1425,9 @@ export default function Layout(props: ParentProps) { ) } - const isMac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") - const reserveWindowButtons = createMemo( - () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"), - ) - return ( - <div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> - <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex items-center"> - <div - classList={{ - "flex items-center w-full min-w-0 pr-2": true, - "pl-2": !isMac(), - }} - > - <Show when={isMac()}> - <div class="w-[72px] h-full shrink-0" data-tauri-drag-region /> - </Show> - <IconButton - icon="menu" - variant="ghost" - class="xl:hidden size-8 rounded-md" - onClick={layout.mobileSidebar.toggle} - /> - <TooltipKeybind - class="hidden xl:flex shrink-0" - placement="bottom" - title="Toggle sidebar" - keybind={command.keybind("sidebar.toggle")} - > - <IconButton - icon={layout.sidebar.opened() ? "layout-left" : "layout-right"} - variant="ghost" - class="size-8 rounded-md" - onClick={layout.sidebar.toggle} - /> - </TooltipKeybind> - <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" /> - <div class="flex-1 h-full" data-tauri-drag-region /> - <div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" /> - <Show when={reserveWindowButtons()}> - <div class="w-[120px] h-full shrink-0" data-tauri-drag-region /> - </Show> - </div> - </header> + <div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> + <Titlebar /> <div class="flex-1 min-h-0 flex"> <div classList={{ @@ -1282,7 +1454,7 @@ export default function Layout(props: ParentProps) { <div class="xl:hidden"> <div classList={{ - "fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true, + "fixed inset-0 z-40 transition-opacity duration-200": true, "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(), "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(), }} @@ -1302,7 +1474,14 @@ export default function Layout(props: ParentProps) { </div> </div> - <main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main> + <main + classList={{ + "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true, + "border-l rounded-tl-sm": !layout.sidebar.opened(), + }} + > + {props.children} + </main> </div> <Toast.Region /> </div> |
