diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx index 8cc98ad4d..cde2f0187 100644 --- a/packages/console/app/src/app.tsx +++ b/packages/console/app/src/app.tsx @@ -3,6 +3,7 @@ import { Router } from "@solidjs/router" import { FileRoutes } from "@solidjs/start/router" import { Suspense } from "solid-js" import { Favicon } from "@opencode-ai/ui/favicon" +import { Font } from "@opencode-ai/ui/font" import "@ibm/plex/css/ibm-plex.css" import "./app.css" @@ -15,6 +16,7 @@ export default function App() { opencode + {props.children} )} diff --git a/packages/console/app/src/style/token/font.css b/packages/console/app/src/style/token/font.css index 5a5f19da6..844677b5f 100644 --- a/packages/console/app/src/style/token/font.css +++ b/packages/console/app/src/style/token/font.css @@ -1,11 +1,3 @@ -@font-face { - font-family: "Berkeley Mono"; - src: url("/fonts/berkley-mono.woff2") format("woff2"); - font-weight: 400; - font-style: normal; - font-display: swap; -} - body { --font-size-2xs: 0.6875rem; --font-size-xs: 0.75rem; diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 22f2c1642..41af8644b 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -579,54 +579,61 @@ export const PromptInput: Component = (props) => {
-
+
Add more models from popular providers
- x?.id} - items={providers().popular()} - activeIcon="plus-small" - sortBy={(a, b) => { - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - return a.name.localeCompare(b.name) - }} - onSelect={(x) => { - layout.dialog.close("model") - }} - > - {(i) => ( -
- - {i.name} - - Recommended - - -
- Connect with Claude Pro/Max or API key -
-
-
- )} -
- +
+ x?.id} + items={providers().popular()} + activeIcon="plus-small" + sortBy={(a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + onSelect={(x) => { + if (!x) return + layout.dialog.connect(x.id) + }} + > + {(i) => ( +
+ + {i.name} + + Recommended + + +
+ Connect with Claude Pro/Max or API key +
+
+
+ )} +
+ +
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx index 3a6062fb8..2a24a845c 100644 --- a/packages/desktop/src/context/global-sync.tsx +++ b/packages/desktop/src/context/global-sync.tsx @@ -1,22 +1,25 @@ -import type { - Message, - Agent, - Session, - Part, - Config, - Path, - File, - FileNode, - Project, - FileDiff, - Todo, - SessionStatus, - ProviderListResponse, -} from "@opencode-ai/sdk/v2" +import { + type Message, + type Agent, + type Session, + type Part, + type Config, + type Path, + type File, + type FileNode, + type Project, + type FileDiff, + type Todo, + type SessionStatus, + type ProviderListResponse, + type ProviderAuthResponse, + createOpencodeClient, +} from "@opencode-ai/sdk/v2/client" import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" +import { onMount } from "solid-js" type State = { ready: boolean @@ -49,19 +52,48 @@ type State = { export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({ name: "GlobalSync", init: () => { - const sdk = useGlobalSDK() + const globalSDK = useGlobalSDK() const [globalStore, setGlobalStore] = createStore<{ ready: boolean project: Project[] provider: ProviderListResponse + provider_auth: ProviderAuthResponse children: Record }>({ ready: false, project: [], provider: { all: [], connected: [], default: {} }, + provider_auth: {}, children: {}, }) + async function bootstrapInstance(directory: string) { + const [store, setStore] = child(directory) + const sdk = createOpencodeClient({ + baseUrl: globalSDK.url, + directory, + }) + const load = { + project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), + provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)), + path: () => sdk.path.get().then((x) => setStore("path", x.data!)), + agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), + session: () => + sdk.session.list().then((x) => { + const sessions = (x.data ?? []) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)) + .slice(0, store.limit) + setStore("session", sessions) + }), + status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), + config: () => sdk.config.get().then((x) => setStore("config", x.data!)), + changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), + node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), + } + await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) + } + const children: Record>> = {} function child(directory: string) { if (!children[directory]) { @@ -83,16 +115,21 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple changes: [], }) children[directory] = createStore(globalStore.children[directory]) + bootstrapInstance(directory) } return children[directory] } - sdk.event.listen((e) => { + globalSDK.event.listen((e) => { const directory = e.name const event = e.details if (directory === "global") { switch (event.type) { + case "global.disposed": { + bootstrap() + break + } case "project.updated": { const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) if (result.found) { @@ -113,6 +150,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple const [store, setStore] = child(directory) switch (event.type) { + case "server.instance.disposed": { + bootstrapInstance(directory) + break + } case "session.updated": { const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) if (result.found) { @@ -181,19 +222,28 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple } }) - Promise.all([ - sdk.client.project.list().then(async (x) => { - setGlobalStore( - "project", - x - .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) - .sort((a, b) => a.id.localeCompare(b.id)), - ) - }), - sdk.client.provider.list().then((x) => { - setGlobalStore("provider", x.data ?? {}) - }), - ]).then(() => setGlobalStore("ready", true)) + async function bootstrap() { + return Promise.all([ + globalSDK.client.project.list().then(async (x) => { + setGlobalStore( + "project", + x + .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) + .sort((a, b) => a.id.localeCompare(b.id)), + ) + }), + globalSDK.client.provider.list().then((x) => { + setGlobalStore("provider", x.data ?? {}) + }), + globalSDK.client.provider.auth().then((x) => { + setGlobalStore("provider_auth", x.data ?? {}) + }), + ]).then(() => setGlobalStore("ready", true)) + } + + onMount(() => { + bootstrap() + }) return { data: globalStore, @@ -201,6 +251,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple return globalStore.ready }, child, + bootstrap, } }, }) diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 5530ad28f..24ba55a53 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -1,5 +1,5 @@ -import { createStore } from "solid-js/store" -import { createMemo, onMount } from "solid-js" +import { createStore, produce } from "solid-js/store" +import { batch, createMemo, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { makePersisted } from "@solid-primitives/storage" import { useGlobalSync } from "./global-sync" @@ -19,6 +19,8 @@ const PASTEL_COLORS = [ "#C1E1C1", // pastel mint ] +type Dialog = "provider" | "model" | "connect" + export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { @@ -44,8 +46,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, ) const [ephemeral, setEphemeral] = createStore({ + connect: { + provider: undefined as undefined | string, + state: undefined as undefined | "pending" | "complete" | "error", + error: undefined as undefined | string, + }, dialog: { - open: undefined as undefined | "provider" | "model", + open: undefined as undefined | Dialog, }, }) const usedColors = new Set() @@ -169,14 +176,47 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, dialog: { opened: createMemo(() => ephemeral.dialog?.open), - open(dialog: "provider" | "model") { + open(dialog: Dialog) { setEphemeral("dialog", "open", dialog) + if (dialog !== "connect") { + setEphemeral("connect", {}) + } }, - close(dialog: "provider" | "model") { + close(dialog: Dialog) { if (ephemeral.dialog?.open === dialog) { setEphemeral("dialog", "open", undefined) + setEphemeral("connect", {}) } }, + connect(provider: string) { + batch(() => { + setEphemeral("dialog", "open", "connect") + setEphemeral("connect", { provider, state: "pending" }) + }) + }, + }, + connect: { + provider: createMemo(() => ephemeral.connect.provider), + state: createMemo(() => ephemeral.connect.state), + complete() { + setEphemeral( + produce((state) => { + state.dialog.open = "model" + state.connect.state = "complete" + }), + ) + }, + error(message: string) { + setEphemeral( + produce((state) => { + state.connect.state = "error" + state.connect.error = message + }), + ) + }, + clear() { + setEphemeral("connect", {}) + }, }, } }, diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 74d3ac364..d8dfa732a 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" import { base64Encode } from "@opencode-ai/util/encode" +import { useProviders } from "@/hooks/use-providers" export type LocalFile = FileNode & Partial<{ @@ -37,10 +38,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ init: () => { const sdk = useSDK() const sync = useSync() + const providers = useProviders() function isModelValid(model: ModelKey) { - const provider = sync.data.provider?.all.find((x) => x.id === model.providerID) - return !!provider?.models[model.modelID] && sync.data.provider?.connected.includes(model.providerID) + const provider = providers().all.find((x) => x.id === model.providerID) + return ( + !!provider?.models[model.modelID] && + providers() + .connected() + .map((p) => p.id) + .includes(model.providerID) + ) } function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) { @@ -115,8 +123,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const list = createMemo(() => - sync.data.provider.all - .filter((p) => sync.data.provider.connected.includes(p.id)) + providers() + .connected() .flatMap((p) => Object.values(p.models).map((m) => ({ ...m, @@ -145,11 +153,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - for (const p of sync.data.provider.connected) { - if (p in sync.data.provider.default) { + for (const p of providers().connected()) { + if (p.id in providers().default) { return { - providerID: p, - modelID: sync.data.provider.default[p], + providerID: p.id, + modelID: providers().default[p.id], } } } diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 1a11cd599..85758c5b6 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const globalSync = useGlobalSync() const sdk = useSDK() const [store, setStore] = globalSync.child(sdk.directory) - - const load = { - project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)), - provider: () => sdk.client.provider.list().then((x) => setStore("provider", x.data!)), - path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)), - agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])), - session: () => - sdk.client.session.list().then((x) => { - const sessions = (x.data ?? []) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - .slice(0, store.limit) - setStore("session", sessions) - }), - status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)), - config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)), - changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)), - node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), - } - - Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) - const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") return { @@ -78,11 +56,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }, fetch: async (count = 10) => { setStore("limit", (x) => x + count) - await load.session() + await sdk.client.session.list().then((x) => { + const sessions = (x.data ?? []) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)) + .slice(0, store.limit) + setStore("session", sessions) + }) }, more: createMemo(() => store.session.length >= store.limit), }, - load, absolute, get directory() { return store.path.directory diff --git a/packages/desktop/src/hooks/use-providers.ts b/packages/desktop/src/hooks/use-providers.ts index c3fcc7898..04ef855d4 100644 --- a/packages/desktop/src/hooks/use-providers.ts +++ b/packages/desktop/src/hooks/use-providers.ts @@ -16,16 +16,14 @@ export function useProviders() { } return globalSync.data.provider }) - const connected = createMemo(() => - providers().all.filter( - (p) => providers().connected.includes(p.id) && Object.values(p.models).find((m) => m.cost?.input), - ), - ) + const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id))) + const paid = createMemo(() => connected().filter((p) => Object.values(p.models).find((m) => m.cost?.input))) const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id))) return createMemo(() => ({ all: providers().all, default: providers().default, popular, connected, + paid, })) } diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 10d4cbfda..39917c420 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -17,7 +17,7 @@ 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, Project } from "@opencode-ai/sdk/v2/client" +import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { createStore } from "solid-js/store" import { @@ -34,6 +34,11 @@ import { SelectDialog } from "@opencode-ai/ui/select-dialog" import { Tag } from "@opencode-ai/ui/tag" import { IconName } from "@opencode-ai/ui/icons/provider" import { popularProviders, useProviders } from "@/hooks/use-providers" +import { Dialog } from "@opencode-ai/ui/dialog" +import { iife } from "@opencode-ai/util/iife" +import { List, ListRef } from "@opencode-ai/ui/list" +import { Input } from "@opencode-ai/ui/input" +import { useGlobalSDK } from "@/context/global-sdk" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -42,6 +47,7 @@ export default function Layout(props: ParentProps) { }) const params = useParams() + const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const layout = useLayout() const platform = usePlatform() @@ -434,7 +440,7 @@ export default function Layout(props: ParentProps) { {" "} + to collect your API key. +
+ + + +
+ Enter your {provider.name} API key to connect your account and use {provider.name}{" "} + models in OpenCode. +
+
+ +
+ + +
+ + ) + })} + + + + + ) + })} + ) diff --git a/packages/console/app/public/fonts/berkley-mono.woff2 b/packages/ui/src/assets/fonts/tx-02.woff2 similarity index 100% rename from packages/console/app/public/fonts/berkley-mono.woff2 rename to packages/ui/src/assets/fonts/tx-02.woff2 diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css index f95317028..3a32672fe 100644 --- a/packages/ui/src/components/button.css +++ b/packages/ui/src/components/button.css @@ -11,27 +11,29 @@ outline: none; &[data-variant="primary"] { - border-color: var(--border-base); - background-color: var(--surface-brand-base); - color: var(--text-on-brand-strong); + background-color: var(--icon-strong-base); + border-color: var(--border-weak-base); + color: var(--icon-invert-base); + + [data-slot="icon-svg"] { + color: var(--icon-invert-base); + } &:hover:not(:disabled) { - border-color: var(--border-hover); - background-color: var(--surface-brand-hover); + background-color: var(--icon-strong-hover); } &:focus:not(:disabled) { - border-color: var(--border-focus); - background-color: var(--surface-brand-focus); + background-color: var(--icon-strong-focus); } &:active:not(:disabled) { - border-color: var(--border-active); - background-color: var(--surface-brand-active); + background-color: var(--icon-strong-active); } &:disabled { - border-color: var(--border-disabled); - background-color: var(--surface-disabled); - color: var(--text-weak); - cursor: not-allowed; + background-color: var(--icon-strong-disabled); + + [data-slot="icon-svg"] { + color: var(--icon-invert-base); + } } } @@ -120,13 +122,13 @@ &[data-size="large"] { height: 32px; - padding: 0 8px; + padding: 6px 12px; &[data-icon] { padding: 0 12px 0 8px; } - gap: 8px; + gap: 4px; /* text-14-medium */ font-family: var(--font-family-sans); diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index a16705a57..56053278d 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -5,7 +5,7 @@ import { DialogCloseButtonProps, DialogDescriptionProps, } from "@kobalte/core/dialog" -import { ComponentProps, type JSX, onCleanup, Show, splitProps } from "solid-js" +import { ComponentProps, type JSX, onCleanup, onMount, Show, splitProps } from "solid-js" import { IconButton } from "./icon-button" export interface DialogProps extends DialogRootProps { @@ -35,6 +35,11 @@ export function DialogRoot(props: DialogProps) { }) } + onMount(() => { + // @ts-ignore + document?.activeElement?.blur?.() + }) + return ( diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 97f2e8eab..080a6274d 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -3,6 +3,7 @@ import { splitProps, type ComponentProps } from "solid-js" const icons = { "align-right": ``, "arrow-up": ``, + "arrow-left": ``, "bubble-5": ``, "bullet-list": ``, "check-small": ``, diff --git a/packages/ui/src/components/input.css b/packages/ui/src/components/input.css index c5f8cb8c5..276e8069b 100644 --- a/packages/ui/src/components/input.css +++ b/packages/ui/src/components/input.css @@ -1,6 +1,5 @@ [data-component="input"] { width: 100%; - /* [data-slot="input-label"] {} */ [data-slot="input-input"] { width: 100%; @@ -22,4 +21,79 @@ color: var(--text-weak); } } + + &[data-variant="normal"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + + [data-slot="input-label"] { + color: var(--text-weak); + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 18px; /* 150% */ + letter-spacing: var(--letter-spacing-normal); + } + + [data-slot="input-input"] { + color: var(--text-strong); + + display: flex; + height: 32px; + padding: 2px 12px; + align-items: center; + gap: 8px; + align-self: stretch; + + border-radius: var(--radius-md); + border: 1px solid var(--border-weak-base); + background: var(--input-base); + + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + + &:focus { + outline: none; + + /* border/shadow-xs/select */ + box-shadow: + 0 0 0 3px var(--border-weak-selected), + 0 0 0 1px var(--border-selected), + 0 1px 2px -1px rgba(19, 16, 16, 0.25), + 0 1px 2px 0 rgba(19, 16, 16, 0.08), + 0 1px 3px 0 rgba(19, 16, 16, 0.12); + } + + &[data-invalid] { + background: var(--surface-critical-weak); + border: 1px solid var(--border-critical-selected); + } + + &::placeholder { + color: var(--text-weak); + } + } + + [data-slot="input-error"] { + color: var(--text-on-critical-base); + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 18px; /* 150% */ + letter-spacing: var(--letter-spacing-normal); + } + } } diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index 82f704e8c..8e2a115c6 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -4,31 +4,61 @@ import type { ComponentProps } from "solid-js" export interface InputProps extends ComponentProps, - Partial, "value" | "onChange" | "onKeyDown">> { + Partial< + Pick< + ComponentProps, + | "name" + | "defaultValue" + | "value" + | "onChange" + | "onKeyDown" + | "validationState" + | "required" + | "disabled" + | "readOnly" + > + > { label?: string hideLabel?: boolean hidden?: boolean description?: string + error?: string + variant?: "normal" | "ghost" } export function Input(props: InputProps) { const [local, others] = splitProps(props, [ + "name", + "defaultValue", + "value", + "onChange", + "onKeyDown", + "validationState", + "required", + "disabled", + "readOnly", "class", "label", "hidden", "hideLabel", "description", - "value", - "onChange", - "onKeyDown", + "error", + "variant", ]) return ( @@ -39,7 +69,7 @@ export function Input(props: InputProps) { {local.description} - + {local.error} ) } diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index a7f2db9ef..013767e60 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -65,8 +65,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) }) - const handleSelect = (item: T | undefined) => { - props.onSelect?.(item) + const handleSelect = (item: T | undefined, index: number) => { + props.onSelect?.(item, index) } const handleKey = (e: KeyboardEvent) => { @@ -75,11 +75,12 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) const all = flat() const selected = all.find((x) => props.key(x) === active()) + const index = selected ? all.indexOf(selected) : -1 props.onKeyEvent?.(e, selected) if (e.key === "Enter") { e.preventDefault() - if (selected) handleSelect(selected) + if (selected) handleSelect(selected, index) } else { onKeyDown(e) } @@ -110,13 +111,13 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void })
- {(item) => ( + {(item, i) => (