/** * Promise-based input dialogs for shell and feature actions (IDEA's * `Messages.showInputDialog` and a simple chooser): * * const name = await promptInput({ title: "Save Layout", label: "Name:" }); * const picked = await chooseItem({ title: "Delete Layout", items: names }); * * Rendered by `InputDialogsHost` (in `OverlayHost`). */ import { useState, type ReactNode } from "react"; import { create } from "zustand"; import { t } from "@/platform/i18n"; import { Dialog, Input, List, T } from "@/ui"; import styles from "./inputDialogs.module.css"; export interface PromptInputOptions { title: string; label?: string; initial?: string; placeholder?: string; okLabel?: string; /** An error message for `value`, or null when valid (default: must not be blank). */ validate?: (value: string) => string | null; /** Extra content under the field (hints). */ hint?: ReactNode; } export interface ChooseItemOptions { title: string; items: readonly string[]; /** Shown when `items` is empty. */ empty?: string; } type Request = | { kind: "prompt"; id: number; options: PromptInputOptions; resolve: (v: string | null) => void } | { kind: "choose"; id: number; options: ChooseItemOptions; resolve: (v: string | null) => void }; const useInputDialogs = create<{ stack: Request[] }>(() => ({ stack: [] })); let seq = 0; function open(make: (id: number, finish: (v: string | null) => void) => Request): Promise { return new Promise((resolve) => { const id = ++seq; const finish = (v: string | null) => { useInputDialogs.setState((s) => ({ stack: s.stack.filter((r) => r.id !== id) })); resolve(v); }; useInputDialogs.setState((s) => ({ stack: [...s.stack, make(id, finish)] })); }); } /** A text field dialog; resolves with the trimmed value, or null when cancelled. */ export function promptInput(options: PromptInputOptions): Promise { return open((id, resolve) => ({ kind: "prompt", id, options, resolve })); } /** A list to pick one entry from; resolves with it, or null when cancelled. */ export function chooseItem(options: ChooseItemOptions): Promise { return open((id, resolve) => ({ kind: "choose", id, options, resolve })); } export function InputDialogsHost() { const stack = useInputDialogs((s) => s.stack); return ( <> {stack.map((r) => r.kind === "prompt" ? ( ) : ( ), )} ); } const notBlank = (v: string) => (v.trim() ? null : "Value must not be empty"); function PromptDialog({ options, done }: { options: PromptInputOptions; done: (v: string | null) => void }) { const [value, setValue] = useState(options.initial ?? ""); const [touched, setTouched] = useState(false); const error = (options.validate ?? notBlank)(value); const submit = () => { setTouched(true); if (!error) done(value.trim()); }; return ( done(null)} width={420} buttons={[ { label: options.okLabel ?? "OK", role: "default", disabled: !!error, onClick: submit }, { label: t("common.cancel"), role: "cancel" }, ]} >
{ e.preventDefault(); submit(); }} > {options.label && ( )} { setTouched(true); setValue(e.target.value); }} /> {touched && error && (
{error}
)} {options.hint &&
{options.hint}
}
); } function ChooseDialog({ options, done }: { options: ChooseItemOptions; done: (v: string | null) => void }) { const [active, setActive] = useState(0); const pick = options.items[active]; return ( done(null)} width={360} buttons={[ { label: "OK", role: "default", disabled: pick === undefined, onClick: () => pick !== undefined && done(pick) }, { label: t("common.cancel"), role: "cancel" }, ]} >
item} activeIndex={active} onActiveChange={setActive} onActivate={(item) => done(item)} rowHeight={24} className={styles.list} aria-label={options.title} empty={
{options.empty ?? "Nothing to choose from"}
} renderItem={(item, state) =>
{item}
} />
); }