diff options
| author | Adam <[email protected]> | 2025-12-14 20:32:14 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-14 21:38:59 -0600 |
| commit | dda579c8ad30f81ade458769971d85ff7afee64c (patch) | |
| tree | 343c82906735fcfc82a5b2a178660aaab5bccb45 /packages/ui | |
| parent | 4246cdb069502c96ab11e260eb36a07a0370b710 (diff) | |
| download | opencode-dda579c8ad30f81ade458769971d85ff7afee64c.tar.gz opencode-dda579c8ad30f81ade458769971d85ff7afee64c.zip | |
wip(desktop): progress
Diffstat (limited to 'packages/ui')
| -rw-r--r-- | packages/ui/src/components/dialog.css | 2 | ||||
| -rw-r--r-- | packages/ui/src/components/dialog.tsx | 123 | ||||
| -rw-r--r-- | packages/ui/src/context/dialog.tsx | 79 | ||||
| -rw-r--r-- | packages/ui/src/context/index.ts | 1 |
4 files changed, 118 insertions, 87 deletions
diff --git a/packages/ui/src/components/dialog.css b/packages/ui/src/components/dialog.css index fa5e1171e..6fa71c64c 100644 --- a/packages/ui/src/components/dialog.css +++ b/packages/ui/src/components/dialog.css @@ -60,6 +60,7 @@ [data-slot="dialog-header"] { display: flex; padding: 16px; + padding-left: 20px; justify-content: space-between; align-items: center; flex-shrink: 0; @@ -82,6 +83,7 @@ [data-slot="dialog-description"] { display: flex; padding: 16px; + padding-left: 20px; padding-top: 0; margin-top: -8px; justify-content: space-between; diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index aebb77885..47d6af42e 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -1,96 +1,45 @@ -import { - Dialog as Kobalte, - DialogRootProps, - DialogTitleProps, - DialogCloseButtonProps, - DialogDescriptionProps, -} from "@kobalte/core/dialog" -import { ComponentProps, type JSX, onCleanup, onMount, Show, splitProps } from "solid-js" +import { Dialog as Kobalte } from "@kobalte/core/dialog" +import { ComponentProps, JSXElement, Match, ParentProps, Show, Switch } from "solid-js" import { IconButton } from "./icon-button" -export interface DialogProps extends DialogRootProps { - trigger?: JSX.Element +export interface DialogProps extends ParentProps { + title?: JSXElement + description?: JSXElement + action?: JSXElement class?: ComponentProps<"div">["class"] classList?: ComponentProps<"div">["classList"] } -function DialogRoot(props: DialogProps) { - let trigger!: HTMLElement - const [local, others] = splitProps(props, ["trigger", "class", "classList", "children"]) - - const resetTabIndex = () => { - trigger.tabIndex = 0 - } - - const handleTriggerFocus = (e: FocusEvent & { currentTarget: HTMLElement | null }) => { - const firstChild = e.currentTarget?.firstElementChild as HTMLElement - if (!firstChild) return - - firstChild.focus() - trigger.tabIndex = -1 - - firstChild.addEventListener("focusout", resetTabIndex) - onCleanup(() => { - firstChild.removeEventListener("focusout", resetTabIndex) - }) - } - - onMount(() => { - // @ts-ignore - document?.activeElement?.blur?.() - }) - +export function Dialog(props: DialogProps) { return ( - <Kobalte {...others}> - <Show when={props.trigger}> - <Kobalte.Trigger ref={trigger} data-component="dialog-trigger" onFocusIn={handleTriggerFocus}> - {props.trigger} - </Kobalte.Trigger> - </Show> - <Kobalte.Portal> - <Kobalte.Overlay data-component="dialog-overlay" /> - <div data-component="dialog"> - <div data-slot="dialog-container"> - <Kobalte.Content - data-slot="dialog-content" - classList={{ - ...(local.classList ?? {}), - [local.class ?? ""]: !!local.class, - }} - > - {local.children} - </Kobalte.Content> - </div> - </div> - </Kobalte.Portal> - </Kobalte> + <div data-component="dialog"> + <div data-slot="dialog-container"> + <Kobalte.Content + data-slot="dialog-content" + classList={{ + ...(props.classList ?? {}), + [props.class ?? ""]: !!props.class, + }} + > + <Show when={props.title || props.action}> + <div data-slot="dialog-header"> + <Show when={props.title}> + <Kobalte.Title data-slot="dialog-title">{props.title}</Kobalte.Title> + </Show> + <Switch> + <Match when={props.action}>{props.action}</Match> + <Match when={true}> + <Kobalte.CloseButton data-slot="dialog-close-button" as={IconButton} icon="close" variant="ghost" /> + </Match> + </Switch> + </div> + </Show> + <Show when={props.description}> + <Kobalte.Description data-slot="dialog-description">{props.description}</Kobalte.Description> + </Show> + <div data-slot="dialog-body">{props.children}</div> + </Kobalte.Content> + </div> + </div> ) } - -function DialogHeader(props: ComponentProps<"div">) { - return <div data-slot="dialog-header" {...props} /> -} - -function DialogBody(props: ComponentProps<"div">) { - return <div data-slot="dialog-body" {...props} /> -} - -function DialogTitle(props: DialogTitleProps & ComponentProps<"h2">) { - return <Kobalte.Title data-slot="dialog-title" {...props} /> -} - -function DialogDescription(props: DialogDescriptionProps & ComponentProps<"p">) { - return <Kobalte.Description data-slot="dialog-description" {...props} /> -} - -function DialogCloseButton(props: DialogCloseButtonProps & ComponentProps<"button">) { - return <Kobalte.CloseButton data-slot="dialog-close-button" as={IconButton} icon="close" variant="ghost" {...props} /> -} - -export const Dialog = Object.assign(DialogRoot, { - Header: DialogHeader, - Title: DialogTitle, - Description: DialogDescription, - CloseButton: DialogCloseButton, - Body: DialogBody, -}) diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx new file mode 100644 index 000000000..af5da06f9 --- /dev/null +++ b/packages/ui/src/context/dialog.tsx @@ -0,0 +1,79 @@ +import { For, Show, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { createSimpleContext } from "@opencode-ai/ui/context" + +type DialogElement = JSX.Element | (() => JSX.Element) + +export const { use: useDialog, provider: DialogProvider } = createSimpleContext({ + name: "Dialog", + init: () => { + const [store, setStore] = createStore({ + stack: [] as { + element: DialogElement + onClose?: () => void + }[], + }) + + return { + get stack() { + return store.stack + }, + push(element: DialogElement, onClose?: () => void) { + setStore("stack", (s) => [...s, { element, onClose }]) + }, + pop() { + const current = store.stack.at(-1) + current?.onClose?.() + setStore("stack", store.stack.slice(0, -1)) + }, + replace(element: DialogElement, onClose?: () => void) { + for (const item of store.stack) { + item.onClose?.() + } + setStore("stack", [{ element, onClose }]) + }, + clear() { + for (const item of store.stack) { + item.onClose?.() + } + setStore("stack", []) + }, + } + }, +}) + +import { Dialog as Kobalte } from "@kobalte/core/dialog" + +export function DialogRoot(props: { children?: JSX.Element }) { + const dialog = useDialog() + return ( + <> + {props.children} + <Show when={dialog.stack.length > 0}> + <div data-component="dialog-stack"> + <For each={dialog.stack}> + {(item, index) => ( + <Show when={index() === dialog.stack.length - 1}> + <Kobalte + modal + defaultOpen + onOpenChange={(open) => { + if (!open) { + item.onClose?.() + dialog.pop() + } + }} + > + <Kobalte.Portal> + <Kobalte.Overlay data-component="dialog-overlay" /> + {typeof item.element === "function" ? item.element() : item.element} + </Kobalte.Portal> + </Kobalte> + </Show> + )} + </For> + </div> + </Show> + </> + ) +} diff --git a/packages/ui/src/context/index.ts b/packages/ui/src/context/index.ts index 3e0f5de74..499cb74d4 100644 --- a/packages/ui/src/context/index.ts +++ b/packages/ui/src/context/index.ts @@ -1,3 +1,4 @@ export * from "./helper" export * from "./data" export * from "./diff" +export * from "./dialog" |
