summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-09 03:45:50 -0600
committerAdam <[email protected]>2025-12-09 06:12:09 -0600
commit0a357be160d672791bb99a36fc7d7c1299d5493d (patch)
tree5c5c3ee1360a0adf40b1e47691fb2ef9ef2408aa
parentd29205e67787d49de9dd4c271ec8e4e7848d81ad (diff)
downloadopencode-0a357be160d672791bb99a36fc7d7c1299d5493d.tar.gz
opencode-0a357be160d672791bb99a36fc7d7c1299d5493d.zip
wip(desktop): progress
-rw-r--r--packages/desktop/src/DesktopInterface.tsx16
-rw-r--r--packages/desktop/src/context/global-sync.tsx7
-rw-r--r--packages/desktop/src/context/layout.tsx20
-rw-r--r--packages/desktop/src/pages/home.tsx66
-rw-r--r--packages/desktop/src/pages/layout.tsx337
-rw-r--r--packages/tauri/package.json3
-rw-r--r--packages/ui/src/components/avatar.css35
-rw-r--r--packages/ui/src/components/avatar.tsx28
-rw-r--r--packages/ui/src/components/dropdown-menu.css119
-rw-r--r--packages/ui/src/components/dropdown-menu.tsx308
-rw-r--r--packages/ui/src/components/icon.tsx2
-rw-r--r--packages/ui/src/components/select.tsx12
-rw-r--r--packages/ui/src/styles/index.css2
13 files changed, 784 insertions, 171 deletions
diff --git a/packages/desktop/src/DesktopInterface.tsx b/packages/desktop/src/DesktopInterface.tsx
index 1979308e4..31d52863d 100644
--- a/packages/desktop/src/DesktopInterface.tsx
+++ b/packages/desktop/src/DesktopInterface.tsx
@@ -2,19 +2,18 @@ import "@/index.css"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
-import { Favicon } from "@opencode-ai/ui/favicon"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { Diff } from "@opencode-ai/ui/diff"
-import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
+import { GlobalSyncProvider } from "./context/global-sync"
import Layout from "@/pages/layout"
+import Home from "@/pages/home"
import DirectoryLayout from "@/pages/directory-layout"
import Session from "@/pages/session"
import { LayoutProvider } from "./context/layout"
import { GlobalSDKProvider } from "./context/global-sdk"
import { SessionProvider } from "./context/session"
-import { base64Encode } from "@opencode-ai/util/encode"
-import { createMemo, Show } from "solid-js"
+import { Show } from "solid-js"
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
@@ -35,14 +34,7 @@ export function DesktopInterface() {
<MetaProvider>
<Font />
<Router root={Layout}>
- <Route
- path="/"
- component={() => {
- const globalSync = useGlobalSync()
- const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
- return <Navigate href={`${slug()}/session`} />
- }}
- />
+ <Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 9f795cded..188db1e69 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -51,7 +51,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
init: () => {
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
- defaultProject?: Project // TODO: remove this when we can select projects
projects: Project[]
children: Record<string, State>
}>({
@@ -165,11 +164,11 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
sdk.client.project.list().then((x) =>
setGlobalStore(
"projects",
- x.data!.filter((x) => !x.worktree.includes("opencode-test")),
+ x
+ .data!.filter((x) => !x.worktree.includes("opencode-test") && x.vcs)
+ .sort((a, b) => b.time.created - a.time.created),
),
),
- // TODO: remove this when we can select projects
- sdk.client.project.current().then((x) => setGlobalStore("defaultProject", x.data)),
]).then(() => setGlobalStore("ready", true))
return {
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index ca736e84e..1b43cf511 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -2,17 +2,15 @@ import { createStore } from "solid-js/store"
import { createMemo } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
-import { useGlobalSync } from "./global-sync"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
- const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
- projects: [] as { directory: string; expanded: boolean }[],
+ projects: [] as { directory: string; expanded: boolean; lastSession?: string }[],
sidebar: {
- opened: true,
+ opened: false,
width: 280,
},
terminal: {
@@ -24,17 +22,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
- name: "____default-layout",
+ name: "default-layout.v4",
},
)
return {
projects: {
- list: createMemo(() =>
- globalSync.data.defaultProject
- ? [{ directory: globalSync.data.defaultProject!.worktree, expanded: true }, ...store.projects]
- : store.projects,
- ),
+ list: createMemo(() => store.projects),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
setStore("projects", (x) => [...x, { directory, expanded: true }])
@@ -48,6 +42,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
collapse(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
},
+ lastSession(directory: string) {
+ return store.projects.find((x) => x.directory === directory)?.lastSession
+ },
+ setLastSession(directory: string, session: string | undefined) {
+ setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, lastSession: session } : x)))
+ },
},
sidebar: {
opened: createMemo(() => store.sidebar.opened),
diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx
index c35d5754e..469e2b7f7 100644
--- a/packages/desktop/src/pages/home.tsx
+++ b/packages/desktop/src/pages/home.tsx
@@ -1,21 +1,63 @@
import { useGlobalSync } from "@/context/global-sync"
-import { base64Encode } from "@opencode-ai/util/encode"
-import { For } from "solid-js"
-import { A } from "@solidjs/router"
+import { For, Match, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
-import { getFilename } from "@opencode-ai/util/path"
+import { Logo } from "@opencode-ai/ui/logo"
+import { useLayout } from "@/context/layout"
+import { useNavigate } from "@solidjs/router"
+import { base64Encode } from "@opencode-ai/util/encode"
+import { Icon } from "@opencode-ai/ui/icon"
export default function Home() {
+ const navigate = useNavigate()
const sync = useGlobalSync()
+ const layout = useLayout()
+
+ function openProject(directory: string) {
+ layout.projects.open(directory)
+ navigate(`/${base64Encode(directory)}`)
+ }
+
return (
- <div class="flex flex-col gap-3">
- <For each={sync.data.projects}>
- {(project) => (
- <Button as={A} href={base64Encode(project.worktree)}>
- {getFilename(project.worktree)}
- </Button>
- )}
- </For>
+ <div class="mx-auto mt-55">
+ <Logo class="w-xl opacity-12" />
+ <Switch>
+ <Match when={sync.data.projects.length > 0}>
+ <div class="mt-20 w-full flex flex-col gap-4">
+ <div class="flex gap-2 items-center justify-between pl-3">
+ <div class="text-14-medium text-text-strong">Recent projects</div>
+ <Button icon="folder-add-left" size="normal" class="pl-2 pr-3">
+ Open project
+ </Button>
+ </div>
+ <ol class="flex flex-col gap-2">
+ <For each={sync.data.projects.slice(0, 5)}>
+ {(project) => (
+ <Button
+ size="large"
+ variant="ghost"
+ class="text-14-mono text-left justify-between px-3"
+ onClick={() => openProject(project.worktree)}
+ >
+ {project.worktree}
+ <div class="text-14-regular text-text-weak">10m ago</div>
+ </Button>
+ )}
+ </For>
+ </ol>
+ </div>
+ </Match>
+ <Match when={true}>
+ <div class="mt-30 mx-auto flex flex-col items-center gap-3">
+ <Icon name="folder-add-left" size="large" />
+ <div class="flex flex-col gap-1 items-center justify-center">
+ <div class="text-14-medium text-text-strong">No recent projects</div>
+ <div class="text-12-regular text-text-weak">Get started by opening a local project</div>
+ </div>
+ <div />
+ <Button class="px-3">Open project</Button>
+ </div>
+ </Match>
+ </Switch>
</div>
)
}
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 166ee7beb..03eeedfb1 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -1,10 +1,11 @@
-import { createMemo, For, ParentProps, Show } from "solid-js"
+import { createEffect, createMemo, For, Match, ParentProps, Show, Switch } from "solid-js"
import { DateTime } from "luxon"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Mark } from "@opencode-ai/ui/logo"
+import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
@@ -14,6 +15,7 @@ import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
export default function Layout(props: ParentProps) {
@@ -25,15 +27,30 @@ export default function Layout(props: ParentProps) {
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+ function navigateToProject(directory: string | undefined) {
+ if (!directory) return
+ navigate(`/${base64Encode(directory)}`)
+ }
+
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
}
+ function closeProject(directory: string) {
+ layout.projects.close(directory)
+ navigate("/")
+ }
+
const handleOpenProject = async () => {
// layout.projects.open(dir.)
}
+ // createEffect(() => {
+ // if (!params.dir) return
+ // layout.projects.setLastSession(base64Decode(params.dir), params.id)
+ // })
+
return (
<div class="relative h-screen flex flex-col">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
@@ -50,61 +67,72 @@ export default function Layout(props: ParentProps) {
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
- <div class="flex items-center gap-3">
- <div class="flex items-center gap-2">
- <Select
- options={layout.projects.list().map((project) => getFilename(project.directory))}
- current={getFilename(currentDirectory())}
- class="text-14-regular text-text-base"
- variant="ghost"
- />
- <div class="text-text-weaker">/</div>
- <Select
- options={sessions()}
- current={currentSession()}
- placeholder="Select session"
- label={(x) => x.title}
- value={(x) => x.id}
- onSelect={navigateToSession}
- class="text-14-regular text-text-base max-w-md"
- variant="ghost"
- />
- </div>
- <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
- New session
- </Button>
- </div>
- <div class="flex items-center gap-4">
- <Tooltip
- class="shrink-0"
- value={
- <div class="flex items-center gap-2">
- <span>Toggle terminal</span>
- <span class="text-icon-base text-12-medium">Ctrl `</span>
- </div>
- }
- >
- <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
- <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
- <Icon
- size="small"
- name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
- class="group-hover/terminal-toggle:hidden"
- />
- <Icon
- size="small"
- name="layout-bottom-partial"
- class="hidden group-hover/terminal-toggle:inline-block"
- />
- <Icon
- size="small"
- name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
- class="hidden group-active/terminal-toggle:inline-block"
- />
- </div>
+ <Show when={params.dir && layout.projects.list().length > 0}>
+ <div class="flex items-center gap-3">
+ <div class="flex items-center gap-2">
+ <Select
+ options={layout.projects.list().map((project) => getFilename(project.directory))}
+ current={getFilename(currentDirectory())}
+ onSelect={(x) => (x ? navigateToProject(x) : undefined)}
+ class="text-14-regular text-text-base"
+ variant="ghost"
+ >
+ {/* @ts-ignore */}
+ {(i) => (
+ <div class="flex items-center gap-2">
+ <Icon name="folder" size="small" />
+ <div class="text-text-strong">{i}</div>
+ </div>
+ )}
+ </Select>
+ <div class="text-text-weaker">/</div>
+ <Select
+ options={sessions()}
+ current={currentSession()}
+ placeholder="Select session"
+ label={(x) => x.title}
+ value={(x) => x.id}
+ onSelect={navigateToSession}
+ class="text-14-regular text-text-base max-w-md"
+ variant="ghost"
+ />
+ </div>
+ <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
+ New session
</Button>
- </Tooltip>
- </div>
+ </div>
+ <div class="flex items-center gap-4">
+ <Tooltip
+ class="shrink-0"
+ value={
+ <div class="flex items-center gap-2">
+ <span>Toggle terminal</span>
+ <span class="text-icon-base text-12-medium">Ctrl `</span>
+ </div>
+ }
+ >
+ <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
+ <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+ <Icon
+ size="small"
+ name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+ class="group-hover/terminal-toggle:hidden"
+ />
+ <Icon
+ size="small"
+ name="layout-bottom-partial"
+ class="hidden group-hover/terminal-toggle:inline-block"
+ />
+ <Icon
+ size="small"
+ name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+ class="hidden group-active/terminal-toggle:inline-block"
+ />
+ </div>
+ </Button>
+ </Tooltip>
+ </div>
+ </Show>
</div>
</header>
<div class="h-[calc(100vh-3rem)] flex">
@@ -159,84 +187,135 @@ export default function Layout(props: ParentProps) {
</Show>
</Button>
</Tooltip>
- <div class="flex flex-col justify-center items-start gap-4 self-stretch min-h-0">
- <div class="hidden @[4rem]:flex size-full flex-col grow overflow-y-auto no-scrollbar">
- <For each={layout.projects.list()}>
- {(project) => {
- const [store] = globalSync.child(project.directory)
- const slug = createMemo(() => base64Encode(project.directory))
- return (
- <Collapsible variant="ghost" defaultOpen class="gap-2">
- <Button
- as={"div"}
- variant="ghost"
- class="flex items-center justify-between gap-3 w-full h-8 pl-2 pr-2.25 self-stretch"
- >
- <Collapsible.Trigger class="p-0 text-left text-14-medium text-text-strong grow min-w-0 truncate">
- {getFilename(project.directory)}
- </Collapsible.Trigger>
- <IconButton as={A} href={`${slug()}/session`} icon="plus-small" size="normal" />
- </Button>
- <Collapsible.Content>
- <nav class="w-full flex flex-col gap-1.5">
- <For each={store.session}>
- {(session) => {
- const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
- return (
- <A
- data-active={session.id === params.id}
- href={`${slug()}/session/${session.id}`}
- class="group/session focus:outline-none cursor-default"
- >
- <Tooltip placement="right" value={session.title}>
- <div
- class="w-full px-2 py-1 rounded-md
+ <div class="size-full min-w-8 flex flex-col gap-2 grow min-h-0 overflow-y-auto no-scrollbar">
+ <For each={layout.projects.list()}>
+ {(project) => {
+ const [store] = globalSync.child(project.directory)
+ const slug = createMemo(() => base64Encode(project.directory))
+ const name = createMemo(() => getFilename(project.directory))
+ return (
+ <Switch>
+ <Match when={layout.sidebar.opened()}>
+ <Collapsible variant="ghost" defaultOpen class="gap-2">
+ <Button
+ as={"div"}
+ variant="ghost"
+ class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none"
+ >
+ <Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
+ <div class="size-6 shrink-0">
+ <Avatar
+ fallback={name()}
+ background="var(--surface-info-base)"
+ class="size-full group-hover/session:hidden"
+ />
+ <Icon
+ name="chevron-right"
+ size="large"
+ class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
+ />
+ </div>
+ <span class="truncate text-14-medium text-text-strong">{name()}</span>
+ </Collapsible.Trigger>
+ <div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
+ <DropdownMenu>
+ <DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
+ <DropdownMenu.Portal>
+ <DropdownMenu.Content>
+ <DropdownMenu.Item onSelect={() => closeProject(project.directory)}>
+ <DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
+ </DropdownMenu.Item>
+ {/* <DropdownMenu.Separator /> */}
+ {/* <DropdownMenu.Item> */}
+ {/* <DropdownMenu.ItemLabel>Action 2</DropdownMenu.ItemLabel> */}
+ {/* </DropdownMenu.Item> */}
+ </DropdownMenu.Content>
+ </DropdownMenu.Portal>
+ </DropdownMenu>
+ <Tooltip placement="bottom" value="New session">
+ <IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
+ </Tooltip>
+ </div>
+ </Button>
+ <Collapsible.Content>
+ <nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
+ <For each={store.session}>
+ {(session) => {
+ const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
+ return (
+ <A
+ data-active={session.id === params.id}
+ href={`${slug()}/session/${session.id}`}
+ class="group/session focus:outline-none cursor-default"
+ >
+ <Tooltip placement="right" value={session.title}>
+ <div
+ class="w-full pl-4 pr-2 py-1 rounded-md
group-data-[active=true]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
- >
- <div class="flex items-center self-stretch gap-6 justify-between">
- <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
- {session.title}
- </span>
- <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
- {Math.abs(updated().diffNow().as("seconds")) < 60
- ? "Now"
- : updated()
- .toRelative({ style: "short", unit: ["days", "hours", "minutes"] })
- ?.replace(" ago", "")
- ?.replace(/ days?/, "d")
- ?.replace(" min.", "m")
- ?.replace(" hr.", "h")}
- </span>
- </div>
- <div class="hidden _flex justify-between items-center self-stretch">
- <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
- <Show when={session.summary}>
- {(summary) => <DiffChanges changes={summary()} />}
- </Show>
+ >
+ <div class="flex items-center self-stretch gap-6 justify-between">
+ <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
+ {session.title}
+ </span>
+ <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
+ {Math.abs(updated().diffNow().as("seconds")) < 60
+ ? "Now"
+ : updated()
+ .toRelative({
+ style: "short",
+ unit: ["days", "hours", "minutes"],
+ })
+ ?.replace(" ago", "")
+ ?.replace(/ days?/, "d")
+ ?.replace(" min.", "m")
+ ?.replace(" hr.", "h")}
+ </span>
+ </div>
+ <div class="hidden _flex justify-between items-center self-stretch">
+ <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
+ <Show when={session.summary}>
+ {(summary) => <DiffChanges changes={summary()} />}
+ </Show>
+ </div>
</div>
- </div>
- </Tooltip>
- </A>
- )
- }}
- </For>
- </nav>
- {/* <Show when={sync.session.more()}> */}
- {/* <button */}
- {/* class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong" */}
- {/* onClick={() => sync.session.fetch()} */}
- {/* > */}
- {/* Show more */}
- {/* </button> */}
- {/* </Show> */}
- </Collapsible.Content>
- </Collapsible>
- )
- }}
- </For>
- </div>
+ </Tooltip>
+ </A>
+ )
+ }}
+ </For>
+ </nav>
+ {/* <Show when={sync.session.more()}> */}
+ {/* <button */}
+ {/* class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong" */}
+ {/* onClick={() => sync.session.fetch()} */}
+ {/* > */}
+ {/* Show more */}
+ {/* </button> */}
+ {/* </Show> */}
+ </Collapsible.Content>
+ </Collapsible>
+ </Match>
+ <Match when={true}>
+ <Tooltip placement="right" value={project.directory}>
+ <Button
+ as={A}
+ href={`${slug()}/session`}
+ variant="ghost"
+ size="large"
+ class="flex items-center justify-center p-0 aspect-square border-none"
+ >
+ <div class="size-6 shrink-0 inset-0">
+ <Avatar fallback={name()} background="var(--surface-info-base)" class="size-full" />
+ </div>
+ </Button>
+ </Tooltip>
+ </Match>
+ </Switch>
+ )
+ }}
+ </For>
</div>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
diff --git a/packages/tauri/package.json b/packages/tauri/package.json
index 70b9e7cc8..30dec36ff 100644
--- a/packages/tauri/package.json
+++ b/packages/tauri/package.json
@@ -9,7 +9,8 @@
"dev": "vite",
"build": "bun run typecheck && vite build",
"preview": "vite preview",
- "tauri": "tauri"
+ "tauri": "tauri",
+ "typecheck": "tsgo --noEmit"
},
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
diff --git a/packages/ui/src/components/avatar.css b/packages/ui/src/components/avatar.css
new file mode 100644
index 000000000..bc87f3bd8
--- /dev/null
+++ b/packages/ui/src/components/avatar.css
@@ -0,0 +1,35 @@
+[data-component="avatar"] {
+ --avatar-bg: var(--color-surface-info-base);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--color-border-weak-base);
+ font-family: var(--font-mono);
+ font-weight: 500;
+ text-transform: uppercase;
+ background-color: var(--avatar-bg);
+ color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h);
+}
+
+[data-component="avatar"][data-size="small"] {
+ width: 1.25rem;
+ height: 1.25rem;
+ font-size: 0.75rem;
+ line-height: 1;
+}
+
+[data-component="avatar"][data-size="normal"] {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 1.125rem;
+ line-height: 1.5rem;
+}
+
+[data-component="avatar"][data-size="large"] {
+ width: 2rem;
+ height: 2rem;
+ font-size: 1.25rem;
+ line-height: 2rem;
+}
diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx
new file mode 100644
index 000000000..183a15b9b
--- /dev/null
+++ b/packages/ui/src/components/avatar.tsx
@@ -0,0 +1,28 @@
+import { type ComponentProps, splitProps, Show } from "solid-js"
+
+export interface AvatarProps extends ComponentProps<"div"> {
+ fallback: string
+ background?: string
+ size?: "small" | "normal" | "large"
+}
+
+export function Avatar(props: AvatarProps) {
+ const [split, rest] = splitProps(props, ["fallback", "background", "size", "class", "classList", "style"])
+ return (
+ <div
+ {...rest}
+ data-component="avatar"
+ data-size={split.size || "normal"}
+ classList={{
+ ...(split.classList ?? {}),
+ [split.class ?? ""]: !!split.class,
+ }}
+ style={{
+ ...(typeof split.style === "object" ? split.style : {}),
+ ...(split.background ? { "--avatar-bg": split.background } : {}),
+ }}
+ >
+ <Show when={split.fallback}>{split.fallback[0]}</Show>
+ </div>
+ )
+}
diff --git a/packages/ui/src/components/dropdown-menu.css b/packages/ui/src/components/dropdown-menu.css
new file mode 100644
index 000000000..710ff2ddc
--- /dev/null
+++ b/packages/ui/src/components/dropdown-menu.css
@@ -0,0 +1,119 @@
+[data-component="dropdown-menu-content"],
+[data-component="dropdown-menu-sub-content"] {
+ min-width: 8rem;
+ overflow: hidden;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-weak-base);
+ background-color: var(--surface-raised-stronger-non-alpha);
+ padding: 4px;
+ box-shadow: var(--shadow-md);
+ z-index: 50;
+ transform-origin: var(--kb-menu-content-transform-origin);
+
+ &[data-closed] {
+ animation: dropdown-menu-close 0.15s ease-out;
+ }
+
+ &[data-expanded] {
+ animation: dropdown-menu-open 0.15s ease-out;
+ }
+}
+
+[data-component="dropdown-menu-content"],
+[data-component="dropdown-menu-sub-content"] {
+ [data-slot="dropdown-menu-item"],
+ [data-slot="dropdown-menu-checkbox-item"],
+ [data-slot="dropdown-menu-radio-item"],
+ [data-slot="dropdown-menu-sub-trigger"] {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 8px;
+ border-radius: var(--radius-sm);
+ cursor: default;
+ user-select: none;
+ outline: none;
+
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large);
+ letter-spacing: var(--letter-spacing-normal);
+ color: var(--text-strong);
+
+ &[data-highlighted] {
+ background: var(--surface-raised-base-hover);
+ }
+
+ &[data-disabled] {
+ color: var(--text-weak);
+ pointer-events: none;
+ }
+ }
+
+ [data-slot="dropdown-menu-sub-trigger"] {
+ &[data-expanded] {
+ background: var(--surface-raised-base-hover);
+ }
+ }
+
+ [data-slot="dropdown-menu-item-indicator"] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ }
+
+ [data-slot="dropdown-menu-item-label"] {
+ flex: 1;
+ }
+
+ [data-slot="dropdown-menu-item-description"] {
+ font-size: var(--font-size-x-small);
+ color: var(--text-weak);
+ }
+
+ [data-slot="dropdown-menu-separator"] {
+ height: 1px;
+ margin: 4px -4px;
+ border-top-color: var(--border-weak-base);
+ }
+
+ [data-slot="dropdown-menu-group-label"] {
+ padding: 4px 8px;
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-x-small);
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large);
+ letter-spacing: var(--letter-spacing-normal);
+ color: var(--text-weak);
+ }
+
+ [data-slot="dropdown-menu-arrow"] {
+ fill: var(--surface-raised-stronger-non-alpha);
+ }
+}
+
+@keyframes dropdown-menu-open {
+ from {
+ opacity: 0;
+ transform: scale(0.96);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes dropdown-menu-close {
+ from {
+ opacity: 1;
+ transform: scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.96);
+ }
+}
diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx
new file mode 100644
index 000000000..efb2b45ca
--- /dev/null
+++ b/packages/ui/src/components/dropdown-menu.tsx
@@ -0,0 +1,308 @@
+import { DropdownMenu as Kobalte } from "@kobalte/core/dropdown-menu"
+import { splitProps } from "solid-js"
+import type { ComponentProps, ParentProps } from "solid-js"
+
+export interface DropdownMenuProps extends ComponentProps<typeof Kobalte> {}
+export interface DropdownMenuTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
+export interface DropdownMenuIconProps extends ComponentProps<typeof Kobalte.Icon> {}
+export interface DropdownMenuPortalProps extends ComponentProps<typeof Kobalte.Portal> {}
+export interface DropdownMenuContentProps extends ComponentProps<typeof Kobalte.Content> {}
+export interface DropdownMenuArrowProps extends ComponentProps<typeof Kobalte.Arrow> {}
+export interface DropdownMenuSeparatorProps extends ComponentProps<typeof Kobalte.Separator> {}
+export interface DropdownMenuGroupProps extends ComponentProps<typeof Kobalte.Group> {}
+export interface DropdownMenuGroupLabelProps extends ComponentProps<typeof Kobalte.GroupLabel> {}
+export interface DropdownMenuItemProps extends ComponentProps<typeof Kobalte.Item> {}
+export interface DropdownMenuItemLabelProps extends ComponentProps<typeof Kobalte.ItemLabel> {}
+export interface DropdownMenuItemDescriptionProps extends ComponentProps<typeof Kobalte.ItemDescription> {}
+export interface DropdownMenuItemIndicatorProps extends ComponentProps<typeof Kobalte.ItemIndicator> {}
+export interface DropdownMenuRadioGroupProps extends ComponentProps<typeof Kobalte.RadioGroup> {}
+export interface DropdownMenuRadioItemProps extends ComponentProps<typeof Kobalte.RadioItem> {}
+export interface DropdownMenuCheckboxItemProps extends ComponentProps<typeof Kobalte.CheckboxItem> {}
+export interface DropdownMenuSubProps extends ComponentProps<typeof Kobalte.Sub> {}
+export interface DropdownMenuSubTriggerProps extends ComponentProps<typeof Kobalte.SubTrigger> {}
+export interface DropdownMenuSubContentProps extends ComponentProps<typeof Kobalte.SubContent> {}
+
+function DropdownMenuRoot(props: DropdownMenuProps) {
+ return <Kobalte {...props} data-component="dropdown-menu" />
+}
+
+function DropdownMenuTrigger(props: ParentProps<DropdownMenuTriggerProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.Trigger
+ {...rest}
+ data-slot="dropdown-menu-trigger"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.Trigger>
+ )
+}
+
+function DropdownMenuIcon(props: ParentProps<DropdownMenuIconProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.Icon
+ {...rest}
+ data-slot="dropdown-menu-icon"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.Icon>
+ )
+}
+
+function DropdownMenuPortal(props: DropdownMenuPortalProps) {
+ return <Kobalte.Portal {...props} />
+}
+
+function DropdownMenuContent(props: ParentProps<DropdownMenuContentProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.Content
+ {...rest}
+ data-component="dropdown-menu-content"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.Content>
+ )
+}
+
+function DropdownMenuArrow(props: DropdownMenuArrowProps) {
+ const [local, rest] = splitProps(props, ["class", "classList"])
+ return (
+ <Kobalte.Arrow
+ {...rest}
+ data-slot="dropdown-menu-arrow"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ />
+ )
+}
+
+function DropdownMenuSeparator(props: DropdownMenuSeparatorProps) {
+ const [local, rest] = splitProps(props, ["class", "classList"])
+ return (
+ <Kobalte.Separator
+ {...rest}
+ data-slot="dropdown-menu-separator"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ />
+ )
+}
+
+function DropdownMenuGroup(props: ParentProps<DropdownMenuGroupProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.Group
+ {...rest}
+ data-slot="dropdown-menu-group"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.Group>
+ )
+}
+
+function DropdownMenuGroupLabel(props: ParentProps<DropdownMenuGroupLabelProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.GroupLabel
+ {...rest}
+ data-slot="dropdown-menu-group-label"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.GroupLabel>
+ )
+}
+
+function DropdownMenuItem(props: ParentProps<DropdownMenuItemProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.Item
+ {...rest}
+ data-slot="dropdown-menu-item"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.Item>
+ )
+}
+
+function DropdownMenuItemLabel(props: ParentProps<DropdownMenuItemLabelProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.ItemLabel
+ {...rest}
+ data-slot="dropdown-menu-item-label"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.ItemLabel>
+ )
+}
+
+function DropdownMenuItemDescription(props: ParentProps<DropdownMenuItemDescriptionProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.ItemDescription
+ {...rest}
+ data-slot="dropdown-menu-item-description"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.ItemDescription>
+ )
+}
+
+function DropdownMenuItemIndicator(props: ParentProps<DropdownMenuItemIndicatorProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.ItemIndicator
+ {...rest}
+ data-slot="dropdown-menu-item-indicator"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.ItemIndicator>
+ )
+}
+
+function DropdownMenuRadioGroup(props: ParentProps<DropdownMenuRadioGroupProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.RadioGroup
+ {...rest}
+ data-slot="dropdown-menu-radio-group"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.RadioGroup>
+ )
+}
+
+function DropdownMenuRadioItem(props: ParentProps<DropdownMenuRadioItemProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.RadioItem
+ {...rest}
+ data-slot="dropdown-menu-radio-item"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.RadioItem>
+ )
+}
+
+function DropdownMenuCheckboxItem(props: ParentProps<DropdownMenuCheckboxItemProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.CheckboxItem
+ {...rest}
+ data-slot="dropdown-menu-checkbox-item"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.CheckboxItem>
+ )
+}
+
+function DropdownMenuSub(props: DropdownMenuSubProps) {
+ return <Kobalte.Sub {...props} />
+}
+
+function DropdownMenuSubTrigger(props: ParentProps<DropdownMenuSubTriggerProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.SubTrigger
+ {...rest}
+ data-slot="dropdown-menu-sub-trigger"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.SubTrigger>
+ )
+}
+
+function DropdownMenuSubContent(props: ParentProps<DropdownMenuSubContentProps>) {
+ const [local, rest] = splitProps(props, ["class", "classList", "children"])
+ return (
+ <Kobalte.SubContent
+ {...rest}
+ data-component="dropdown-menu-sub-content"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {local.children}
+ </Kobalte.SubContent>
+ )
+}
+
+export const DropdownMenu = Object.assign(DropdownMenuRoot, {
+ Trigger: DropdownMenuTrigger,
+ Icon: DropdownMenuIcon,
+ Portal: DropdownMenuPortal,
+ Content: DropdownMenuContent,
+ Arrow: DropdownMenuArrow,
+ Separator: DropdownMenuSeparator,
+ Group: DropdownMenuGroup,
+ GroupLabel: DropdownMenuGroupLabel,
+ Item: DropdownMenuItem,
+ ItemLabel: DropdownMenuItemLabel,
+ ItemDescription: DropdownMenuItemDescription,
+ ItemIndicator: DropdownMenuItemIndicator,
+ RadioGroup: DropdownMenuRadioGroup,
+ RadioItem: DropdownMenuRadioItem,
+ CheckboxItem: DropdownMenuCheckboxItem,
+ Sub: DropdownMenuSub,
+ SubTrigger: DropdownMenuSubTrigger,
+ SubContent: DropdownMenuSubContent,
+})
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx
index 9e4f00a0d..8c83b41ce 100644
--- a/packages/ui/src/components/icon.tsx
+++ b/packages/ui/src/components/icon.tsx
@@ -133,6 +133,7 @@ const newIcons = {
"magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
+ "chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
"arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
"edit-small-2": `<path d="M17.0834 17.0833V17.5833H17.5834V17.0833H17.0834ZM2.91675 17.0833H2.41675V17.5833H2.91675V17.0833ZM2.91675 2.91659V2.41659H2.41675V2.91659H2.91675ZM9.58341 3.41659H10.0834V2.41659H9.58341V2.91659V3.41659ZM17.5834 10.4166V9.91659H16.5834V10.4166H17.0834H17.5834ZM10.4167 7.08325L10.0632 6.7297L9.91675 6.87615V7.08325H10.4167ZM10.4167 9.58325H9.91675V10.0833H10.4167V9.58325ZM12.9167 9.58325V10.0833H13.1239L13.2703 9.93681L12.9167 9.58325ZM15.4167 2.08325L15.7703 1.7297L15.4167 1.37615L15.0632 1.7297L15.4167 2.08325ZM17.9167 4.58325L18.2703 4.93681L18.6239 4.58325L18.2703 4.2297L17.9167 4.58325ZM17.0834 17.0833V16.5833H2.91675V17.0833V17.5833H17.0834V17.0833ZM2.91675 17.0833H3.41675V2.91659H2.91675H2.41675V17.0833H2.91675ZM2.91675 2.91659V3.41659H9.58341V2.91659V2.41659H2.91675V2.91659ZM17.0834 10.4166H16.5834V17.0833H17.0834H17.5834V10.4166H17.0834ZM10.4167 7.08325H9.91675V9.58325H10.4167H10.9167V7.08325H10.4167ZM10.4167 9.58325V10.0833H12.9167V9.58325V9.08325H10.4167V9.58325ZM10.4167 7.08325L10.7703 7.43681L15.7703 2.43681L15.4167 2.08325L15.0632 1.7297L10.0632 6.7297L10.4167 7.08325ZM15.4167 2.08325L15.0632 2.43681L17.5632 4.93681L17.9167 4.58325L18.2703 4.2297L15.7703 1.7297L15.4167 2.08325ZM17.9167 4.58325L17.5632 4.2297L12.5632 9.2297L12.9167 9.58325L13.2703 9.93681L18.2703 4.93681L17.9167 4.58325Z" fill="currentColor"/>`,
@@ -170,6 +171,7 @@ const newIcons = {
"layout-bottom": `<path d="M18.125 18.125L1.875 18.125L1.875 1.875L18.125 1.875L18.125 18.125ZM3.125 12.8308L3.125 16.875L16.875 16.875L16.875 12.8308L3.125 12.8308ZM3.125 3.125L3.125 11.5808L16.875 11.5808L16.875 3.125L3.125 3.125Z" fill="currentColor"/>`,
"layout-bottom-partial": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor" fill-opacity="40%" /><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
"layout-bottom-full": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor"/><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
+ "dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
index 9ba1f177b..bde5325e9 100644
--- a/packages/ui/src/components/select.tsx
+++ b/packages/ui/src/components/select.tsx
@@ -1,10 +1,10 @@
import { Select as Kobalte } from "@kobalte/core/select"
-import { createMemo, splitProps, type ComponentProps } from "solid-js"
+import { createMemo, splitProps, type ComponentProps, type JSX } from "solid-js"
import { pipe, groupBy, entries, map } from "remeda"
import { Button, ButtonProps } from "./button"
import { Icon } from "./icon"
-export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect"> & {
+export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
placeholder?: string
options: T[]
current?: T
@@ -14,6 +14,7 @@ export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "
onSelect?: (value: T | undefined) => void
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
+ children?: (item: T | undefined) => JSX.Element
}
export function Select<T>(props: SelectProps<T> & ButtonProps) {
@@ -27,6 +28,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
"label",
"groupBy",
"onSelect",
+ "children",
])
const grouped = createMemo(() => {
const result = pipe(
@@ -63,7 +65,11 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
{...itemProps}
>
<Kobalte.ItemLabel data-slot="select-select-item-label">
- {local.label ? local.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
+ {local.children
+ ? local.children(itemProps.item.rawValue)
+ : local.label
+ ? local.label(itemProps.item.rawValue)
+ : (itemProps.item.rawValue as string)}
</Kobalte.ItemLabel>
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
<Icon name="check-small" size="small" />
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index afe005f84..656bb928a 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -6,6 +6,7 @@
@import "./base.css" layer(base);
@import "../components/accordion.css" layer(components);
+@import "../components/avatar.css" layer(components);
@import "../components/basic-tool.css" layer(components);
@import "../components/button.css" layer(components);
@import "../components/card.css" layer(components);
@@ -14,6 +15,7 @@
@import "../components/collapsible.css" layer(components);
@import "../components/diff.css" layer(components);
@import "../components/diff-changes.css" layer(components);
+@import "../components/dropdown-menu.css" layer(components);
@import "../components/dialog.css" layer(components);
@import "../components/file-icon.css" layer(components);
@import "../components/icon.css" layer(components);