refactor(server): canonicalize service API (#31049)
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { batch, createContext, createEffect, onCleanup, Show, useContext, type JSX, type ParentProps } from "solid-js"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { MouseButton, Renderable, RGBA } from "@opentui/core"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useToast } from "./toast"
|
||||
import { useTuiEnvironment } from "../runtime"
|
||||
import { useBindings, useOpencodeModeStack } from "../keymap"
|
||||
import { useOptionalTuiPlatform } from "../platform"
|
||||
|
||||
export function Dialog(
|
||||
props: ParentProps<{
|
||||
size?: "medium" | "large" | "xlarge"
|
||||
onClose: () => void
|
||||
}>,
|
||||
) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
|
||||
let dismiss = false
|
||||
const width = () => {
|
||||
if (props.size === "xlarge") return 116
|
||||
if (props.size === "large") return 88
|
||||
return 60
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
onMouseDown={() => {
|
||||
dismiss = !!renderer.getSelection()
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
if (dismiss) {
|
||||
dismiss = false
|
||||
return
|
||||
}
|
||||
props.onClose?.()
|
||||
}}
|
||||
width={dimensions().width}
|
||||
height={dimensions().height}
|
||||
alignItems="center"
|
||||
position="absolute"
|
||||
zIndex={3000}
|
||||
paddingTop={dimensions().height / 4}
|
||||
left={0}
|
||||
top={0}
|
||||
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
|
||||
>
|
||||
<box
|
||||
onMouseUp={(e: { stopPropagation(): void }) => {
|
||||
dismiss = false
|
||||
e.stopPropagation()
|
||||
}}
|
||||
width={width()}
|
||||
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" | "xlarge",
|
||||
})
|
||||
|
||||
const renderer = useRenderer()
|
||||
const modeStack = useOpencodeModeStack()
|
||||
|
||||
createEffect(() => {
|
||||
if (store.stack.length === 0) return
|
||||
const popMode = modeStack.push("modal")
|
||||
onCleanup(popMode)
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: store.stack.length > 0 && !renderer.getSelection()?.getSelectedText(),
|
||||
bindings: [
|
||||
{
|
||||
key: "escape",
|
||||
desc: "Close dialog",
|
||||
group: "Dialog",
|
||||
cmd: () => {
|
||||
if (renderer.getSelection()) {
|
||||
renderer.clearSelection()
|
||||
}
|
||||
const current = store.stack.at(-1)
|
||||
current?.onClose?.()
|
||||
setStore("stack", store.stack.slice(0, -1))
|
||||
refocus()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "ctrl+c",
|
||||
desc: "Close dialog",
|
||||
group: "Dialog",
|
||||
cmd: () => {
|
||||
if (renderer.getSelection()) {
|
||||
renderer.clearSelection()
|
||||
}
|
||||
const current = store.stack.at(-1)
|
||||
current?.onClose?.()
|
||||
setStore("stack", store.stack.slice(0, -1))
|
||||
refocus()
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
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
|
||||
focus?.blur()
|
||||
}
|
||||
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" | "xlarge") {
|
||||
setStore("size", size)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type DialogContext = ReturnType<typeof init>
|
||||
|
||||
const ctx = createContext<DialogContext>()
|
||||
|
||||
export function DialogProvider(props: ParentProps) {
|
||||
const value = init()
|
||||
const renderer = useRenderer()
|
||||
const toast = useToast()
|
||||
const environment = useTuiEnvironment()
|
||||
const platform = useOptionalTuiPlatform()
|
||||
|
||||
function copySelection() {
|
||||
const text = renderer.getSelection()?.getSelectedText()
|
||||
if (!text || !platform?.clipboard?.write) return false
|
||||
void platform.clipboard.write(text).then(
|
||||
() => toast.show({ message: "Copied to clipboard", variant: "info" }),
|
||||
(error) => toast.error(error),
|
||||
)
|
||||
renderer.clearSelection()
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
<ctx.Provider value={value}>
|
||||
{props.children}
|
||||
<box
|
||||
position="absolute"
|
||||
zIndex={3000}
|
||||
onMouseDown={(evt: { button: number; preventDefault(): void; stopPropagation(): void }) => {
|
||||
if (environment.capabilities.copyOnSelect) return
|
||||
if (evt.button !== MouseButton.RIGHT) return
|
||||
|
||||
if (!copySelection()) return
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
}}
|
||||
onMouseUp={environment.capabilities.copyOnSelect ? copySelection : undefined}
|
||||
>
|
||||
<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
|
||||
}
|
||||
Reference in New Issue
Block a user