mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-23 10:44:21 +01:00
165 lines
3.7 KiB
TypeScript
165 lines
3.7 KiB
TypeScript
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 (
|
|
<box
|
|
onMouseUp={async () => {
|
|
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)}
|
|
>
|
|
<box
|
|
onMouseUp={async (e) => {
|
|
e.stopPropagation()
|
|
}}
|
|
width={props.size === "large" ? 80 : 60}
|
|
maxWidth={dimensions().width - 2}
|
|
backgroundColor={theme.backgroundPanel}
|
|
paddingTop={1}
|
|
>
|
|
{props.children}
|
|
</box>
|
|
</box>
|
|
)
|
|
}
|
|
|
|
function init() {
|
|
const [store, setStore] = createStore({
|
|
stack: [] as {
|
|
element: JSX.Element
|
|
onClose?: () => void
|
|
}[],
|
|
size: "medium" as "medium" | "large",
|
|
})
|
|
const allClosedEvent = createEventBus<void>()
|
|
|
|
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<typeof init>
|
|
|
|
const ctx = createContext<DialogContext>()
|
|
|
|
export function DialogProvider(props: ParentProps) {
|
|
const value = init()
|
|
return (
|
|
<ctx.Provider value={value}>
|
|
{props.children}
|
|
<box position="absolute">
|
|
<Show when={value.stack.length}>
|
|
<Dialog onClose={() => value.clear()} size={value.size}>
|
|
{value.stack.at(-1)!.element}
|
|
</Dialog>
|
|
</Show>
|
|
</box>
|
|
</ctx.Provider>
|
|
)
|
|
}
|
|
|
|
export function useDialog() {
|
|
const value = useContext(ctx)
|
|
if (!value) {
|
|
throw new Error("useDialog must be used within a DialogProvider")
|
|
}
|
|
return value
|
|
}
|