diff options
| author | adamelmore <[email protected]> | 2026-01-26 06:28:25 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-26 09:33:42 -0600 |
| commit | af3d8c383e5fe2feba79ace5bb2e1082195a459d (patch) | |
| tree | fa41ca0096016cd75737ee6709df2d37b0fbcff4 | |
| parent | 7f75f71f6b970c714b9e74f5715073e07ca8e431 (diff) | |
| download | opencode-af3d8c383e5fe2feba79ace5bb2e1082195a459d.tar.gz opencode-af3d8c383e5fe2feba79ace5bb2e1082195a459d.zip | |
wip(app): sidebar hover full
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 768 |
1 files changed, 414 insertions, 354 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2702f119b..a14b4a8e5 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -135,6 +135,21 @@ export default function Layout(props: ParentProps) { const editorRef = { current: undefined as HTMLInputElement | undefined } const [hoverSession, setHoverSession] = createSignal<string | undefined>() + const [hoverProject, setHoverProject] = createSignal<string | undefined>() + + const sidebarHovering = createMemo(() => !layout.sidebar.opened() && hoverProject() !== undefined) + const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) + + const hoverProjectData = createMemo(() => { + const id = hoverProject() + if (!id) return + return layout.projects.list().find((project) => project.worktree === id) + }) + + createEffect(() => { + if (!layout.sidebar.opened()) return + setHoverProject(undefined) + }) const autoselecting = createMemo(() => { if (params.dir) return false @@ -1119,15 +1134,13 @@ export default function Layout(props: ParentProps) { return language.t("common.requestFailed") } - const deleteWorkspace = async (directory: string) => { - const current = currentProject() - if (!current) return - if (directory === current.worktree) return + const deleteWorkspace = async (root: string, directory: string) => { + if (directory === root) return setBusy(directory, true) const result = await globalSDK.client.worktree - .remove({ directory: current.worktree, worktreeRemoveInput: { directory } }) + .remove({ directory: root, worktreeRemoveInput: { directory } }) .then((x) => x.data) .catch((err) => { showToast({ @@ -1142,17 +1155,15 @@ export default function Layout(props: ParentProps) { if (!result) return layout.projects.close(directory) - layout.projects.open(current.worktree) + layout.projects.open(root) if (params.dir && base64Decode(params.dir) === directory) { - navigateToProject(current.worktree) + navigateToProject(root) } } - const resetWorkspace = async (directory: string) => { - const current = currentProject() - if (!current) return - if (directory === current.worktree) return + const resetWorkspace = async (root: string, directory: string) => { + if (directory === root) return setBusy(directory, true) const progress = showToast({ @@ -1168,7 +1179,7 @@ export default function Layout(props: ParentProps) { .catch(() => []) const result = await globalSDK.client.worktree - .reset({ directory: current.worktree, worktreeResetInput: { directory } }) + .reset({ directory: root, worktreeResetInput: { directory } }) .then((x) => x.data) .catch((err) => { showToast({ @@ -1251,7 +1262,7 @@ export default function Layout(props: ParentProps) { ) } - function DialogDeleteWorkspace(props: { directory: string }) { + function DialogDeleteWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [data, setData] = createStore({ status: "loading" as "loading" | "ready" | "error", @@ -1259,12 +1270,6 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - const current = currentProject() - if (!current) { - setData({ status: "error", dirty: false }) - return - } - globalSDK.client.file .status({ directory: props.directory }) .then((x) => { @@ -1279,7 +1284,7 @@ export default function Layout(props: ParentProps) { const handleDelete = () => { dialog.close() - void deleteWorkspace(props.directory) + void deleteWorkspace(props.root, props.directory) } const description = () => { @@ -1311,7 +1316,7 @@ export default function Layout(props: ParentProps) { ) } - function DialogResetWorkspace(props: { directory: string }) { + function DialogResetWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [state, setState] = createStore({ status: "loading" as "loading" | "ready" | "error", @@ -1329,12 +1334,6 @@ export default function Layout(props: ParentProps) { } onMount(() => { - const current = currentProject() - if (!current) { - setState({ status: "error", dirty: false }) - return - } - globalSDK.client.file .status({ directory: props.directory }) .then((x) => { @@ -1350,7 +1349,7 @@ export default function Layout(props: ParentProps) { const handleReset = () => { dialog.close() - void resetWorkspace(props.directory) + void resetWorkspace(props.root, props.directory) } const archivedCount = () => state.sessions.length @@ -1444,6 +1443,7 @@ export default function Layout(props: ParentProps) { function handleDragStart(event: unknown) { const id = getDraggableId(event) if (!id) return + setHoverProject(undefined) setStore("activeProject", id) } @@ -1483,6 +1483,13 @@ export default function Layout(props: ParentProps) { return [...merged, extra] } + const sidebarProject = createMemo(() => { + if (layout.sidebar.opened()) return currentProject() + const hovered = hoverProjectData() + if (hovered) return hovered + return currentProject() + }) + function handleWorkspaceDragStart(event: unknown) { const id = getDraggableId(event) if (!id) return @@ -1493,7 +1500,7 @@ export default function Layout(props: ParentProps) { const { draggable, droppable } = event if (!draggable || !droppable) return - const project = currentProject() + const project = sidebarProject() if (!project) return const ids = workspaceIds(project) @@ -1593,7 +1600,7 @@ export default function Layout(props: ParentProps) { sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), ) const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) - const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) + const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) const [menuOpen, setMenuOpen] = createSignal(false) @@ -1611,7 +1618,11 @@ export default function Layout(props: ParentProps) { class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menuOpen() ? "pr-7" : ""} 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")} - onClick={() => setHoverSession(undefined)} + onClick={() => { + setHoverSession(undefined) + if (layout.sidebar.opened()) return + queueMicrotask(() => setHoverProject(undefined)) + }} > <div class="flex items-center gap-1 w-full"> <div @@ -1753,13 +1764,17 @@ export default function Layout(props: ParentProps) { const NewSessionItem = (props: { slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => { const label = language.t("command.session.new") - const tooltip = () => props.mobile || !layout.sidebar.opened() + const tooltip = () => props.mobile || !sidebarExpanded() const item = ( <A href={`${props.slug}/session`} end class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`} - onClick={() => setHoverSession(undefined)} + onClick={() => { + setHoverSession(undefined) + if (layout.sidebar.opened()) return + queueMicrotask(() => setHoverProject(undefined)) + }} > <div class="flex items-center gap-1 w-full"> <div class="shrink-0 size-6 flex items-center justify-center"> @@ -1814,7 +1829,7 @@ export default function Layout(props: ParentProps) { const WorkspaceDragOverlay = (): JSX.Element => { const label = createMemo(() => { - const project = currentProject() + const project = sidebarProject() if (!project) return const directory = store.activeWorkspace if (!directory) return @@ -1985,13 +2000,21 @@ export default function Layout(props: ParentProps) { </DropdownMenu.Item> <DropdownMenu.Item disabled={local() || busy()} - onSelect={() => dialog.show(() => <DialogResetWorkspace directory={props.directory} />)} + onSelect={() => + dialog.show(() => ( + <DialogResetWorkspace root={props.project.worktree} directory={props.directory} /> + )) + } > <DropdownMenu.ItemLabel>{language.t("common.reset")}</DropdownMenu.ItemLabel> </DropdownMenu.Item> <DropdownMenu.Item disabled={local() || busy()} - onSelect={() => dialog.show(() => <DialogDeleteWorkspace directory={props.directory} />)} + onSelect={() => + dialog.show(() => ( + <DialogDeleteWorkspace root={props.project.worktree} directory={props.directory} /> + )) + } > <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel> </DropdownMenu.Item> @@ -2005,9 +2028,7 @@ export default function Layout(props: ParentProps) { <Collapsible.Content> <nav class="flex flex-col gap-1 px-2"> - <Show when={workspaceSetting()}> - <NewSessionItem slug={slug()} mobile={props.mobile} /> - </Show> + <NewSessionItem slug={slug()} mobile={props.mobile} /> <Show when={loading()}> <SessionSkeleton /> </Show> @@ -2049,6 +2070,16 @@ export default function Layout(props: ParentProps) { ) const [open, setOpen] = createSignal(false) + const preview = createMemo(() => !props.mobile && layout.sidebar.opened()) + const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened()) + const active = createMemo(() => (preview() ? open() : overlay() && hoverProject() === props.project.worktree)) + + createEffect(() => { + if (preview()) return + if (!open()) return + setOpen(false) + }) + const label = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) const kind = @@ -2087,8 +2118,20 @@ export default function Layout(props: ParentProps) { "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true, "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": selected(), "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": - !selected() && !open(), - "bg-surface-base-hover border border-border-weak-base": !selected() && open(), + !selected() && !active(), + "bg-surface-base-hover border border-border-weak-base": !selected() && active(), + }} + onMouseEnter={() => { + if (!overlay()) return + globalSync.child(props.project.worktree) + setHoverProject(props.project.worktree) + setHoverSession(undefined) + }} + onFocus={() => { + if (!overlay()) return + globalSync.child(props.project.worktree) + setHoverProject(props.project.worktree) + setHoverSession(undefined) }} onClick={() => navigateToProject(props.project.worktree)} onBlur={() => setOpen(false)} @@ -2100,96 +2143,98 @@ export default function Layout(props: ParentProps) { return ( // @ts-ignore <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}> - <HoverCard - open={open()} - openDelay={0} - closeDelay={0} - placement="right-start" - gutter={6} - trigger={trigger} - onOpenChange={(value) => { - setOpen(value) - if (value) setHoverSession(undefined) - }} - > - <div class="-m-3 p-2 flex flex-col w-72"> - <div class="px-4 pt-2 pb-1 flex items-center gap-2"> - <div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div> - <Tooltip value={language.t("common.close")} placement="top" gutter={6}> - <IconButton - icon="circle-x" + <Show when={preview()} fallback={trigger}> + <HoverCard + open={open()} + openDelay={0} + closeDelay={0} + placement="right-start" + gutter={6} + trigger={trigger} + onOpenChange={(value) => { + setOpen(value) + if (value) setHoverSession(undefined) + }} + > + <div class="-m-3 p-2 flex flex-col w-72"> + <div class="px-4 pt-2 pb-1 flex items-center gap-2"> + <div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div> + <Tooltip value={language.t("common.close")} placement="top" gutter={6}> + <IconButton + icon="circle-x" + variant="ghost" + class="shrink-0" + aria-label={language.t("common.close")} + onClick={(event) => { + event.stopPropagation() + setOpen(false) + closeProject(props.project.worktree) + }} + /> + </Tooltip> + </div> + <div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div> + <div class="px-2 pb-2 flex flex-col gap-2"> + <Show + when={workspaceEnabled()} + fallback={ + <For each={projectSessions()}> + {(session) => ( + <SessionItem + session={session} + slug={base64Encode(props.project.worktree)} + dense + mobile={props.mobile} + popover={false} + /> + )} + </For> + } + > + <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-base">{label(directory)}</span> + </div> + <For each={sessions(directory)}> + {(session) => ( + <SessionItem + session={session} + slug={base64Encode(directory)} + dense + mobile={props.mobile} + popover={false} + /> + )} + </For> + </div> + )} + </For> + </Show> + </div> + <div class="px-2 py-2 border-t border-border-weak-base"> + <Button variant="ghost" - class="shrink-0" - aria-label={language.t("common.close")} - onClick={(event) => { - event.stopPropagation() + class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent" + onClick={() => { + layout.sidebar.open() setOpen(false) - closeProject(props.project.worktree) + if (selected()) { + return + } + navigateToProject(props.project.worktree) }} - /> - </Tooltip> - </div> - <div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div> - <div class="px-2 pb-2 flex flex-col gap-2"> - <Show - when={workspaceEnabled()} - fallback={ - <For each={projectSessions()}> - {(session) => ( - <SessionItem - session={session} - slug={base64Encode(props.project.worktree)} - dense - mobile={props.mobile} - popover={false} - /> - )} - </For> - } - > - <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-base">{label(directory)}</span> - </div> - <For each={sessions(directory)}> - {(session) => ( - <SessionItem - session={session} - slug={base64Encode(directory)} - dense - mobile={props.mobile} - popover={false} - /> - )} - </For> - </div> - )} - </For> - </Show> - </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 hover:bg-transparent active:bg-transparent" - onClick={() => { - layout.sidebar.open() - setOpen(false) - if (selected()) { - return - } - navigateToProject(props.project.worktree) - }} - > - {language.t("sidebar.project.viewAllSessions")} - </Button> + > + {language.t("sidebar.project.viewAllSessions")} + </Button> + </div> </div> - </div> - </HoverCard> + </HoverCard> + </Show> </div> ) } @@ -2219,9 +2264,6 @@ export default function Layout(props: ParentProps) { style={{ "overflow-anchor": "none" }} > <nav class="flex flex-col gap-1 px-2"> - <Show when={workspaceSetting()}> - <NewSessionItem slug={slug()} mobile={props.mobile} /> - </Show> <Show when={loading()}> <SessionSkeleton /> </Show> @@ -2248,60 +2290,241 @@ export default function Layout(props: ParentProps) { ) } - const SidebarContent = (sidebarProps: { mobile?: boolean }) => { - const expanded = () => sidebarProps.mobile || layout.sidebar.opened() + const createWorkspace = async (project: LocalProject) => { + const created = await globalSDK.client.worktree + .create({ directory: project.worktree }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("workspace.create.failed.title"), + description: errorMessage(err), + }) + return undefined + }) - const sync = useGlobalSync() - const project = createMemo(() => currentProject()) + if (!created?.directory) return + + const local = project.worktree + const key = workspaceKey(created.directory) + const root = workspaceKey(local) + + setBusy(created.directory, true) + WorktreeState.pending(created.directory) + setStore("workspaceExpanded", key, true) + if (key !== created.directory) { + setStore("workspaceExpanded", created.directory, true) + } + setStore("workspaceOrder", project.worktree, (prev) => { + const existing = prev ?? [] + const next = existing.filter((item) => { + const id = workspaceKey(item) + if (id === root) return false + return id !== key + }) + return [local, created.directory, ...next] + }) + + globalSync.child(created.directory) + navigate(`/${base64Encode(created.directory)}/session`) + layout.mobileSidebar.hide() + } + + const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean }) => { const projectName = createMemo(() => { - const current = project() - if (!current) return "" - return current.name || getFilename(current.worktree) + const project = panelProps.project + if (!project) return "" + return project.name || getFilename(project.worktree) }) - const projectId = createMemo(() => project()?.id ?? "") - const workspaces = createMemo(() => workspaceIds(project())) - - const createWorkspace = async () => { - const current = project() - if (!current) return - - const created = await globalSDK.client.worktree - .create({ directory: current.worktree }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("workspace.create.failed.title"), - description: errorMessage(err), - }) - return undefined - }) + const projectId = createMemo(() => panelProps.project?.id ?? "") + const workspaces = createMemo(() => workspaceIds(panelProps.project)) + const workspacesEnabled = createMemo(() => { + const project = panelProps.project + if (!project) return false + if (project.vcs !== "git") return false + return layout.sidebar.workspaces(project.worktree)() + }) + const homedir = createMemo(() => globalSync.data.path.home) - if (!created?.directory) return + return ( + <div + classList={{ + "flex flex-col min-h-0 bg-background-stronger border border-b-0 border-border-weak-base rounded-tl-sm": true, + "flex-1 min-w-0": panelProps.mobile, + }} + style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }} + > + <Show when={panelProps.project} keyed> + {(p) => ( + <> + <div class="shrink-0 px-2 py-1"> + <div class="group/project flex items-start justify-between gap-2 p-2 pr-1"> + <div class="flex flex-col min-w-0"> + <InlineEditor + id={`project:${projectId()}`} + value={projectName} + onSave={(next) => renameProject(p, next)} + class="text-16-medium text-text-strong truncate" + displayClass="text-16-medium text-text-strong truncate" + stopPropagation + /> + + <Tooltip + placement="bottom" + gutter={2} + value={p.worktree} + class="shrink-0" + contentStyle={{ + "max-width": "640px", + transform: "translate3d(52px, 0, 0)", + }} + > + <span class="text-12-regular text-text-base truncate select-text"> + {p.worktree.replace(homedir(), "~")} + </span> + </Tooltip> + </div> - const local = current.worktree - const key = workspaceKey(created.directory) - const root = workspaceKey(local) + <DropdownMenu> + <DropdownMenu.Trigger + as={IconButton} + icon="dot-grid" + variant="ghost" + class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" + aria-label={language.t("common.moreOptions")} + /> + <DropdownMenu.Portal> + <DropdownMenu.Content class="mt-1"> + <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}> + <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Item + disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()} + onSelect={() => { + const enabled = layout.sidebar.workspaces(p.worktree)() + if (enabled) { + layout.sidebar.toggleWorkspaces(p.worktree) + return + } + if (p.vcs !== "git") return + layout.sidebar.toggleWorkspaces(p.worktree) + }} + > + <DropdownMenu.ItemLabel> + {layout.sidebar.workspaces(p.worktree)() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} + </DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Separator /> + <DropdownMenu.Item onSelect={() => closeProject(p.worktree)}> + <DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenu> + </div> + </div> - setBusy(created.directory, true) - WorktreeState.pending(created.directory) - setStore("workspaceExpanded", key, true) - if (key !== created.directory) { - setStore("workspaceExpanded", created.directory, true) - } - setStore("workspaceOrder", current.worktree, (prev) => { - const existing = prev ?? [] - const next = existing.filter((item) => { - const id = workspaceKey(item) - if (id === root) return false - return id !== key - }) - return [local, created.directory, ...next] - }) + <Show + when={workspacesEnabled()} + fallback={ + <> + <div class="py-4 px-3"> + <TooltipKeybind + title={language.t("command.session.new")} + keybind={command.keybind("session.new")} + placement="top" + > + <Button + size="large" + icon="plus-small" + class="w-full" + onClick={() => { + navigate(`/${base64Encode(p.worktree)}/session`) + layout.mobileSidebar.hide() + }} + > + {language.t("command.session.new")} + </Button> + </TooltipKeybind> + </div> + <div class="flex-1 min-h-0"> + <LocalWorkspace project={p} mobile={panelProps.mobile} /> + </div> + </> + } + > + <> + <div class="py-4 px-3"> + <TooltipKeybind + title={language.t("workspace.new")} + keybind={command.keybind("workspace.new")} + placement="top" + > + <Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p)}> + {language.t("workspace.new")} + </Button> + </TooltipKeybind> + </div> + <div class="relative flex-1 min-h-0"> + <DragDropProvider + onDragStart={handleWorkspaceDragStart} + onDragEnd={handleWorkspaceDragEnd} + onDragOver={handleWorkspaceDragOver} + collisionDetector={closestCenter} + > + <DragDropSensors /> + <ConstrainDragXAxis /> + <div + ref={(el) => { + if (!panelProps.mobile) scrollContainerRef = el + }} + class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar" + style={{ "overflow-anchor": "none" }} + > + <SortableProvider ids={workspaces()}> + <For each={workspaces()}> + {(directory) => ( + <SortableWorkspace directory={directory} project={p} mobile={panelProps.mobile} /> + )} + </For> + </SortableProvider> + </div> + <DragOverlay> + <WorkspaceDragOverlay /> + </DragOverlay> + </DragDropProvider> + </div> + </> + </Show> + </> + )} + </Show> + <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">{language.t("sidebar.gettingStarted.title")}</div> + <div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div> + <div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</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} + > + {language.t("command.provider.connect")} + </Button> + </div> + </div> + </Show> + </div> + ) + } - globalSync.child(created.directory) - navigate(`/${base64Encode(created.directory)}/session`) - layout.mobileSidebar.hide() - } + const SidebarContent = (sidebarProps: { mobile?: boolean }) => { + const expanded = () => sidebarProps.mobile || layout.sidebar.opened() command.register(() => [ { @@ -2310,12 +2533,14 @@ export default function Layout(props: ParentProps) { category: language.t("command.category.workspace"), keybind: "mod+shift+w", disabled: !workspaceSetting(), - onSelect: createWorkspace, + onSelect: () => { + const project = currentProject() + if (!project) return + return createWorkspace(project) + }, }, ]) - 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 flex flex-col items-center overflow-hidden"> @@ -2386,180 +2611,7 @@ export default function Layout(props: ParentProps) { </div> <Show when={expanded()}> - <div - classList={{ - "flex flex-col min-h-0 bg-background-stronger border border-b-0 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` }} - > - <Show when={project()} keyed> - {(p) => ( - <> - <div class="shrink-0 px-2 py-1"> - <div class="group/project flex items-start justify-between gap-2 p-2 pr-1"> - <div class="flex flex-col min-w-0"> - <InlineEditor - id={`project:${projectId()}`} - value={projectName} - onSave={(next) => project() && renameProject(project()!, next)} - class="text-16-medium text-text-strong truncate" - displayClass="text-16-medium text-text-strong truncate" - stopPropagation - /> - - <Tooltip - placement="bottom" - gutter={2} - value={project()?.worktree} - class="shrink-0" - contentStyle={{ - "max-width": "640px", - transform: "translate3d(52px, 0, 0)", - }} - > - <span class="text-12-regular text-text-base truncate select-text"> - {project()?.worktree.replace(homedir(), "~")} - </span> - </Tooltip> - </div> - - <DropdownMenu> - <DropdownMenu.Trigger - as={IconButton} - icon="dot-grid" - variant="ghost" - class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" - aria-label={language.t("common.moreOptions")} - /> - <DropdownMenu.Portal> - <DropdownMenu.Content class="mt-1"> - <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}> - <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - <DropdownMenu.Item - disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()} - onSelect={() => { - const enabled = layout.sidebar.workspaces(p.worktree)() - if (enabled) { - layout.sidebar.toggleWorkspaces(p.worktree) - return - } - if (p.vcs !== "git") return - layout.sidebar.toggleWorkspaces(p.worktree) - }} - > - <DropdownMenu.ItemLabel> - {layout.sidebar.workspaces(p.worktree)() - ? language.t("sidebar.workspaces.disable") - : language.t("sidebar.workspaces.enable")} - </DropdownMenu.ItemLabel> - </DropdownMenu.Item> - <DropdownMenu.Separator /> - <DropdownMenu.Item onSelect={() => closeProject(p.worktree)}> - <DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - </DropdownMenu.Content> - </DropdownMenu.Portal> - </DropdownMenu> - </div> - </div> - - <Show - when={workspaceSetting()} - fallback={ - <> - <div class="py-4 px-3"> - <TooltipKeybind - title={language.t("command.session.new")} - keybind={command.keybind("session.new")} - placement="top" - > - <Button - size="large" - icon="plus-small" - class="w-full" - onClick={() => { - navigate(`/${base64Encode(p.worktree)}/session`) - layout.mobileSidebar.hide() - }} - > - {language.t("command.session.new")} - </Button> - </TooltipKeybind> - </div> - <div class="flex-1 min-h-0"> - <LocalWorkspace project={p} mobile={sidebarProps.mobile} /> - </div> - </> - } - > - <> - <div class="py-4 px-3"> - <TooltipKeybind - title={language.t("workspace.new")} - keybind={command.keybind("workspace.new")} - placement="top" - > - <Button size="large" icon="plus-small" class="w-full" onClick={createWorkspace}> - {language.t("workspace.new")} - </Button> - </TooltipKeybind> - </div> - <div class="relative 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" - style={{ "overflow-anchor": "none" }} - > - <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={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">{language.t("sidebar.gettingStarted.title")}</div> - <div class="text-text-base">{language.t("sidebar.gettingStarted.line1")}</div> - <div class="text-text-base">{language.t("sidebar.gettingStarted.line2")}</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} - > - {language.t("command.provider.connect")} - </Button> - </div> - </div> - </Show> - </div> + <SidebarPanel project={currentProject()} mobile={sidebarProps.mobile} /> </Show> </div> ) @@ -2576,10 +2628,18 @@ export default function Layout(props: ParentProps) { "relative shrink-0": true, }} style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }} + onMouseLeave={() => setHoverProject(undefined)} > <div class="@container w-full h-full contain-strict"> <SidebarContent /> </div> + <Show when={!layout.sidebar.opened() ? hoverProjectData() : undefined} keyed> + {(project) => ( + <div class="absolute inset-y-0 left-16 z-50 flex"> + <SidebarPanel project={project} /> + </div> + )} + </Show> <Show when={layout.sidebar.opened()}> <ResizeHandle direction="horizontal" |
