import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { batch, createContext, createEffect, Show, useContext, type JSX, type ParentProps, } from "solid-js" import { useTheme } from "@tui/context/theme" import { Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { createEventBus } from "@solid-primitives/event-bus" export function Dialog( props: ParentProps<{ size?: "medium" | "large" onClose: () => void }>, ) { const dimensions = useTerminalDimensions() const { theme } = useTheme() return ( { props.onClose?.() }} width={dimensions().width} height={dimensions().height} alignItems="center" position="absolute" paddingTop={dimensions().height / 4} left={0} top={0} backgroundColor={RGBA.fromInts(0, 0, 0, 150)} > { e.stopPropagation() }} width={props.size === "large" ? 80 : 60} maxWidth={dimensions().width - 2} backgroundColor={theme.backgroundPanel} paddingTop={1} > {props.children} ) } function init() { const [store, setStore] = createStore({ stack: [] as { element: JSX.Element onClose?: () => void }[], size: "medium" as "medium" | "large", }) const allClosedEvent = createEventBus() useKeyboard((evt) => { if (evt.name === "escape" && store.stack.length > 0) { const current = store.stack.at(-1)! current.onClose?.() setStore("stack", store.stack.slice(0, -1)) evt.preventDefault() refocus() } }) const renderer = useRenderer() let focus: Renderable | null function refocus() { setTimeout(() => { if (!focus) return if (focus.isDestroyed) return function find(item: Renderable) { for (const child of item.getChildren()) { if (child === focus) return true if (find(child)) return true } return false } const found = find(renderer.root) if (!found) return focus.focus() }, 1) } createEffect(() => { if (store.stack.length === 0) { allClosedEvent.emit() } }) return { clear() { for (const item of store.stack) { if (item.onClose) item.onClose() } batch(() => { setStore("size", "medium") setStore("stack", []) }) refocus() }, replace(input: any, onClose?: () => void) { if (store.stack.length === 0) focus = renderer.currentFocusedRenderable for (const item of store.stack) { if (item.onClose) item.onClose() } setStore("size", "medium") setStore("stack", [ { element: input, onClose, }, ]) }, get stack() { return store.stack }, get size() { return store.size }, setSize(size: "medium" | "large") { setStore("size", size) }, get allClosedEvent() { return allClosedEvent }, } } export type DialogContext = ReturnType const ctx = createContext() export function DialogProvider(props: ParentProps) { const value = init() return ( {props.children} value.clear()} size={value.size}> {value.stack.at(-1)!.element} ) } export function useDialog() { const value = useContext(ctx) if (!value) { throw new Error("useDialog must be used within a DialogProvider") } return value }