|
|
@@ -1,4 +1,4 @@
|
|
|
-import { createMemo, For, Show } from "solid-js"
|
|
|
+import { type Accessor, createMemo, For, Show } from "solid-js"
|
|
|
import { DragDropProvider, PointerSensor } from "@dnd-kit/solid"
|
|
|
import { isSortable, useSortable } from "@dnd-kit/solid/sortable"
|
|
|
import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
|
|
|
@@ -10,98 +10,119 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|
|
-import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
|
|
+import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout"
|
|
|
import { ServerConnection } from "@/context/server"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
|
|
import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu"
|
|
|
import { ServerHealthIndicator } from "@/components/server/server-row"
|
|
|
import { type ServerHealth } from "@/utils/server-health"
|
|
|
-import { type HomeProjectsController } from "./home-projects-controller"
|
|
|
|
|
|
const HOME_ROW_LAYOUT =
|
|
|
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
|
|
|
const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
|
const HOME_PROJECT_NAV_ROW = `${HOME_ROW_LAYOUT} h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:bg-v2-background-bg-layer-03 data-[selected]:text-v2-text-text-base data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-03 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]`
|
|
|
|
|
|
-export function HomeProjectsView(props: { controller: HomeProjectsController; onWheel: (event: WheelEvent) => void }) {
|
|
|
+export type HomeProjectsViewProps = {
|
|
|
+ language: ReturnType<typeof useLanguage>
|
|
|
+ servers: Accessor<ServerConnection.Any[]>
|
|
|
+ projects: Accessor<LocalProject[]>
|
|
|
+ recentlyClosed: Accessor<LocalProject[]>
|
|
|
+ selection: Accessor<HomeProjectSelection>
|
|
|
+ homedir: Accessor<string>
|
|
|
+ serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined
|
|
|
+ projectsForServer: (server: ServerConnection.Any) => LocalProject[]
|
|
|
+ collapsed: (server: ServerConnection.Any) => boolean
|
|
|
+ menuOpen: (id: string) => boolean
|
|
|
+ canDefaultServer: Accessor<boolean>
|
|
|
+ isDefaultServer: (server: ServerConnection.Any) => boolean
|
|
|
+ canRevealProject: (server: ServerConnection.Any) => boolean
|
|
|
+ fileManagerActionLabel: Accessor<string>
|
|
|
+ unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
|
|
|
+ serverMenuID: (server: ServerConnection.Any) => string
|
|
|
+ projectMenuID: (server: ServerConnection.Any, directory: string) => string
|
|
|
+ onWheel: (event: WheelEvent) => void
|
|
|
+ onChooseProject: (server: ServerConnection.Any) => void
|
|
|
+ onFocusServer: (server: ServerConnection.Any) => void
|
|
|
+ onToggleCollapsed: (server: ServerConnection.Any) => void
|
|
|
+ onEditServer: (server: ServerConnection.Http) => void
|
|
|
+ onSetDefaultServer: (server: ServerConnection.Any | undefined) => void
|
|
|
+ onRemoveServer: (server: ServerConnection.Any) => void
|
|
|
+ onSetMenuOpen: (id: string, open: boolean) => void
|
|
|
+ onMoveProject: (server: ServerConnection.Any, worktree: string, index: number) => void
|
|
|
+ onSelectProject: (server: ServerConnection.Any, directory: string) => void
|
|
|
+ onAddProjects: (server: ServerConnection.Any, directories: string[]) => void
|
|
|
+ onOpenProjectNewSession: (server: ServerConnection.Any, directory: string) => void
|
|
|
+ onEditProject: (server: ServerConnection.Any, project: LocalProject) => void
|
|
|
+ onRevealProject: (server: ServerConnection.Any, project: LocalProject) => void
|
|
|
+ onClearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
|
|
|
+ onCloseProject: (server: ServerConnection.Any, directory: string) => void
|
|
|
+ onOpenSettings: () => void
|
|
|
+ onOpenHelp: () => void
|
|
|
+}
|
|
|
+
|
|
|
+export function HomeProjectsView(props: HomeProjectsViewProps) {
|
|
|
return (
|
|
|
<aside
|
|
|
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:sticky lg:top-14 lg:mt-14 lg:h-[calc(100cqh-56px)] lg:self-start lg:pt-[52px]"
|
|
|
- aria-label={props.controller.language.t("home.projects")}
|
|
|
+ aria-label={props.language.t("home.projects")}
|
|
|
onWheel={(event) => {
|
|
|
if (event.target === event.currentTarget) return
|
|
|
props.onWheel(event)
|
|
|
}}
|
|
|
>
|
|
|
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
|
|
- <div class="text-v2-text-text-muted [font-weight:530]">{props.controller.language.t("home.projects")}</div>
|
|
|
+ <div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
|
|
<Show
|
|
|
- when={
|
|
|
- props.controller.servers().length === 1 &&
|
|
|
- !(props.controller.projects().length === 0 && props.controller.recentlyClosed().length > 0)
|
|
|
- }
|
|
|
+ when={props.servers().length === 1 && !(props.projects().length === 0 && props.recentlyClosed().length > 0)}
|
|
|
>
|
|
|
- <TooltipV2 placement="bottom" value={props.controller.language.t("home.project.add")}>
|
|
|
+ <TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
|
|
<IconButtonV2
|
|
|
data-action="home-add-project"
|
|
|
variant="ghost-muted"
|
|
|
size="large"
|
|
|
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
|
|
icon={<IconV2 name="folder-add-left" />}
|
|
|
- disabled={props.controller.serverHealth(props.controller.servers()[0])?.healthy === false}
|
|
|
- onClick={() => props.controller.chooseProject(props.controller.servers()[0])}
|
|
|
- aria-label={props.controller.language.t("home.project.add")}
|
|
|
+ disabled={props.serverHealth(props.servers()[0])?.healthy === false}
|
|
|
+ onClick={() => props.onChooseProject(props.servers()[0])}
|
|
|
+ aria-label={props.language.t("home.project.add")}
|
|
|
/>
|
|
|
</TooltipV2>
|
|
|
</Show>
|
|
|
</div>
|
|
|
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
|
|
|
<Show
|
|
|
- when={props.controller.servers().length > 1}
|
|
|
+ when={props.servers().length > 1}
|
|
|
fallback={
|
|
|
<div class="pr-3">
|
|
|
<Show
|
|
|
- when={props.controller.projects().length > 0}
|
|
|
- fallback={
|
|
|
- <HomeProjectEmpty
|
|
|
- controller={props.controller}
|
|
|
- server={props.controller.servers()[0]}
|
|
|
- recentlyClosed={props.controller.recentlyClosed()}
|
|
|
- />
|
|
|
- }
|
|
|
+ when={props.projects().length > 0}
|
|
|
+ fallback={<HomeProjectEmpty {...props} server={props.servers()[0]} items={props.recentlyClosed()} />}
|
|
|
>
|
|
|
- <HomeProjectList
|
|
|
- controller={props.controller}
|
|
|
- server={props.controller.servers()[0]}
|
|
|
- projects={props.controller.projects()}
|
|
|
- />
|
|
|
+ <HomeProjectList {...props} server={props.servers()[0]} items={props.projects()} />
|
|
|
</Show>
|
|
|
</div>
|
|
|
}
|
|
|
>
|
|
|
<div class="flex min-w-0 flex-col gap-4 pr-3">
|
|
|
- <For each={props.controller.servers()}>
|
|
|
+ <For each={props.servers()}>
|
|
|
{(item) => {
|
|
|
- const projects = () => props.controller.projectsForServer(item)
|
|
|
- const healthy = () => !!props.controller.serverHealth(item)?.healthy
|
|
|
+ const projects = () => props.projectsForServer(item)
|
|
|
+ const healthy = () => !!props.serverHealth(item)?.healthy
|
|
|
const hasProjects = () => projects().length > 0
|
|
|
- const collapsed = () => props.controller.collapsed(item)
|
|
|
+ const collapsed = () => props.collapsed(item)
|
|
|
return (
|
|
|
<div class="flex min-w-0 flex-col gap-1">
|
|
|
<HomeServerRow
|
|
|
server={item}
|
|
|
- controller={props.controller}
|
|
|
- selected={
|
|
|
- props.controller.selection().server === ServerConnection.key(item) &&
|
|
|
- !props.controller.selection().directory
|
|
|
- }
|
|
|
+ {...props}
|
|
|
+ selected={props.selection().server === ServerConnection.key(item) && !props.selection().directory}
|
|
|
collapsed={collapsed()}
|
|
|
- health={props.controller.serverHealth(item)}
|
|
|
+ health={props.serverHealth(item)}
|
|
|
/>
|
|
|
<Show when={healthy() && hasProjects() && !collapsed()}>
|
|
|
<div class="mx-3 h-px bg-v2-border-border-base" />
|
|
|
- <HomeProjectList controller={props.controller} server={item} projects={projects()} />
|
|
|
+ <HomeProjectList {...props} server={item} items={projects()} />
|
|
|
</Show>
|
|
|
</div>
|
|
|
)
|
|
|
@@ -112,9 +133,9 @@ export function HomeProjectsView(props: { controller: HomeProjectsController; on
|
|
|
</ScrollView>
|
|
|
<HomeUtilityNav
|
|
|
class="mb-8 mt-4 hidden shrink-0 lg:flex"
|
|
|
- openSettings={props.controller.openSettings}
|
|
|
- openHelp={props.controller.openHelp}
|
|
|
- language={props.controller.language}
|
|
|
+ onOpenSettings={props.onOpenSettings}
|
|
|
+ onOpenHelp={props.onOpenHelp}
|
|
|
+ language={props.language}
|
|
|
/>
|
|
|
</aside>
|
|
|
)
|
|
|
@@ -122,8 +143,8 @@ export function HomeProjectsView(props: { controller: HomeProjectsController; on
|
|
|
|
|
|
export function HomeUtilityNav(props: {
|
|
|
class?: string
|
|
|
- openSettings: () => void
|
|
|
- openHelp: () => void
|
|
|
+ onOpenSettings: () => void
|
|
|
+ onOpenHelp: () => void
|
|
|
language: ReturnType<typeof useLanguage>
|
|
|
}) {
|
|
|
return (
|
|
|
@@ -131,7 +152,7 @@ export function HomeUtilityNav(props: {
|
|
|
<button
|
|
|
type="button"
|
|
|
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
|
|
- onClick={props.openSettings}
|
|
|
+ onClick={props.onOpenSettings}
|
|
|
>
|
|
|
<IconV2 name="settings-gear" size="small" />
|
|
|
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.settings")}</span>
|
|
|
@@ -139,7 +160,7 @@ export function HomeUtilityNav(props: {
|
|
|
<button
|
|
|
type="button"
|
|
|
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
|
|
- onClick={props.openHelp}
|
|
|
+ onClick={props.onOpenHelp}
|
|
|
>
|
|
|
<IconV2 name="help" size="small" />
|
|
|
<span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.help")}</span>
|
|
|
@@ -149,15 +170,27 @@ export function HomeUtilityNav(props: {
|
|
|
}
|
|
|
|
|
|
function HomeServerRow(props: {
|
|
|
- controller: HomeProjectsController
|
|
|
+ language: HomeProjectsViewProps["language"]
|
|
|
+ projectsForServer: HomeProjectsViewProps["projectsForServer"]
|
|
|
+ menuOpen: HomeProjectsViewProps["menuOpen"]
|
|
|
+ canDefaultServer: HomeProjectsViewProps["canDefaultServer"]
|
|
|
+ isDefaultServer: HomeProjectsViewProps["isDefaultServer"]
|
|
|
+ serverMenuID: HomeProjectsViewProps["serverMenuID"]
|
|
|
+ onFocusServer: HomeProjectsViewProps["onFocusServer"]
|
|
|
+ onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"]
|
|
|
+ onEditServer: HomeProjectsViewProps["onEditServer"]
|
|
|
+ onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"]
|
|
|
+ onRemoveServer: HomeProjectsViewProps["onRemoveServer"]
|
|
|
+ onSetMenuOpen: HomeProjectsViewProps["onSetMenuOpen"]
|
|
|
+ onChooseProject: HomeProjectsViewProps["onChooseProject"]
|
|
|
server: ServerConnection.Any
|
|
|
selected: boolean
|
|
|
collapsed: boolean
|
|
|
health: ServerHealth | undefined
|
|
|
}) {
|
|
|
const healthy = () => !!props.health?.healthy
|
|
|
- const canToggle = () => healthy() && props.controller.projectsForServer(props.server).length > 0
|
|
|
- const menuID = () => props.controller.serverMenuID(props.server)
|
|
|
+ const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0
|
|
|
+ const menuID = () => props.serverMenuID(props.server)
|
|
|
return (
|
|
|
<div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
|
|
|
<button
|
|
|
@@ -165,7 +198,7 @@ function HomeServerRow(props: {
|
|
|
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
|
|
|
data-selected={props.selected ? "" : undefined}
|
|
|
disabled={!healthy()}
|
|
|
- onClick={() => props.controller.focusServer(props.server)}
|
|
|
+ onClick={() => props.onFocusServer(props.server)}
|
|
|
>
|
|
|
<span
|
|
|
data-action="home-server-collapse"
|
|
|
@@ -175,9 +208,7 @@ function HomeServerRow(props: {
|
|
|
"cursor-default opacity-40": !canToggle(),
|
|
|
}}
|
|
|
aria-label={
|
|
|
- props.collapsed
|
|
|
- ? props.controller.language.t("home.server.expand")
|
|
|
- : props.controller.language.t("home.server.collapse")
|
|
|
+ props.collapsed ? props.language.t("home.server.expand") : props.language.t("home.server.collapse")
|
|
|
}
|
|
|
aria-disabled={!canToggle()}
|
|
|
aria-expanded={canToggle() ? !props.collapsed : undefined}
|
|
|
@@ -185,7 +216,7 @@ function HomeServerRow(props: {
|
|
|
event.preventDefault()
|
|
|
event.stopPropagation()
|
|
|
if (!canToggle()) return
|
|
|
- props.controller.toggleCollapsed(props.server)
|
|
|
+ props.onToggleCollapsed(props.server)
|
|
|
}}
|
|
|
onPointerDown={(event) => event.preventDefault()}
|
|
|
>
|
|
|
@@ -212,33 +243,29 @@ function HomeServerRow(props: {
|
|
|
</button>
|
|
|
<div
|
|
|
class="hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
|
|
|
- data-menu={props.controller.menuOpen(menuID())}
|
|
|
+ data-menu={props.menuOpen(menuID())}
|
|
|
>
|
|
|
<ServerRowMenuView
|
|
|
server={props.server}
|
|
|
- labels={serverMenuLabels(props.controller.language)}
|
|
|
- canDefault={props.controller.canDefaultServer()}
|
|
|
- isDefault={props.controller.isDefaultServer(props.server)}
|
|
|
- onEdit={props.controller.openEditServer}
|
|
|
- onSetDefault={() => props.controller.setDefaultServer(props.server)}
|
|
|
- onRemoveDefault={() => props.controller.setDefaultServer(undefined)}
|
|
|
- onRemove={() => props.controller.removeServer(props.server)}
|
|
|
- open={props.controller.menuOpen(menuID())}
|
|
|
- onOpenChange={(open) => props.controller.setMenuOpen(menuID(), open)}
|
|
|
+ labels={serverMenuLabels(props.language)}
|
|
|
+ canDefault={props.canDefaultServer()}
|
|
|
+ isDefault={props.isDefaultServer(props.server)}
|
|
|
+ onEdit={props.onEditServer}
|
|
|
+ onSetDefault={() => props.onSetDefaultServer(props.server)}
|
|
|
+ onRemoveDefault={() => props.onSetDefaultServer(undefined)}
|
|
|
+ onRemove={() => props.onRemoveServer(props.server)}
|
|
|
+ open={props.menuOpen(menuID())}
|
|
|
+ onOpenChange={(open) => props.onSetMenuOpen(menuID(), open)}
|
|
|
/>
|
|
|
- <TooltipV2
|
|
|
- class="flex shrink-0 items-center"
|
|
|
- placement="bottom"
|
|
|
- value={props.controller.language.t("home.project.add")}
|
|
|
- >
|
|
|
+ <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
|
|
|
<IconButtonV2
|
|
|
data-action="home-add-project"
|
|
|
variant="ghost-muted"
|
|
|
size="small"
|
|
|
icon={<IconV2 name="folder-add-left" />}
|
|
|
- aria-label={props.controller.language.t("home.project.add")}
|
|
|
+ aria-label={props.language.t("home.project.add")}
|
|
|
disabled={props.health?.healthy === false}
|
|
|
- onClick={() => props.controller.chooseProject(props.server)}
|
|
|
+ onClick={() => props.onChooseProject(props.server)}
|
|
|
/>
|
|
|
</TooltipV2>
|
|
|
</div>
|
|
|
@@ -246,10 +273,9 @@ function HomeServerRow(props: {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-type HomeProjectListProps = {
|
|
|
- controller: HomeProjectsController
|
|
|
+type HomeProjectListProps = HomeProjectsViewProps & {
|
|
|
server: ServerConnection.Any
|
|
|
- projects: LocalProject[]
|
|
|
+ items: LocalProject[]
|
|
|
}
|
|
|
|
|
|
function HomeProjectList(props: HomeProjectListProps) {
|
|
|
@@ -276,10 +302,9 @@ function HomeProjectList(props: HomeProjectListProps) {
|
|
|
onDragEnd={(event) => {
|
|
|
const source = event.operation.source
|
|
|
if (event.canceled || !isSortable(source)) return
|
|
|
- if (source.initialIndex !== source.index)
|
|
|
- props.controller.moveProject(props.server, source.id.toString(), source.index)
|
|
|
- if (props.controller.selection().server !== ServerConnection.key(props.server))
|
|
|
- props.controller.selectProject(props.server, source.id.toString())
|
|
|
+ if (source.initialIndex !== source.index) props.onMoveProject(props.server, source.id.toString(), source.index)
|
|
|
+ if (props.selection().server !== ServerConnection.key(props.server))
|
|
|
+ props.onSelectProject(props.server, source.id.toString())
|
|
|
}}
|
|
|
>
|
|
|
<div class="flex min-w-0 flex-col gap-1" ref={listRef}>
|
|
|
@@ -288,7 +313,7 @@ function HomeProjectList(props: HomeProjectListProps) {
|
|
|
remounts all rows — killing any in-flight drag activation (the
|
|
|
row's sortable unregisters on unmount) and discarding animations.
|
|
|
String keys keep row elements alive and move them on reorder. */}
|
|
|
- <For each={props.projects.map((project) => project.worktree)}>
|
|
|
+ <For each={props.items.map((project) => project.worktree)}>
|
|
|
{(worktree, index) => <HomeProjectSlot {...props} worktree={worktree} index={index} />}
|
|
|
</For>
|
|
|
</div>
|
|
|
@@ -302,34 +327,35 @@ function HomeProjectSlot(
|
|
|
index: () => number
|
|
|
},
|
|
|
) {
|
|
|
- const project = createMemo(() => props.projects.find((item) => item.worktree === props.worktree))
|
|
|
+ const project = createMemo(() => props.items.find((item) => item.worktree === props.worktree))
|
|
|
|
|
|
return (
|
|
|
<Show when={project()}>
|
|
|
{(item) => (
|
|
|
<HomeProjectRow
|
|
|
- controller={props.controller}
|
|
|
+ {...props}
|
|
|
project={item()}
|
|
|
server={props.server}
|
|
|
index={props.index}
|
|
|
- serverSelected={props.controller.selection().server === ServerConnection.key(props.server)}
|
|
|
+ serverSelected={props.selection().server === ServerConnection.key(props.server)}
|
|
|
selected={
|
|
|
- props.controller.selection().server === ServerConnection.key(props.server) &&
|
|
|
- props.controller.selection().directory === props.worktree
|
|
|
+ props.selection().server === ServerConnection.key(props.server) &&
|
|
|
+ props.selection().directory === props.worktree
|
|
|
}
|
|
|
- unseenCount={props.controller.unseenCount(props.server, item())}
|
|
|
+ unseen={props.unseenCount(props.server, item())}
|
|
|
/>
|
|
|
)}
|
|
|
</Show>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function HomeProjectEmpty(props: {
|
|
|
- controller: HomeProjectsController
|
|
|
- server: ServerConnection.Any
|
|
|
- recentlyClosed: LocalProject[]
|
|
|
-}) {
|
|
|
- const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false
|
|
|
+function HomeProjectEmpty(
|
|
|
+ props: HomeProjectsViewProps & {
|
|
|
+ server: ServerConnection.Any
|
|
|
+ items: LocalProject[]
|
|
|
+ },
|
|
|
+) {
|
|
|
+ const unreachable = () => props.serverHealth(props.server)?.healthy === false
|
|
|
return (
|
|
|
<div class="flex min-w-0 flex-col gap-1">
|
|
|
<button
|
|
|
@@ -337,33 +363,32 @@ function HomeProjectEmpty(props: {
|
|
|
data-action="home-add-project-row"
|
|
|
class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60 [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
|
|
|
disabled={unreachable()}
|
|
|
- onClick={() => props.controller.chooseProject(props.server)}
|
|
|
+ onClick={() => props.onChooseProject(props.server)}
|
|
|
>
|
|
|
<IconV2 name="folder-add-left" size="small" />
|
|
|
- <span class={HOME_PROJECT_NAV_LABEL}>{props.controller.language.t("home.project.add")}</span>
|
|
|
+ <span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
|
|
|
</button>
|
|
|
- <Show when={props.recentlyClosed.length > 0}>
|
|
|
+ <Show when={props.items.length > 0}>
|
|
|
<div class="mt-3 flex h-7 min-w-0 shrink-0 items-center pl-1.5 pr-3">
|
|
|
- <div class="text-v2-text-text-faint [font-weight:530]">
|
|
|
- {props.controller.language.t("home.recentlyClosed")}
|
|
|
- </div>
|
|
|
+ <div class="text-v2-text-text-faint [font-weight:530]">{props.language.t("home.recentlyClosed")}</div>
|
|
|
</div>
|
|
|
- <For each={props.recentlyClosed}>
|
|
|
- {(project) => <HomeRecentlyClosedRow project={project} controller={props.controller} server={props.server} />}
|
|
|
+ <For each={props.items}>
|
|
|
+ {(project) => <HomeRecentlyClosedRow {...props} project={project} server={props.server} />}
|
|
|
</For>
|
|
|
</Show>
|
|
|
</div>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function HomeRecentlyClosedRow(props: {
|
|
|
- controller: HomeProjectsController
|
|
|
- project: LocalProject
|
|
|
- server: ServerConnection.Any
|
|
|
-}) {
|
|
|
- const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false
|
|
|
+function HomeRecentlyClosedRow(
|
|
|
+ props: HomeProjectsViewProps & {
|
|
|
+ project: LocalProject
|
|
|
+ server: ServerConnection.Any
|
|
|
+ },
|
|
|
+) {
|
|
|
+ const unreachable = () => props.serverHealth(props.server)?.healthy === false
|
|
|
const path = () => {
|
|
|
- const home = props.controller.homedir()
|
|
|
+ const home = props.homedir()
|
|
|
const worktree = props.project.worktree
|
|
|
if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}`
|
|
|
return worktree
|
|
|
@@ -375,7 +400,7 @@ function HomeRecentlyClosedRow(props: {
|
|
|
data-component="home-recently-closed-row"
|
|
|
class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60`}
|
|
|
disabled={unreachable()}
|
|
|
- onClick={() => props.controller.addProjects(props.server, [props.project.worktree])}
|
|
|
+ onClick={() => props.onAddProjects(props.server, [props.project.worktree])}
|
|
|
>
|
|
|
<HomeProjectAvatar project={props.project} outline />
|
|
|
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
|
|
|
@@ -384,16 +409,17 @@ function HomeRecentlyClosedRow(props: {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function HomeProjectRow(props: {
|
|
|
- controller: HomeProjectsController
|
|
|
- project: LocalProject
|
|
|
- server: ServerConnection.Any
|
|
|
- index: () => number
|
|
|
- serverSelected: boolean
|
|
|
- selected: boolean
|
|
|
- unseenCount: number
|
|
|
-}) {
|
|
|
- const serverUnreachable = () => props.controller.serverHealth(props.server)?.healthy === false
|
|
|
+function HomeProjectRow(
|
|
|
+ props: HomeProjectsViewProps & {
|
|
|
+ project: LocalProject
|
|
|
+ server: ServerConnection.Any
|
|
|
+ index: () => number
|
|
|
+ serverSelected: boolean
|
|
|
+ selected: boolean
|
|
|
+ unseen: number
|
|
|
+ },
|
|
|
+) {
|
|
|
+ const serverUnreachable = () => props.serverHealth(props.server)?.healthy === false
|
|
|
const sortable = useSortable({
|
|
|
get id() {
|
|
|
return props.project.worktree
|
|
|
@@ -403,7 +429,7 @@ function HomeProjectRow(props: {
|
|
|
},
|
|
|
})
|
|
|
let pointerDownSelected: boolean | undefined
|
|
|
- const menuID = () => props.controller.projectMenuID(props.server, props.project.worktree)
|
|
|
+ const menuID = () => props.projectMenuID(props.server, props.project.worktree)
|
|
|
return (
|
|
|
<div
|
|
|
ref={sortable.ref}
|
|
|
@@ -432,7 +458,7 @@ function HomeProjectRow(props: {
|
|
|
if (event.button !== 0 || event.pointerType === "touch") return
|
|
|
if (!props.serverSelected) return
|
|
|
pointerDownSelected = props.selected
|
|
|
- if (!props.selected) props.controller.selectProject(props.server, props.project.worktree)
|
|
|
+ if (!props.selected) props.onSelectProject(props.server, props.project.worktree)
|
|
|
}}
|
|
|
onClick={(event) => {
|
|
|
// The drag sensor calls preventDefault on post-drag clicks; never
|
|
|
@@ -440,12 +466,12 @@ function HomeProjectRow(props: {
|
|
|
if (event.defaultPrevented) return
|
|
|
// Keyboard activation and touch taps keep the original toggle.
|
|
|
if (event.detail === 0 || pointerDownSelected === undefined) {
|
|
|
- props.controller.selectProject(props.server, props.project.worktree)
|
|
|
+ props.onSelectProject(props.server, props.project.worktree)
|
|
|
return
|
|
|
}
|
|
|
// Mouse: pointerdown already selected unselected rows; a plain click
|
|
|
// on an already-selected row toggles it off.
|
|
|
- if (pointerDownSelected) props.controller.selectProject(props.server, props.project.worktree)
|
|
|
+ if (pointerDownSelected) props.onSelectProject(props.server, props.project.worktree)
|
|
|
pointerDownSelected = undefined
|
|
|
}}
|
|
|
>
|
|
|
@@ -454,14 +480,14 @@ function HomeProjectRow(props: {
|
|
|
</button>
|
|
|
<div
|
|
|
class="hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/project:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
|
|
|
- data-menu={props.controller.menuOpen(menuID())}
|
|
|
+ data-menu={props.menuOpen(menuID())}
|
|
|
>
|
|
|
<MenuV2
|
|
|
gutter={6}
|
|
|
modal={false}
|
|
|
placement="bottom-end"
|
|
|
- open={props.controller.menuOpen(menuID())}
|
|
|
- onOpenChange={(open) => props.controller.setMenuOpen(menuID(), open)}
|
|
|
+ open={props.menuOpen(menuID())}
|
|
|
+ onOpenChange={(open) => props.onSetMenuOpen(menuID(), open)}
|
|
|
>
|
|
|
<MenuV2.Trigger
|
|
|
as={IconButtonV2}
|
|
|
@@ -469,32 +495,30 @@ function HomeProjectRow(props: {
|
|
|
variant="ghost-muted"
|
|
|
size="small"
|
|
|
icon={<IconV2 name="outline-dots" />}
|
|
|
- aria-label={props.controller.language.t("common.moreOptions")}
|
|
|
+ aria-label={props.language.t("common.moreOptions")}
|
|
|
/>
|
|
|
<MenuV2.Portal>
|
|
|
<MenuV2.Content>
|
|
|
- <MenuV2.Item
|
|
|
- onSelect={() => props.controller.openProjectNewSession(props.server, props.project.worktree)}
|
|
|
- >
|
|
|
- {props.controller.language.t("command.session.new")}
|
|
|
+ <MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
|
|
|
+ {props.language.t("command.session.new")}
|
|
|
</MenuV2.Item>
|
|
|
- <MenuV2.Item onSelect={() => props.controller.editProject(props.server, props.project)}>
|
|
|
- {props.controller.language.t("dialog.project.edit.title")}
|
|
|
+ <MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
|
|
|
+ {props.language.t("dialog.project.edit.title")}
|
|
|
</MenuV2.Item>
|
|
|
- <Show when={props.controller.canRevealProject(props.server)}>
|
|
|
- <MenuV2.Item onSelect={() => props.controller.revealProject(props.server, props.project)}>
|
|
|
- {props.controller.fileManagerActionLabel()}
|
|
|
+ <Show when={props.canRevealProject(props.server)}>
|
|
|
+ <MenuV2.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
|
|
|
+ {props.fileManagerActionLabel()}
|
|
|
</MenuV2.Item>
|
|
|
</Show>
|
|
|
<MenuV2.Item
|
|
|
- disabled={props.unseenCount === 0}
|
|
|
- onSelect={() => props.controller.clearNotifications(props.server, props.project)}
|
|
|
+ disabled={props.unseen === 0}
|
|
|
+ onSelect={() => props.onClearNotifications(props.server, props.project)}
|
|
|
>
|
|
|
- {props.controller.language.t("sidebar.project.clearNotifications")}
|
|
|
+ {props.language.t("sidebar.project.clearNotifications")}
|
|
|
</MenuV2.Item>
|
|
|
<MenuV2.Separator />
|
|
|
- <MenuV2.Item onSelect={() => props.controller.closeProject(props.server, props.project.worktree)}>
|
|
|
- {props.controller.language.t("common.close")}
|
|
|
+ <MenuV2.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
|
|
|
+ {props.language.t("common.close")}
|
|
|
</MenuV2.Item>
|
|
|
</MenuV2.Content>
|
|
|
</MenuV2.Portal>
|
|
|
@@ -504,8 +528,8 @@ function HomeProjectRow(props: {
|
|
|
variant="ghost-muted"
|
|
|
size="small"
|
|
|
icon={<IconV2 name="edit" />}
|
|
|
- aria-label={props.controller.language.t("command.session.new")}
|
|
|
- onClick={() => props.controller.openProjectNewSession(props.server, props.project.worktree)}
|
|
|
+ aria-label={props.language.t("command.session.new")}
|
|
|
+ onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|