refactor(app): detach home views from controllers
This commit is contained in:
parent
1612baf254
commit
b751e26565
4 changed files with 360 additions and 230 deletions
|
|
@ -18,12 +18,82 @@ export function NewHome() {
|
|||
onWheel={sessions.scroll.containOuterWheel}
|
||||
>
|
||||
<div class="mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
||||
<HomeProjectsView controller={projects} onWheel={sessions.scroll.containWheel} />
|
||||
<HomeSessionsView controller={sessions} />
|
||||
<HomeProjectsView
|
||||
language={projects.language}
|
||||
servers={projects.servers}
|
||||
projects={projects.projects}
|
||||
recentlyClosed={projects.recentlyClosed}
|
||||
selection={projects.selection}
|
||||
homedir={projects.homedir}
|
||||
serverHealth={projects.serverHealth}
|
||||
projectsForServer={projects.projectsForServer}
|
||||
collapsed={projects.collapsed}
|
||||
menuOpen={projects.menuOpen}
|
||||
canDefaultServer={projects.canDefaultServer}
|
||||
isDefaultServer={projects.isDefaultServer}
|
||||
canRevealProject={projects.canRevealProject}
|
||||
fileManagerActionLabel={projects.fileManagerActionLabel}
|
||||
unseenCount={projects.unseenCount}
|
||||
serverMenuID={projects.serverMenuID}
|
||||
projectMenuID={projects.projectMenuID}
|
||||
onWheel={sessions.scroll.containWheel}
|
||||
onChooseProject={projects.chooseProject}
|
||||
onFocusServer={projects.focusServer}
|
||||
onToggleCollapsed={projects.toggleCollapsed}
|
||||
onEditServer={projects.openEditServer}
|
||||
onSetDefaultServer={projects.setDefaultServer}
|
||||
onRemoveServer={projects.removeServer}
|
||||
onSetMenuOpen={projects.setMenuOpen}
|
||||
onMoveProject={projects.moveProject}
|
||||
onSelectProject={projects.selectProject}
|
||||
onAddProjects={projects.addProjects}
|
||||
onOpenProjectNewSession={projects.openProjectNewSession}
|
||||
onEditProject={projects.editProject}
|
||||
onRevealProject={projects.revealProject}
|
||||
onClearNotifications={projects.clearNotifications}
|
||||
onCloseProject={projects.closeProject}
|
||||
onOpenSettings={projects.openSettings}
|
||||
onOpenHelp={projects.openHelp}
|
||||
/>
|
||||
<HomeSessionsView
|
||||
language={sessions.language}
|
||||
groups={sessions.groups}
|
||||
loading={sessions.loading}
|
||||
showProjectName={sessions.showProjectName}
|
||||
server={sessions.server}
|
||||
canCreateSession={sessions.canCreateSession}
|
||||
searchValue={sessions.search.value}
|
||||
searchPlaceholder={sessions.search.placeholder}
|
||||
searchOpen={sessions.search.open}
|
||||
searchLoading={sessions.search.loading}
|
||||
searchResults={sessions.search.results}
|
||||
searchActive={sessions.search.active}
|
||||
searchNoResultsLabel={sessions.search.noResultsLabel}
|
||||
titleOpacity={sessions.header.titleOpacity}
|
||||
isOpenTab={sessions.hasOpenTab}
|
||||
onCreateSession={sessions.createSession}
|
||||
onOpenSession={sessions.openSession}
|
||||
onArchiveSession={sessions.archiveSession}
|
||||
onSetHoverTarget={sessions.scroll.setHoverTarget}
|
||||
onSetThumbTrack={sessions.scroll.setThumbTrack}
|
||||
onSetContent={sessions.header.setContent}
|
||||
onSetHeader={sessions.header.setHeader}
|
||||
onWheel={sessions.scroll.containWheel}
|
||||
onSetSearchRoot={sessions.search.setRoot}
|
||||
onSetSearchInput={sessions.search.setInput}
|
||||
onSetSearchList={sessions.search.setList}
|
||||
onSearchFocus={sessions.search.focus}
|
||||
onSearchInput={sessions.search.input}
|
||||
onSearchClose={sessions.search.close}
|
||||
onSearchMove={sessions.search.move}
|
||||
onSearchSelectActive={sessions.search.selectActive}
|
||||
onSearchHighlight={sessions.search.highlight}
|
||||
onSearchSelect={sessions.search.select}
|
||||
/>
|
||||
<HomeUtilityNav
|
||||
class="flex lg:hidden"
|
||||
openSettings={projects.openSettings}
|
||||
openHelp={projects.openHelp}
|
||||
onOpenSettings={projects.openSettings}
|
||||
onOpenHelp={projects.openHelp}
|
||||
language={projects.language}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -560,18 +560,19 @@ function createHomeSessionHeaderController(groups: () => HomeSessionGroup[]) {
|
|||
export type HomeSessionsController = ReturnType<typeof createHomeSessionsController>
|
||||
|
||||
export function HomeSessionStatusController(props: {
|
||||
controller: HomeSessionsController
|
||||
server: Accessor<ServerConnection.Key>
|
||||
record: HomeSessionRecord
|
||||
isOpenTab: (record: HomeSessionRecord) => boolean
|
||||
render: (state: { unread: Accessor<boolean>; loading: Accessor<boolean>; open: Accessor<boolean> }) => JSX.Element
|
||||
}) {
|
||||
const avatar = useSessionTabAvatarState(
|
||||
props.controller.server,
|
||||
props.server,
|
||||
() => props.record.session.directory,
|
||||
() => props.record.session.id,
|
||||
)
|
||||
return props.render({
|
||||
unread: avatar.unread,
|
||||
loading: avatar.loading,
|
||||
open: () => props.controller.hasOpenTab(props.record),
|
||||
open: () => props.isOpenTab(props.record),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { type ComponentProps, createMemo, For, Show } from "solid-js"
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { type Accessor, createMemo, For, Show } from "solid-js"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
|
|
@ -6,14 +7,16 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
|
||||
import { sessionTitle } from "@/utils/session-title"
|
||||
import { shouldOpenSessionInBackground } from "../home-session-open"
|
||||
import {
|
||||
HomeSessionStatusController,
|
||||
homeSessionSearchKey,
|
||||
type HomeSessionGroup,
|
||||
type HomeSessionRecord,
|
||||
type HomeSessionsController,
|
||||
type OpenSessionOptions,
|
||||
} from "./home-sessions-controller"
|
||||
|
||||
const SHOW_HOME_SESSION_ARCHIVE = false
|
||||
|
|
@ -43,19 +46,52 @@ function isBackgroundOpen(event: MouseEvent) {
|
|||
})
|
||||
}
|
||||
|
||||
export function HomeSessionsView(props: { controller: HomeSessionsController }) {
|
||||
export type HomeSessionsViewProps = {
|
||||
language: ReturnType<typeof useLanguage>
|
||||
groups: Accessor<HomeSessionGroup[]>
|
||||
loading: Accessor<boolean>
|
||||
showProjectName: Accessor<boolean>
|
||||
server: Accessor<ServerConnection.Key>
|
||||
canCreateSession: Accessor<boolean>
|
||||
searchValue: Accessor<string>
|
||||
searchPlaceholder: Accessor<string>
|
||||
searchOpen: Accessor<boolean>
|
||||
searchLoading: Accessor<boolean>
|
||||
searchResults: Accessor<HomeSessionRecord[]>
|
||||
searchActive: Accessor<string>
|
||||
searchNoResultsLabel: Accessor<string>
|
||||
titleOpacity: (id: HomeSessionGroup["id"]) => number
|
||||
isOpenTab: (record: HomeSessionRecord) => boolean
|
||||
onCreateSession: () => void
|
||||
onOpenSession: (session: Session, options?: OpenSessionOptions) => void
|
||||
onArchiveSession: (session: Session) => Promise<void>
|
||||
onSetHoverTarget: (element: HTMLElement) => void
|
||||
onSetThumbTrack: (element: HTMLDivElement) => void
|
||||
onSetContent: (element: HTMLDivElement) => void
|
||||
onSetHeader: (id: HomeSessionGroup["id"], element: HTMLDivElement) => void
|
||||
onWheel: (event: WheelEvent) => void
|
||||
onSetSearchRoot: (element: HTMLDivElement) => void
|
||||
onSetSearchInput: (element: HTMLInputElement) => void
|
||||
onSetSearchList: (element: HTMLDivElement) => void
|
||||
onSearchFocus: () => void
|
||||
onSearchInput: (value: string) => void
|
||||
onSearchClose: () => void
|
||||
onSearchMove: (delta: number) => void
|
||||
onSearchSelectActive: () => void
|
||||
onSearchHighlight: (record: HomeSessionRecord) => void
|
||||
onSearchSelect: (record: HomeSessionRecord, options?: OpenSessionOptions) => void
|
||||
}
|
||||
|
||||
export function HomeSessionsView(props: HomeSessionsViewProps) {
|
||||
return (
|
||||
<section
|
||||
ref={props.controller.scroll.setHoverTarget}
|
||||
ref={props.onSetHoverTarget}
|
||||
class="min-h-0 min-w-0 flex-1 flex flex-col"
|
||||
aria-label={props.controller.language.t("sidebar.project.recentSessions")}
|
||||
aria-label={props.language.t("sidebar.project.recentSessions")}
|
||||
>
|
||||
<div
|
||||
class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12"
|
||||
onWheel={props.controller.scroll.containWheel}
|
||||
>
|
||||
<HomeSessionSearch controller={props.controller} />
|
||||
<Show when={props.controller.groups().length > 0 && props.controller.canCreateSession()}>
|
||||
<div class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12" onWheel={props.onWheel}>
|
||||
<HomeSessionSearch {...props} />
|
||||
<Show when={props.groups().length > 0 && props.canCreateSession()}>
|
||||
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
||||
<ButtonV2
|
||||
data-action="home-new-session"
|
||||
|
|
@ -63,54 +99,52 @@ export function HomeSessionsView(props: { controller: HomeSessionsController })
|
|||
size="normal"
|
||||
icon="edit"
|
||||
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
||||
onClick={props.controller.createSession}
|
||||
onClick={props.onCreateSession}
|
||||
>
|
||||
{props.controller.language.t("command.session.new")}
|
||||
{props.language.t("command.session.new")}
|
||||
</ButtonV2>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
|
||||
<div
|
||||
ref={props.controller.scroll.setThumbTrack}
|
||||
ref={props.onSetThumbTrack}
|
||||
data-component="home-session-scroll-track"
|
||||
class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="-mr-3 min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
|
||||
<Show
|
||||
when={!props.controller.loading()}
|
||||
when={!props.loading()}
|
||||
fallback={
|
||||
<div class="pt-3">
|
||||
<HomeSessionSkeleton label={props.controller.language.t("common.loading")} />
|
||||
<HomeSessionSkeleton label={props.language.t("common.loading")} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.controller.groups().length > 0}
|
||||
when={props.groups().length > 0}
|
||||
fallback={
|
||||
<HomeSessionsEmpty
|
||||
onNewSession={props.controller.canCreateSession() ? props.controller.createSession : undefined}
|
||||
language={props.controller.language}
|
||||
onNewSession={props.canCreateSession() ? props.onCreateSession : undefined}
|
||||
language={props.language}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div ref={props.controller.header.setContent} class="flex flex-col pt-3 pr-3 pb-16">
|
||||
<For each={props.controller.groups()}>
|
||||
<div ref={props.onSetContent} class="flex flex-col pt-3 pr-3 pb-16">
|
||||
<For each={props.groups()}>
|
||||
{(group, index) => (
|
||||
<>
|
||||
<HomeSessionGroupHeader
|
||||
title={group.title}
|
||||
titleOpacity={props.controller.header.titleOpacity(group.id)}
|
||||
ref={(element) => props.controller.header.setHeader(group.id, element)}
|
||||
titleOpacity={props.titleOpacity(group.id)}
|
||||
onSetRef={(element) => props.onSetHeader(group.id, element)}
|
||||
elevated={index() === 0}
|
||||
/>
|
||||
<div
|
||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === props.controller.groups().length - 1 ? "" : "mb-6"}`}
|
||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === props.groups().length - 1 ? "" : "mb-6"}`}
|
||||
>
|
||||
<For each={group.sessions}>
|
||||
{(record) => <HomeSessionRow controller={props.controller} record={record} />}
|
||||
</For>
|
||||
<For each={group.sessions}>{(record) => <HomeSessionRow {...props} record={record} />}</For>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -124,14 +158,16 @@ export function HomeSessionsView(props: { controller: HomeSessionsController })
|
|||
}
|
||||
|
||||
function HomeSessionLeadingController(props: {
|
||||
controller: HomeSessionsController
|
||||
server: HomeSessionsViewProps["server"]
|
||||
isOpenTab: HomeSessionsViewProps["isOpenTab"]
|
||||
record: HomeSessionRecord
|
||||
revealProjectOnHover: boolean
|
||||
}) {
|
||||
return (
|
||||
<HomeSessionStatusController
|
||||
controller={props.controller}
|
||||
server={props.server}
|
||||
record={props.record}
|
||||
isOpenTab={props.isOpenTab}
|
||||
render={(state) => (
|
||||
<HomeSessionLeading
|
||||
record={props.record}
|
||||
|
|
@ -172,11 +208,11 @@ function HomeSessionLeading(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function HomeSessionSearch(props: { controller: HomeSessionsController }) {
|
||||
function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
return (
|
||||
<div class="w-full">
|
||||
<div ref={props.controller.search.setRoot} data-component="home-session-search" class="relative z-30 w-full">
|
||||
<Show when={props.controller.search.open()}>
|
||||
<div ref={props.onSetSearchRoot} data-component="home-session-search" class="relative z-30 w-full">
|
||||
<Show when={props.searchOpen()}>
|
||||
<div
|
||||
data-component="home-session-search-panel"
|
||||
class="absolute flex flex-col overflow-hidden rounded-[12px] bg-v2-background-bg-base shadow-[var(--v2-elevation-floating)]"
|
||||
|
|
@ -185,7 +221,7 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
|
|||
<div class="flex flex-col pt-9">
|
||||
<div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4">
|
||||
<Show
|
||||
when={!props.controller.search.loading()}
|
||||
when={!props.searchLoading()}
|
||||
fallback={
|
||||
<div class="flex items-center justify-center px-4 py-3 text-v2-text-text-muted [font-weight:440]">
|
||||
<Spinner class="size-4" />
|
||||
|
|
@ -193,25 +229,25 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
|
|||
}
|
||||
>
|
||||
<Show
|
||||
when={props.controller.search.results().length > 0}
|
||||
when={props.searchResults().length > 0}
|
||||
fallback={
|
||||
<p class="my-1.5 px-4 pb-2 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
|
||||
{props.controller.search.noResultsLabel()}
|
||||
{props.searchNoResultsLabel()}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<p class="my-1.5 pl-[18px] pr-6 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
|
||||
{props.controller.language.t("home.sessions.search.sessions")}
|
||||
{props.language.t("home.sessions.search.sessions")}
|
||||
</p>
|
||||
<ScrollView class="max-h-80" viewportRef={props.controller.search.setList}>
|
||||
<ScrollView class="max-h-80" viewportRef={props.onSetSearchList}>
|
||||
<div class="flex flex-col gap-px pb-2">
|
||||
<For each={props.controller.search.results()}>
|
||||
<For each={props.searchResults()}>
|
||||
{(record) => (
|
||||
<HomeSessionSearchResultRow
|
||||
controller={props.controller}
|
||||
{...props}
|
||||
record={record}
|
||||
selected={props.controller.search.active() === homeSessionSearchKey(record)}
|
||||
selected={props.searchActive() === homeSessionSearchKey(record)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -227,57 +263,57 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
|
|||
<label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02">
|
||||
<IconV2 name="magnifying-glass" />
|
||||
<input
|
||||
ref={props.controller.search.setInput}
|
||||
ref={props.onSetSearchInput}
|
||||
class="relative z-20 min-w-0 flex-1 border-0 bg-transparent text-v2-text-text-base outline-0 [font-weight:440] placeholder:text-v2-text-text-faint"
|
||||
value={props.controller.search.value()}
|
||||
placeholder={props.controller.search.placeholder()}
|
||||
aria-label={props.controller.search.placeholder()}
|
||||
aria-expanded={props.controller.search.open()}
|
||||
value={props.searchValue()}
|
||||
placeholder={props.searchPlaceholder()}
|
||||
aria-label={props.searchPlaceholder()}
|
||||
aria-expanded={props.searchOpen()}
|
||||
aria-controls={HOME_SESSION_SEARCH_RESULTS_ID}
|
||||
aria-autocomplete="list"
|
||||
aria-activedescendant={
|
||||
props.controller.search.active() && props.controller.search.open()
|
||||
? `home-session-search-option-${props.controller.search.active()}`
|
||||
props.searchActive() && props.searchOpen()
|
||||
? `home-session-search-option-${props.searchActive()}`
|
||||
: undefined
|
||||
}
|
||||
onFocus={props.controller.search.focus}
|
||||
onInput={(event) => props.controller.search.input(event.currentTarget.value)}
|
||||
onFocus={props.onSearchFocus}
|
||||
onInput={(event) => props.onSearchInput(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
props.controller.search.close()
|
||||
props.onSearchClose()
|
||||
event.currentTarget.blur()
|
||||
return
|
||||
}
|
||||
if (!props.controller.search.open() || props.controller.search.results().length === 0) return
|
||||
if (!props.searchOpen() || props.searchResults().length === 0) return
|
||||
if (event.altKey || event.metaKey) return
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
props.controller.search.move(1)
|
||||
props.onSearchMove(1)
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
props.controller.search.move(-1)
|
||||
props.onSearchMove(-1)
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter" && !event.isComposing) {
|
||||
event.preventDefault()
|
||||
props.controller.search.selectActive()
|
||||
props.onSearchSelectActive()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Show when={props.controller.search.value()}>
|
||||
<Show when={props.searchValue()}>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
class="relative z-20 shrink-0"
|
||||
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||
aria-label={props.controller.search.placeholder()}
|
||||
aria-label={props.searchPlaceholder()}
|
||||
onClick={() => {
|
||||
props.controller.search.close()
|
||||
props.controller.search.focus()
|
||||
props.onSearchClose()
|
||||
props.onSearchFocus()
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
|
|
@ -287,13 +323,14 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
|
|||
)
|
||||
}
|
||||
|
||||
function HomeSessionSearchResultRow(props: {
|
||||
controller: HomeSessionsController
|
||||
record: HomeSessionRecord
|
||||
selected: boolean
|
||||
}) {
|
||||
function HomeSessionSearchResultRow(
|
||||
props: HomeSessionsViewProps & {
|
||||
record: HomeSessionRecord
|
||||
selected: boolean
|
||||
},
|
||||
) {
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
const showProjectName = () => props.controller.showProjectName() && props.record.projectName
|
||||
const showProjectName = () => props.showProjectName() && props.record.projectName
|
||||
const key = () => homeSessionSearchKey(props.record)
|
||||
|
||||
return (
|
||||
|
|
@ -309,19 +346,20 @@ function HomeSessionSearchResultRow(props: {
|
|||
"bg-v2-overlay-simple-overlay-hover": props.selected,
|
||||
group: !!showProjectName(),
|
||||
}}
|
||||
onMouseEnter={() => props.controller.search.highlight(props.record)}
|
||||
onMouseEnter={() => props.onSearchHighlight(props.record)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === 1) event.preventDefault()
|
||||
}}
|
||||
onClick={(event) => props.controller.search.select(props.record, { background: isBackgroundOpen(event) })}
|
||||
onClick={(event) => props.onSearchSelect(props.record, { background: isBackgroundOpen(event) })}
|
||||
onAuxClick={(event) => {
|
||||
if (!isBackgroundOpen(event)) return
|
||||
event.preventDefault()
|
||||
props.controller.search.select(props.record, { background: true })
|
||||
props.onSearchSelect(props.record, { background: true })
|
||||
}}
|
||||
>
|
||||
<HomeSessionLeadingController
|
||||
controller={props.controller}
|
||||
server={props.server}
|
||||
isOpenTab={props.isOpenTab}
|
||||
record={props.record}
|
||||
revealProjectOnHover={!!showProjectName()}
|
||||
/>
|
||||
|
|
@ -342,12 +380,12 @@ function HomeSessionSearchResultRow(props: {
|
|||
function HomeSessionGroupHeader(props: {
|
||||
title: string
|
||||
titleOpacity: number
|
||||
ref: ComponentProps<"div">["ref"]
|
||||
onSetRef: (element: HTMLDivElement) => void
|
||||
elevated?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
ref={props.ref}
|
||||
ref={props.onSetRef}
|
||||
class={`pointer-events-none sticky top-[84px] lg:top-[108px] flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
|
||||
>
|
||||
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
||||
|
|
@ -357,9 +395,9 @@ function HomeSessionGroupHeader(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function HomeSessionRow(props: { controller: HomeSessionsController; record: HomeSessionRecord }) {
|
||||
function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionRecord }) {
|
||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||
const showProjectName = () => props.controller.showProjectName() && props.record.projectName
|
||||
const showProjectName = () => props.showProjectName() && props.record.projectName
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -373,15 +411,16 @@ function HomeSessionRow(props: { controller: HomeSessionsController; record: Hom
|
|||
onMouseDown={(event) => {
|
||||
if (event.button === 1) event.preventDefault()
|
||||
}}
|
||||
onClick={(event) => props.controller.openSession(props.record.session, { background: isBackgroundOpen(event) })}
|
||||
onClick={(event) => props.onOpenSession(props.record.session, { background: isBackgroundOpen(event) })}
|
||||
onAuxClick={(event) => {
|
||||
if (!isBackgroundOpen(event)) return
|
||||
event.preventDefault()
|
||||
props.controller.openSession(props.record.session, { background: true })
|
||||
props.onOpenSession(props.record.session, { background: true })
|
||||
}}
|
||||
>
|
||||
<HomeSessionLeadingController
|
||||
controller={props.controller}
|
||||
server={props.server}
|
||||
isOpenTab={props.isOpenTab}
|
||||
record={props.record}
|
||||
revealProjectOnHover={!!showProjectName()}
|
||||
/>
|
||||
|
|
@ -398,21 +437,17 @@ function HomeSessionRow(props: { controller: HomeSessionsController; record: Hom
|
|||
</button>
|
||||
<Show when={SHOW_HOME_SESSION_ARCHIVE}>
|
||||
<div class="hover-reveal absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/session:opacity-100 focus-within:opacity-100">
|
||||
<TooltipV2
|
||||
class="flex shrink-0 items-center"
|
||||
placement="bottom"
|
||||
value={props.controller.language.t("common.archive")}
|
||||
>
|
||||
<TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
|
||||
<IconButtonV2
|
||||
data-action="home-session-archive"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
icon={<IconV2 name="archive" />}
|
||||
aria-label={props.controller.language.t("common.archive")}
|
||||
aria-label={props.language.t("common.archive")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void props.controller.archiveSession(props.record.session)
|
||||
void props.onArchiveSession(props.record.session)
|
||||
}}
|
||||
/>
|
||||
</TooltipV2>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue