mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
wip(desktop): progress
This commit is contained in:
parent
d29205e677
commit
0a357be160
13 changed files with 784 additions and 171 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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 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>
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
35
packages/ui/src/components/avatar.css
Normal file
35
packages/ui/src/components/avatar.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
28
packages/ui/src/components/avatar.tsx
Normal file
28
packages/ui/src/components/avatar.tsx
Normal file
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
119
packages/ui/src/components/dropdown-menu.css
Normal file
119
packages/ui/src/components/dropdown-menu.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
308
packages/ui/src/components/dropdown-menu.tsx
Normal file
308
packages/ui/src/components/dropdown-menu.tsx
Normal file
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -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"> {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue