wip(app): sidebar hover full

This commit is contained in:
adamelmore
2026-01-26 06:28:25 -06:00
parent 7f75f71f6b
commit af3d8c383e

View File

@@ -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"
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}
/>
<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>
}
>
<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>
</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>
</div>
</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>
</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 sync = useGlobalSync()
const project = createMemo(() => currentProject())
const projectName = createMemo(() => {
const current = project()
if (!current) return ""
return current.name || getFilename(current.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 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),
})
if (!created?.directory) return
const local = current.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", 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]
return undefined
})
globalSync.child(created.directory)
navigate(`/${base64Encode(created.directory)}/session`)
layout.mobileSidebar.hide()
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 project = panelProps.project
if (!project) return ""
return project.name || getFilename(project.worktree)
})
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)
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>
<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={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>
)
}
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"