From 6866a060bc48031ecc3b0f7c1c1e99f6961a4cb1 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:26:24 -0600 Subject: wip(desktop): progress --- packages/ui/src/styles/tailwind/index.css | 1 + packages/ui/src/styles/theme.css | 1 + 2 files changed, 2 insertions(+) (limited to 'packages/ui/src/styles') diff --git a/packages/ui/src/styles/tailwind/index.css b/packages/ui/src/styles/tailwind/index.css index bc6bb6f6d..d0a414fee 100644 --- a/packages/ui/src/styles/tailwind/index.css +++ b/packages/ui/src/styles/tailwind/index.css @@ -57,6 +57,7 @@ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; + --radius-xl: 0.625rem; --shadow-xs: var(--shadow-xs); --shadow-md: var(--shadow-md); diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 4450358f8..338e045ef 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -43,6 +43,7 @@ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; + --radius-xl: 0.625rem; --shadow-xs: 0 1px 2px -1px rgba(19, 16, 16, 0.04), 0 1px 2px 0 rgba(19, 16, 16, 0.06), 0 1px 3px 0 rgba(19, 16, 16, 0.08); -- cgit v1.2.3 From ada40decd14fc18901486382a10b1ec1d0d21f7e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:55:44 -0600 Subject: wip(desktop): progress --- packages/desktop/src/components/prompt-input.tsx | 20 +- packages/ui/index.html | 14 -- packages/ui/src/components/icon.tsx | 187 ++------------- packages/ui/src/components/select-dialog.css | 17 +- packages/ui/src/components/select-dialog.tsx | 2 + packages/ui/src/components/tag.css | 37 +++ packages/ui/src/components/tag.tsx | 22 ++ packages/ui/src/demo.tsx | 291 ----------------------- packages/ui/src/index.css | 40 ---- packages/ui/src/index.tsx | 22 -- packages/ui/src/styles/index.css | 1 + packages/ui/src/styles/theme.css | 1 + 12 files changed, 101 insertions(+), 553 deletions(-) delete mode 100644 packages/ui/index.html create mode 100644 packages/ui/src/components/tag.css create mode 100644 packages/ui/src/components/tag.tsx delete mode 100644 packages/ui/src/demo.tsx delete mode 100644 packages/ui/src/index.css delete mode 100644 packages/ui/src/index.tsx (limited to 'packages/ui/src/styles') diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 824d3da12..fbb643e58 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -3,7 +3,6 @@ import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Mat import { createStore } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" -import { DateTime } from "luxon" import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session" import { useSDK } from "@/context/sdk" import { useNavigate } from "@solidjs/router" @@ -14,10 +13,9 @@ import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Select } from "@opencode-ai/ui/select" +import { Tag } from "@opencode-ai/ui/tag" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { type IconName } from "@opencode-ai/ui/icons/provider" interface PromptInputProps { class?: string @@ -486,20 +484,10 @@ export const PromptInput: Component = (props) => { } > {(i) => ( -
-
- {/* */} -
- {i.name} - - - {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")} - - -
-
+
+ {i.name} -
Free
+ Free
)} diff --git a/packages/ui/index.html b/packages/ui/index.html deleted file mode 100644 index 7697a5f96..000000000 --- a/packages/ui/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - OpenCode UI - - - -
- - - diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 8c83b41ce..97f2e8eab 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -1,171 +1,44 @@ import { splitProps, type ComponentProps } from "solid-js" -// prettier-ignore const icons = { - close: '', - menu: ' ', - "chevron-right": '', - "chevron-left": '', - "chevron-down": '', - "chevron-up": '', - "chevron-down-square": '', - "chevron-up-square": '', - "chevron-right-square": '', - "chevron-left-square": '', - settings: '', - globe: '', - github: '', - hammer: '', - "avatar-square": '', - slash: '', - robot: '', - cloud: '', - "file-text": '', - file: '', - "file-checkmark": '', - "file-code": '', - "file-important": '', - "file-minus": '', - "file-plus": '', - files: '', - "file-zip": '', - jpg: '', - pdf: '', - png: '', - gif: '', - archive: '', - sun: '', - moon: '', - monitor: '', - command: '', - link: '', - share: '', - branch: '', - logout: '', - login: '', - keys: '', - key: '', - info: '', - warning: '', - checkmark: '', - "checkmark-square": '', - plus: '', - minus: '', - undo: '', - merge: '', - redo: '', - refresh: '', - rotate: '', - "arrow-left": '', - "arrow-down": '', - "arrow-right": '', - "arrow-up": '', - enter: '', - trash: '', - package: '', - box: '', - lock: '', - unlocked: '', - activity: '', - asterisk: '', - bell: '', - "bell-off": '', - bolt: '', - bookmark: '', - brain: '', - browser: '', - "browser-cursor": '', - bug: '', - "carat-down": '', - "carat-left": '', - "carat-right": '', - "carat-up": '', - cards: '', - chart: '', - "check-circle": '', - checklist: '', - "checklist-cards": '', - lab: '', - circle: '', - "circle-dotted": '', - clipboard: '', - clock: '', - "close-circle": '', - terminal: '', - code: '', - components: '', - copy: '', - cpu: '', - dashboard: '', - transfer: '', - devices: '', - diamond: '', - dice: '', - discord: '', - dots: '', - expand: '', - droplet: '', - "chevron-double-down": '', - "chevron-double-left": '', - "chevron-double-right": '', - "chevron-double-up": '', - "speech-bubble": '', - message: '', - annotation: '', - square: '', - "pull-request": '', - pencil: '', - sparkles: '', - photo: '', - columns: '', - "open-pane": '', - "close-pane": '', - "file-search": '', - "folder-search": '', - search: '', - "web-search": '', - loading: '', - mic: '', -} as const - -const newIcons = { - "circle-x": ``, - "magnifying-glass": ``, - "plus-small": ``, - "chevron-down": ``, - "chevron-right": ``, + "align-right": ``, "arrow-up": ``, + "bubble-5": ``, + "bullet-list": ``, "check-small": ``, + "chevron-down": ``, + "chevron-right": ``, + "chevron-grabber-vertical": ``, + "circle-x": ``, + close: ``, + checklist: ``, + console: ``, + expand: ``, + collapse: ``, + "code-lines": ``, + "circle-ban-sign": ``, "edit-small-2": ``, + enter: ``, folder: ``, + "magnifying-glass": ``, + "plus-small": ``, "pencil-line": ``, - "chevron-grabber-vertical": ``, mcp: ``, glasses: ``, - "bullet-list": ``, "magnifying-glass-menu": ``, "window-cursor": ``, task: ``, - checklist: ``, - console: ``, - "code-lines": ``, - "square-arrow-top-right": ``, - "circle-ban-sign": ``, stop: ``, - enter: ``, "layout-left": ``, "layout-left-partial": ``, "layout-left-full": ``, "layout-right": ``, "layout-right-partial": ``, "layout-right-full": ``, + "square-arrow-top-right": ``, "speech-bubble": ``, - "align-right": ``, - expand: ``, - collapse: ``, "folder-add-left": ``, "settings-gear": ``, - "bubble-5": ``, github: ``, discord: ``, "layout-bottom": ``, @@ -175,32 +48,12 @@ const newIcons = { } export interface IconProps extends ComponentProps<"svg"> { - name: keyof typeof icons | keyof typeof newIcons + name: keyof typeof icons size?: "small" | "normal" | "large" } export function Icon(props: IconProps) { const [local, others] = splitProps(props, ["name", "size", "class", "classList"]) - - if (local.name in newIcons) { - return ( -
- -
- ) - } - return (
+ {split.children} + + ) +} diff --git a/packages/ui/src/demo.tsx b/packages/ui/src/demo.tsx deleted file mode 100644 index 6081f0894..000000000 --- a/packages/ui/src/demo.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import type { Component } from "solid-js" -import { createSignal } from "solid-js" -import "./index.css" -import { Button } from "./components/button" -import { Select } from "./components/select" -import { Font } from "./components/font" -import { Accordion } from "./components/accordion" -import { Tabs } from "./components/tabs" -import { Tooltip } from "./components/tooltip" -import { Input } from "./components/input" -import { Checkbox } from "./components/checkbox" -import { Icon } from "./components/icon" -import { IconButton } from "./components/icon-button" -import { Dialog } from "./components/dialog" -import { SelectDialog } from "./components/select-dialog" -import { Collapsible } from "./components/collapsible" - -const Demo: Component = () => { - const [dialogOpen, setDialogOpen] = createSignal(false) - const [selectDialogOpen, setSelectDialogOpen] = createSignal(false) - const [inputValue, setInputValue] = createSignal("") - const [checked, setChecked] = createSignal(false) - const [termsAccepted, setTermsAccepted] = createSignal(false) - - const Content = (props: { dark?: boolean }) => ( -
-

Buttons

-
- - - - - - - - -
-

Select

-
- - setInputValue(e.currentTarget.value)} - /> - - -
-

Checkbox

-
- - - - - - - - -
-

Icons

-
- - - - - - - - -
-

Icon Buttons

-
- console.log("Close clicked")} /> - console.log("Check clicked")} /> - console.log("Search clicked")} disabled /> -
-

