summaryrefslogtreecommitdiffhomepage
path: root/packages/ui
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-14 20:32:14 -0600
committerAdam <[email protected]>2025-12-14 21:38:59 -0600
commitdda579c8ad30f81ade458769971d85ff7afee64c (patch)
tree343c82906735fcfc82a5b2a178660aaab5bccb45 /packages/ui
parent4246cdb069502c96ab11e260eb36a07a0370b710 (diff)
downloadopencode-dda579c8ad30f81ade458769971d85ff7afee64c.tar.gz
opencode-dda579c8ad30f81ade458769971d85ff7afee64c.zip
wip(desktop): progress
Diffstat (limited to 'packages/ui')
-rw-r--r--packages/ui/src/components/dialog.css2
-rw-r--r--packages/ui/src/components/dialog.tsx123
-rw-r--r--packages/ui/src/context/dialog.tsx79
-rw-r--r--packages/ui/src/context/index.ts1
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"