Dialog

-
- - - Example Dialog - This is an example dialog with a title and description. -
- - -
-
-
-

Select Dialog

-
- - x} - onSelect={(option) => { - console.log("Selected:", option) - setSelectDialogOpen(false) - }} - placeholder="Search options..." - > - {(item) =>
{item}
} -
-
-

Collapsible

-
- - - - - -
-

This is collapsible content that can be toggled open and closed.

-

It animates smoothly using CSS animations.

-
-
-
-
-

Accordion

-
- - - - What is Kobalte? - - -
-

Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS.

-
-
-
- - - Is it accessible? - - -
-

Yes. It adheres to the WAI-ARIA design patterns.

-
-
-
- - - Can it be animated? - - -
-

Yes! You can animate the content height using CSS animations.

-
-
-
-
-
-
- ) - - return ( - <> - -
- - -
- - ) -} - -export default Demo diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css deleted file mode 100644 index 27bcac4da..000000000 --- a/packages/ui/src/index.css +++ /dev/null @@ -1,40 +0,0 @@ -@import "./styles/index.css"; - -:root { - body { - margin: 0; - background-color: var(--background-base); - color: var(--text-base); - } - main { - display: flex; - flex-direction: row; - overflow-x: hidden; - } - main > div { - flex: 1; - padding: 2rem; - min-width: 0; - overflow-x: hidden; - display: flex; - flex-direction: column; - gap: 2rem; - } - h3 { - font-size: 1.25rem; - font-weight: 600; - margin: 0 0 1rem 0; - margin-bottom: -1rem; - } - section { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - align-items: flex-start; - } -} - -.dark { - background-color: var(--background-base); - color: var(--text-base); -} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx deleted file mode 100644 index fa76ba9af..000000000 --- a/packages/ui/src/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* @refresh reload */ -import { render } from "solid-js/web" -import { MetaProvider } from "@solidjs/meta" - -import Demo from "./demo" - -const root = document.getElementById("root") - -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error( - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", - ) -} - -render( - () => ( - - - - ), - root!, -) diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index ab45a3a25..074859f35 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -36,6 +36,7 @@ @import "../components/session-turn.css" layer(components); @import "../components/sticky-accordion-header.css" layer(components); @import "../components/tabs.css" layer(components); +@import "../components/tag.css" layer(components); @import "../components/tooltip.css" layer(components); @import "../components/typewriter.css" layer(components); diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 338e045ef..01ccc3fcc 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -40,6 +40,7 @@ --container-6xl: 72rem; --container-7xl: 80rem; + --radius-xs: 0.125rem; --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; -- cgit v1.2.3 From 58e66dd3d1dfd975195dac916fb4b23093404243 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:16:57 -0600 Subject: wip(desktop): progress --- packages/ui/src/components/list.css | 107 +++++++++++++++++++ packages/ui/src/components/list.tsx | 141 +++++++++++++++++++++++++ packages/ui/src/components/select-dialog.css | 118 ++------------------- packages/ui/src/components/select-dialog.tsx | 150 ++++++--------------------- packages/ui/src/styles/index.css | 1 + 5 files changed, 290 insertions(+), 227 deletions(-) create mode 100644 packages/ui/src/components/list.css create mode 100644 packages/ui/src/components/list.tsx (limited to 'packages/ui/src/styles') diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css new file mode 100644 index 000000000..63d9a2fe1 --- /dev/null +++ b/packages/ui/src/components/list.css @@ -0,0 +1,107 @@ +[data-component="list"] { + display: flex; + flex-direction: column; + gap: 20px; + + [data-slot="list-empty-state"] { + display: flex; + padding: 32px 0px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + align-self: stretch; + + [data-slot="list-message"] { + display: flex; + justify-content: center; + align-items: center; + gap: 2px; + color: var(--text-weak); + text-align: center; + + /* 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); + } + + [data-slot="list-filter"] { + color: var(--text-strong); + } + } + + [data-slot="list-group"] { + position: relative; + display: flex; + flex-direction: column; + + [data-slot="list-header"] { + display: flex; + height: 28px; + padding: 0 10px; + justify-content: space-between; + align-items: center; + align-self: stretch; + background: var(--surface-raised-stronger-non-alpha); + position: sticky; + top: 0; + + color: var(--text-base); + + /* text-14-medium */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + } + + [data-slot="list-items"] { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + + [data-slot="list-item"] { + display: flex; + width: 100%; + height: 28px; + padding: 4px 10px; + align-items: center; + color: var(--text-strong); + + /* text-14-medium */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + + [data-slot="list-item-selected-icon"] { + color: var(--icon-strong-base); + } + [data-slot="list-item-active-icon"] { + display: none; + color: var(--icon-strong-base); + } + + &[data-active="true"] { + border-radius: var(--radius-md); + background: var(--surface-raised-base-hover); + [data-slot="list-item-active-icon"] { + display: block; + } + } + &:active { + background: var(--surface-raised-base-active); + } + } + } + } +} diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx new file mode 100644 index 000000000..3fbeb35f6 --- /dev/null +++ b/packages/ui/src/components/list.tsx @@ -0,0 +1,141 @@ +import { createEffect, Show, For, type JSX, createSignal } from "solid-js" +import { createStore } from "solid-js/store" +import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" +import { Icon, IconProps } from "./icon" + +export interface ListProps extends FilteredListProps { + children: (item: T) => JSX.Element + emptyMessage?: string + onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void + activeIcon?: IconProps["name"] + filter?: string +} + +export interface ListRef { + onKeyDown: (e: KeyboardEvent) => void + setScrollRef: (el: HTMLDivElement | undefined) => void +} + +export function List(props: ListProps & { ref?: (ref: ListRef) => void }) { + const [scrollRef, setScrollRef] = createSignal(undefined) + const [store, setStore] = createStore({ + mouseActive: false, + }) + + const { filter, grouped, flat, reset, active, setActive, onKeyDown, onInput } = useFilteredList({ + items: props.items, + key: props.key, + filterKeys: props.filterKeys, + current: props.current, + groupBy: props.groupBy, + sortBy: props.sortBy, + sortGroupsBy: props.sortGroupsBy, + }) + + createEffect(() => { + if (props.filter === undefined) return + onInput(props.filter) + }) + + createEffect(() => { + filter() + scrollRef()?.scrollTo(0, 0) + reset() + }) + + createEffect(() => { + if (!scrollRef()) return + if (!props.current) return + const key = props.key(props.current) + requestAnimationFrame(() => { + const element = scrollRef()!.querySelector(`[data-key="${key}"]`) + element?.scrollIntoView({ block: "center" }) + }) + }) + + createEffect(() => { + const all = flat() + if (store.mouseActive || all.length === 0) return + if (active() === props.key(all[0])) { + scrollRef()?.scrollTo(0, 0) + return + } + const element = scrollRef()?.querySelector(`[data-key="${active()}"]`) + element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) + }) + + const handleSelect = (item: T | undefined) => { + props.onSelect?.(item) + } + + const handleKey = (e: KeyboardEvent) => { + setStore("mouseActive", false) + if (e.key === "Escape") return + + const all = flat() + const selected = all.find((x) => props.key(x) === active()) + props.onKeyEvent?.(e, selected) + + if (e.key === "Enter") { + e.preventDefault() + if (selected) handleSelect(selected) + } else { + onKeyDown(e) + } + } + + props.ref?.({ + onKeyDown: handleKey, + setScrollRef, + }) + + return ( +
+ 0} + fallback={ +
+
+ {props.emptyMessage ?? "No results"} for "{filter()}" +
+
+ } + > + + {(group) => ( +
+ +
{group.category}
+
+
+ + {(item) => ( + + )} + +
+
+ )} +
+
+
+ ) +} diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css index f5687ad8e..9759174a6 100644 --- a/packages/ui/src/components/select-dialog.css +++ b/packages/ui/src/components/select-dialog.css @@ -5,6 +5,14 @@ overflow: hidden; gap: 20px; padding: 0 10px; + + [data-slot="dialog-body"] { + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + } } [data-component="select-dialog-input"] { @@ -22,7 +30,7 @@ [data-slot="select-dialog-input-container"] { display: flex; align-items: center; - gap: 12px; + gap: 16px; flex: 1 0 0; /* [data-slot="select-dialog-icon"] {} */ @@ -34,111 +42,3 @@ /* [data-slot="select-dialog-clear-button"] {} */ } - -[data-component="select-dialog"] { - display: flex; - flex-direction: column; - gap: 20px; - - [data-slot="select-dialog-empty-state"] { - display: flex; - padding: 32px 0px; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 8px; - align-self: stretch; - - [data-slot="select-dialog-message"] { - display: flex; - justify-content: center; - align-items: center; - gap: 2px; - color: var(--text-weak); - text-align: center; - - /* 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); - } - - [data-slot="select-dialog-filter"] { - color: var(--text-strong); - } - } - - [data-slot="select-dialog-group"] { - position: relative; - display: flex; - flex-direction: column; - - [data-slot="select-dialog-header"] { - display: flex; - height: 28px; - padding: 0 10px; - justify-content: space-between; - align-items: center; - align-self: stretch; - background: var(--surface-raised-stronger-non-alpha); - position: sticky; - top: 0; - - color: var(--text-base); - - /* text-14-medium */ - font-family: var(--font-family-sans); - font-size: 14px; - font-style: normal; - font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); /* 142.857% */ - letter-spacing: var(--letter-spacing-normal); - } - - [data-slot="select-dialog-list"] { - display: flex; - flex-direction: column; - align-items: flex-start; - align-self: stretch; - - [data-slot="select-dialog-item"] { - display: flex; - width: 100%; - height: 28px; - padding: 4px 10px; - align-items: center; - color: var(--text-strong); - - /* text-14-medium */ - font-family: var(--font-family-sans); - font-size: 14px; - font-style: normal; - font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); /* 142.857% */ - letter-spacing: var(--letter-spacing-normal); - - [data-slot="select-dialog-item-selected-icon"] { - color: var(--icon-strong-base); - } - [data-slot="select-dialog-item-active-icon"] { - display: none; - color: var(--icon-strong-base); - } - - &[data-active="true"] { - border-radius: var(--radius-md); - background: var(--surface-raised-base-hover); - [data-slot="select-dialog-item-active-icon"] { - display: block; - } - } - &:active { - background: var(--surface-raised-base-active); - } - } - } - } -} diff --git a/packages/ui/src/components/select-dialog.tsx b/packages/ui/src/components/select-dialog.tsx index 86f723225..952ba881f 100644 --- a/packages/ui/src/components/select-dialog.tsx +++ b/packages/ui/src/components/select-dialog.tsx @@ -1,98 +1,46 @@ -import { createEffect, Show, For, type JSX, splitProps, createSignal } from "solid-js" -import { createStore } from "solid-js/store" -import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" +import { createEffect, Show, type JSX, splitProps, createSignal } from "solid-js" import { Dialog, DialogProps } from "./dialog" -import { Icon, IconProps } from "./icon" +import { Icon } from "./icon" import { Input } from "./input" import { IconButton } from "./icon-button" +import { List, ListRef, ListProps } from "./list" interface SelectDialogProps - extends FilteredListProps, + extends Omit, "filter">, Pick { title: string placeholder?: string - emptyMessage?: string - children: (item: T) => JSX.Element - onSelect?: (value: T | undefined) => void - onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void actions?: JSX.Element - activeIcon?: IconProps["name"] } export function SelectDialog(props: SelectDialogProps) { const [dialog, others] = splitProps(props, ["trigger", "onOpenChange", "defaultOpen"]) let closeButton!: HTMLButtonElement let inputRef: HTMLInputElement | undefined - let [scrollRef, setScrollRef] = createSignal(undefined) - const [store, setStore] = createStore({ - mouseActive: false, - }) - - const { filter, grouped, flat, reset, clear, active, setActive, onKeyDown, onInput } = useFilteredList({ - items: others.items, - key: others.key, - filterKeys: others.filterKeys, - current: others.current, - groupBy: others.groupBy, - sortBy: others.sortBy, - sortGroupsBy: others.sortGroupsBy, - }) - - createEffect(() => { - filter() - scrollRef()?.scrollTo(0, 0) - reset() - }) + const [filter, setFilter] = createSignal("") + let listRef: ListRef | undefined createEffect(() => { - if (!scrollRef()) return - if (!others.current) return - const key = others.key(others.current) + if (!props.current) return + const key = props.key(props.current) requestAnimationFrame(() => { - const element = scrollRef()!.querySelector(`[data-key="${key}"]`) + const element = document.querySelector(`[data-key="${key}"]`) element?.scrollIntoView({ block: "center" }) }) }) - createEffect(() => { - const all = flat() - if (store.mouseActive || all.length === 0) return - if (active() === others.key(all[0])) { - scrollRef()?.scrollTo(0, 0) - return - } - const element = scrollRef()?.querySelector(`[data-key="${active()}"]`) - element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) - }) - - const handleInput = (value: string) => { - onInput(value) - reset() - } - const handleSelect = (item: T | undefined) => { others.onSelect?.(item) closeButton.click() } const handleKey = (e: KeyboardEvent) => { - setStore("mouseActive", false) if (e.key === "Escape") return - - const all = flat() - const selected = all.find((x) => others.key(x) === active()) - props.onKeyEvent?.(e, selected) - - if (e.key === "Enter") { - e.preventDefault() - if (selected) handleSelect(selected) - } else { - onKeyDown(e) - } + listRef?.onKeyDown(e) } const handleOpenChange = (open: boolean) => { - if (!open) clear() + if (!open) setFilter("") props.onOpenChange?.(open) } @@ -113,7 +61,7 @@ export function SelectDialog(props: SelectDialogProps) { data-slot="select-dialog-input" type="text" value={filter()} - onChange={(value) => handleInput(value)} + onChange={setFilter} onKeyDown={handleKey} placeholder={others.placeholder} spellcheck={false} @@ -123,63 +71,29 @@ export function SelectDialog(props: SelectDialogProps) { />
- { - onInput("") - reset() - }} - /> + setFilter("")} />
- - 0} - fallback={ -
-
- {props.emptyMessage ?? "No results"} for{" "} - "{filter()}" -
-
- } + + { + listRef = ref + }} + items={others.items} + key={others.key} + filterKeys={others.filterKeys} + current={others.current} + groupBy={others.groupBy} + sortBy={others.sortBy} + sortGroupsBy={others.sortGroupsBy} + emptyMessage={others.emptyMessage} + activeIcon={others.activeIcon} + filter={filter()} + onSelect={handleSelect} + onKeyEvent={others.onKeyEvent} > - - {(group) => ( -
- -
{group.category}
-
-
- - {(item) => ( - - )} - -
-
- )} -
-
+ {others.children} +
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 074859f35..4c7f6e80b 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -22,6 +22,7 @@ @import "../components/icon.css" layer(components); @import "../components/icon-button.css" layer(components); @import "../components/input.css" layer(components); +@import "../components/list.css" layer(components); @import "../components/logo.css" layer(components); @import "../components/markdown.css" layer(components); @import "../components/message-part.css" layer(components); -- cgit v1.2.3