@tooee/choose 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,29 @@ Terminal selection with fuzzy filtering. `Choose` and `ChooseOverlay` are compat
14
14
 
15
15
  Use `controllerRef` on an assembly, or the controller returned by `useChoose`, to set/clear the filter, move or set the active row, toggle selection, submit/cancel, change mode, or reload the current source.
16
16
 
17
+ ## Promise-based dialog
18
+
19
+ `useChooseDialog<T>()` opens `ChooseOverlay` as a modal dialog on the host's
20
+ overlay stack and resolves the typed selection:
21
+
22
+ ```tsx
23
+ const choose = useChooseDialog<Model>()
24
+
25
+ const model = await choose.open({
26
+ items: models, // readonly T[] or a sync/async loader
27
+ toItem: (m) => ({ text: m.label }), // optional when T is a ChooseItem
28
+ prompt: "Pick a model",
29
+ })
30
+ // T on select, T[] with { multi: true }, null on cancel/replacement/unmount
31
+ ```
32
+
33
+ Generics are retained without casts: single-select resolves `T | null`,
34
+ multi-select resolves `T[] | null` (Choose selection semantics: toggled items,
35
+ falling back to the active item). Each `open()` owns one overlay record
36
+ (unique id per call) and one owned modal command surface; the promise settles
37
+ exactly once. The host must render overlay content (`AppLayout` does; custom
38
+ hosts render `useCurrentOverlay()`).
39
+
17
40
  ## Command surfaces
18
41
 
19
42
  Built-in interaction uses `useCommand`, so a nested modal command surface suspends the chooser automatically. A filter also blurs while another modal surface owns input. Mouse events bypass command-surface arbitration; pass `suspended` to a custom `ChooseList` host when covered rows can remain visible.
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { ChooseHighlightedText } from "./ChooseHighlightedText.js";
12
12
  export type { ChooseHighlightedTextProps } from "./ChooseHighlightedText.js";
13
13
  export { useChoose } from "./use-choose.js";
14
14
  export type { ChooseCommandGroup, ChooseController, ChooseState, ChooseViewModel, UseChooseOptions, UseChooseResult, } from "./use-choose.js";
15
+ export { useChooseDialog } from "./use-choose-dialog.js";
16
+ export type { ChooseDialogHandle, ChooseDialogItems, ChooseDialogOptions, ChooseDialogOptionsBase, ChooseDialogToItem, } from "./use-choose-dialog.js";
15
17
  export { loadChooseSource } from "./source.js";
16
18
  export { launch } from "./launch.js";
17
19
  export type { ChooseLaunchOptions } from "./launch.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAChE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,YAAY,EACV,UAAU,EACV,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAChE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,YAAY,EACV,UAAU,EACV,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { ChooseFilter } from "./ChooseFilter.js";
5
5
  export { ChooseList } from "./ChooseList.js";
6
6
  export { ChooseHighlightedText } from "./ChooseHighlightedText.js";
7
7
  export { useChoose } from "./use-choose.js";
8
+ export { useChooseDialog } from "./use-choose-dialog.js";
8
9
  export { loadChooseSource } from "./source.js";
9
10
  export { launch } from "./launch.js";
10
11
  export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEhE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAElE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAS3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEhE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAElE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAS3C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAQxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEpF,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,qBAAqB,CAAA;IACtC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkDpF"}
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEpF,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,qBAAqB,CAAA;IACtC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAiBpF"}
package/dist/launch.js CHANGED
@@ -1,46 +1,11 @@
1
1
  import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import * as fs from "node:fs";
3
- import * as tty from "node:tty";
4
- import { createCliRenderer } from "@opentui/core";
5
- import { createRoot } from "@opentui/react";
6
- import { TooeeProvider, guardTerminalHealth } from "@tooee/shell";
2
+ import { runCliSession } from "@tooee/shell";
7
3
  import { Choose } from "./Choose.js";
8
4
  export async function launch(opts) {
9
- // When stdin is piped (not a TTY), open /dev/tty for keyboard input
10
- // so the renderer doesn't consume the piped data stream.
11
- const stdinIsPiped = !process.stdin.isTTY;
12
- let ttyStdin;
13
- if (stdinIsPiped) {
14
- const fd = fs.openSync("/dev/tty", "r");
15
- ttyStdin = new tty.ReadStream(fd);
16
- }
17
- return new Promise((resolve) => {
18
- let renderer = null;
19
- const cleanup = (result) => {
20
- if (renderer) {
21
- renderer.destroy();
22
- }
23
- if (ttyStdin) {
24
- ttyStdin.destroy();
25
- }
26
- resolve(result);
27
- };
28
- createCliRenderer({
29
- exitOnCtrlC: false,
30
- ...(ttyStdin ? { stdin: ttyStdin } : {}),
31
- })
32
- .then((r) => {
33
- renderer = r;
34
- guardTerminalHealth(r);
35
- createRoot(r).render(_jsx(TooeeProvider, { initialMode: "insert", children: _jsx(Choose, { contentProvider: opts.contentProvider, options: opts.options, actions: opts.actions, onConfirm: (result) => cleanup(result), onCancel: () => cleanup(null) }) }));
36
- })
37
- .catch(() => {
38
- // Renderer init failed — release /dev/tty and resolve so the caller never hangs.
39
- if (ttyStdin) {
40
- ttyStdin.destroy();
41
- }
42
- resolve(null);
43
- });
5
+ return runCliSession(({ resolve, cancel }) => (_jsx(Choose, { contentProvider: opts.contentProvider, options: opts.options, actions: opts.actions, onConfirm: resolve, onCancel: cancel })), {
6
+ exitOnCtrlC: false,
7
+ provider: { initialMode: "insert" },
8
+ stdinPolicy: "tty-if-piped",
44
9
  });
45
10
  }
46
11
  //# sourceMappingURL=launch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AASpC,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAyB;IACpD,oEAAoE;IACpE,yDAAyD;IACzD,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAA;IACzC,IAAI,QAAoC,CAAA;IACxC,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QACvC,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IACnC,CAAC;IAED,OAAO,IAAI,OAAO,CAAsB,CAAC,OAAO,EAAE,EAAE;QAClD,IAAI,QAAQ,GAAyD,IAAI,CAAA;QAEzE,MAAM,OAAO,GAAG,CAAC,MAA2B,EAAE,EAAE;YAC9C,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,OAAO,EAAE,CAAA;YACpB,CAAC;YACD,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,OAAO,EAAE,CAAA;YACpB,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,CAAA;QACjB,CAAC,CAAA;QAED,iBAAiB,CAAC;YAChB,WAAW,EAAE,KAAK;YAClB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAwC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE,CAAC;aACC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACV,QAAQ,GAAG,CAAC,CAAA;YACZ,mBAAmB,CAAC,CAAC,CAAC,CAAA;YACtB,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAClB,KAAC,aAAa,IAAC,WAAW,EAAC,QAAQ,YACjC,KAAC,MAAM,IACL,eAAe,EAAE,IAAI,CAAC,eAAe,EACrC,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EACtC,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAC7B,GACY,CACjB,CAAA;QACH,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,iFAAiF;YACjF,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,OAAO,EAAE,CAAA;YACpB,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,CAAA;QACf,CAAC,CAAC,CAAA;IACN,CAAC,CAAC,CAAA;AACJ,CAAC"}
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AASpC,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAyB;IACpD,OAAO,aAAa,CAClB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CACvB,KAAC,MAAM,IACL,eAAe,EAAE,IAAI,CAAC,eAAe,EACrC,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,SAAS,EAAE,OAAO,EAClB,QAAQ,EAAE,MAAM,GAChB,CACH,EACD;QACE,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE;QACnC,WAAW,EAAE,cAAc;KAC5B,CACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,69 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ActionDefinition } from "@tooee/commands";
3
+ import type { ChooseListProps } from "./ChooseList.js";
4
+ import type { ChoosePanelProps } from "./ChoosePanel.js";
5
+ import type { ChooseItem } from "./types.js";
6
+ /** Typed item source: a fixed list, or a loader invoked when the dialog opens. */
7
+ export type ChooseDialogItems<T> = readonly T[] | (() => readonly T[] | Promise<readonly T[]>);
8
+ export interface ChooseDialogOptionsBase<T> {
9
+ items: ChooseDialogItems<T>;
10
+ /** Multi-select: Tab toggles, Enter submits the selection (default false). */
11
+ multi?: boolean;
12
+ prompt?: string;
13
+ placeholder?: string;
14
+ emptyMessage?: string;
15
+ /**
16
+ * Extra commands registered on the dialog's own command surface. Handlers
17
+ * may open further dialogs; the nested dialog suspends this one until it
18
+ * settles.
19
+ */
20
+ commands?: ActionDefinition[];
21
+ /** Chrome pass-throughs (see ChooseOverlay). */
22
+ renderItem?: ChooseListProps["renderItem"];
23
+ hints?: ChoosePanelProps["hints"];
24
+ statusRight?: ReactNode;
25
+ footer?: ReactNode;
26
+ inset?: ChoosePanelProps["inset"];
27
+ }
28
+ /**
29
+ * Projection from a typed item to the displayed `ChooseItem`. Optional when
30
+ * `T` is itself a `ChooseItem`; required otherwise, which is what keeps the
31
+ * public generic cast-free for arbitrary item types.
32
+ */
33
+ export type ChooseDialogToItem<T> = [T] extends [ChooseItem] ? {
34
+ toItem?: (item: T) => ChooseItem;
35
+ } : {
36
+ toItem: (item: T) => ChooseItem;
37
+ };
38
+ export type ChooseDialogOptions<T> = ChooseDialogOptionsBase<T> & ChooseDialogToItem<T>;
39
+ export interface ChooseDialogHandle<T> {
40
+ /**
41
+ * Open a modal chooser and resolve with the chosen item(s), or `null` when
42
+ * the dialog is cancelled, replaced, or unmounted. Settles exactly once per
43
+ * call. Result shape follows Choose selection semantics: single-select
44
+ * resolves the confirmed item; multi-select resolves the toggled items
45
+ * (falling back to the active item when nothing is toggled).
46
+ */
47
+ open(options: ChooseDialogOptions<T> & {
48
+ multi: true;
49
+ }): Promise<T[] | null>;
50
+ open(options: ChooseDialogOptions<T> & {
51
+ multi?: false;
52
+ }): Promise<T | null>;
53
+ }
54
+ /**
55
+ * Promise-based modal chooser dialog on the overlay stack.
56
+ *
57
+ * Each `open()` owns one modal overlay record and one owned command surface
58
+ * (via `ownCommands`), so the host app's commands are suspended and its global
59
+ * mode is never touched while the dialog is up. The returned promise settles
60
+ * exactly once: with the typed selection, or with `null` on cancel (Escape in
61
+ * cursor mode, `q`, or the close button), same-id replacement, or unmount of
62
+ * the owning component.
63
+ *
64
+ * The overlay renders wherever the host presents overlay content
65
+ * (`AppLayout` does this automatically; custom hosts render
66
+ * `useCurrentOverlay()`).
67
+ */
68
+ export declare function useChooseDialog<T>(): ChooseDialogHandle<T>;
69
+ //# sourceMappingURL=use-choose-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-choose-dialog.d.ts","sourceRoot":"","sources":["../src/use-choose-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAGvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,YAAY,CAAA;AAU1D,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,SAAS,CAAC,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAA;AAE9F,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAA;IAC3B,8EAA8E;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC7B,gDAAgD;IAChD,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAA;IAC1C,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACjC,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;CAClC;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,GACxD;IAAE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,UAAU,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,UAAU,CAAA;CAAE,CAAA;AAEvC,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAA;AAEvF,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG;QAAE,KAAK,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IAC5E,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;CAC7E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,KAAK,kBAAkB,CAAC,CAAC,CAAC,CAoI1D"}
@@ -0,0 +1,132 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { useOverlay } from "@tooee/overlays";
4
+ import { ChooseOverlay } from "./ChooseOverlay.js";
5
+ /**
6
+ * Per-open unique overlay id. A module-level sequence guarantees that
7
+ * concurrent dialogs — including dialogs opened from separate providers in the
8
+ * same process — never share an overlay id, so one dialog can never displace
9
+ * another through same-id replacement.
10
+ */
11
+ let chooseDialogSequence = 0;
12
+ /**
13
+ * Promise-based modal chooser dialog on the overlay stack.
14
+ *
15
+ * Each `open()` owns one modal overlay record and one owned command surface
16
+ * (via `ownCommands`), so the host app's commands are suspended and its global
17
+ * mode is never touched while the dialog is up. The returned promise settles
18
+ * exactly once: with the typed selection, or with `null` on cancel (Escape in
19
+ * cursor mode, `q`, or the close button), same-id replacement, or unmount of
20
+ * the owning component.
21
+ *
22
+ * The overlay renders wherever the host presents overlay content
23
+ * (`AppLayout` does this automatically; custom hosts render
24
+ * `useCurrentOverlay()`).
25
+ */
26
+ export function useChooseDialog() {
27
+ const overlay = useOverlay();
28
+ const overlayRef = useRef(overlay);
29
+ overlayRef.current = overlay;
30
+ // Open dialogs by overlay id, so unmounting the owner closes (and thereby
31
+ // settles) every dialog it opened.
32
+ const openHandlesRef = useRef(new Map());
33
+ const unmountedRef = useRef(false);
34
+ useEffect(() => {
35
+ unmountedRef.current = false;
36
+ const handles = openHandlesRef.current;
37
+ return () => {
38
+ unmountedRef.current = true;
39
+ // Map iteration tolerates deletion: settle() removes entries as each
40
+ // close lands.
41
+ for (const handle of handles.values())
42
+ handle.close("unmounted");
43
+ handles.clear();
44
+ };
45
+ }, []);
46
+ // Stable identity so command handlers and effects can capture the handle.
47
+ const handleRef = useRef(null);
48
+ if (handleRef.current === null) {
49
+ const open = (options) => {
50
+ return new Promise((resolvePromise) => {
51
+ if (unmountedRef.current) {
52
+ resolvePromise(null);
53
+ return;
54
+ }
55
+ const id = `choose-dialog-${++chooseDialogSequence}`;
56
+ let settled = false;
57
+ const settle = (value) => {
58
+ if (settled)
59
+ return;
60
+ settled = true;
61
+ openHandlesRef.current.delete(id);
62
+ resolvePromise(value);
63
+ };
64
+ // Displayed rows map back to typed items by identity: every mapped
65
+ // row is a fresh object (spread copy), so duplicates in `items` and
66
+ // `toItem` results that share references stay unambiguous.
67
+ // Safe default: `toItem` may only be omitted when T is a ChooseItem
68
+ // (enforced by ChooseDialogToItem).
69
+ const toItem = options.toItem ?? ((item) => item);
70
+ const rowToValue = new Map();
71
+ const mapValues = (values) => {
72
+ rowToValue.clear();
73
+ return values.map((value) => {
74
+ const row = { ...toItem(value) };
75
+ rowToValue.set(row, value);
76
+ return row;
77
+ });
78
+ };
79
+ // Created once per open() so the source identity is stable across
80
+ // overlay re-renders (a fresh source each render would reload forever).
81
+ const items = options.items;
82
+ const source = Array.isArray(items)
83
+ ? mapValues(items)
84
+ : () => {
85
+ const loaded = items();
86
+ return loaded instanceof Promise ? loaded.then(mapValues) : mapValues(loaded);
87
+ };
88
+ const multi = options.multi === true;
89
+ const shared = {
90
+ items: source,
91
+ prompt: options.prompt,
92
+ placeholder: options.placeholder,
93
+ emptyMessage: options.emptyMessage,
94
+ commands: options.commands,
95
+ renderItem: options.renderItem,
96
+ hints: options.hints,
97
+ statusRight: options.statusRight,
98
+ footer: options.footer,
99
+ inset: options.inset,
100
+ };
101
+ const handle = overlayRef.current.open(id, () => multi ? (_jsx(ChooseOverlay, { ...shared, multi: true, onSubmit: (result) => {
102
+ if (settled)
103
+ return;
104
+ const values = result.items.flatMap((row) => {
105
+ const value = rowToValue.get(row);
106
+ return value === undefined ? [] : [value];
107
+ });
108
+ settle(values);
109
+ handle.close("close");
110
+ }, onCancel: () => handle.close("escape") })) : (_jsx(ChooseOverlay, { ...shared, onSelect: (row) => {
111
+ if (settled)
112
+ return;
113
+ const value = rowToValue.get(row);
114
+ settle(value === undefined ? null : value);
115
+ handle.close("close");
116
+ }, onCancel: () => handle.close("escape") })), undefined, {
117
+ ownCommands: true,
118
+ role: "modal",
119
+ surfaceMode: "insert",
120
+ // Single settlement funnel: every close path (cancel, escape,
121
+ // replacement, unmount, external closeTop) lands here. Select and
122
+ // submit settle first, making this a no-op.
123
+ onClose: () => settle(null),
124
+ });
125
+ openHandlesRef.current.set(id, handle);
126
+ });
127
+ };
128
+ handleRef.current = { open: open };
129
+ }
130
+ return handleRef.current;
131
+ }
132
+ //# sourceMappingURL=use-choose-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-choose-dialog.js","sourceRoot":"","sources":["../src/use-choose-dialog.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function chooseDialogTypeChecks(): Promise<void>;
2
+ //# sourceMappingURL=use-choose-dialog.typecheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-choose-dialog.typecheck.d.ts","sourceRoot":"","sources":["../src/use-choose-dialog.typecheck.ts"],"names":[],"mappings":"AAwBA,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkC5D"}
@@ -0,0 +1,32 @@
1
+ // Never called; exists only to be typechecked.
2
+ export async function chooseDialogTypeChecks() {
3
+ // --- Single select resolves T | null, cast-free ---------------------------
4
+ const single = await modelDialog.open({
5
+ items: [{ id: "a", label: "A" }],
6
+ toItem: (model) => ({ text: model.label }),
7
+ });
8
+ if (single !== null) {
9
+ expectType(single.id);
10
+ }
11
+ // --- Multi select resolves T[] | null, cast-free ---------------------------
12
+ const multi = await modelDialog.open({
13
+ items: [{ id: "a", label: "A" }],
14
+ multi: true,
15
+ toItem: (model) => ({ text: model.label }),
16
+ });
17
+ if (multi !== null) {
18
+ expectType(multi.map((model) => model.id));
19
+ }
20
+ // --- toItem may be omitted only when T is a ChooseItem ---------------------
21
+ const row = await rowDialog.open({ items: [{ text: "one" }] });
22
+ if (row !== null) {
23
+ expectType(row.text);
24
+ }
25
+ // @ts-expect-error toItem is required when T is not a ChooseItem
26
+ await modelDialog.open({ items: [{ id: "a", label: "A" }] });
27
+ // @ts-expect-error toItem must return a ChooseItem
28
+ await modelDialog.open({ items: [{ id: "a", label: "A" }], toItem: (model) => model });
29
+ // @ts-expect-error items must be T, not ChooseItem
30
+ await modelDialog.open({ items: [{ text: "raw" }], toItem: (model) => ({ text: model.label }) });
31
+ }
32
+ //# sourceMappingURL=use-choose-dialog.typecheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-choose-dialog.typecheck.js","sourceRoot":"","sources":["../src/use-choose-dialog.typecheck.ts"],"names":[],"mappings":"AAuBA,+CAA+C;AAC/C,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC1C,6EAA6E;IAC7E,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC;QACpC,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QAChC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KAC3C,CAAC,CAAA;IACF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,UAAU,CAAS,MAAM,CAAC,EAAE,CAAC,CAAA;IAC/B,CAAC;IAED,8EAA8E;IAC9E,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC;QACnC,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QAChC,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KAC3C,CAAC,CAAA;IACF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,UAAU,CAAW,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;IACtD,CAAC;IAED,8EAA8E;IAC9E,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IAC9D,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,UAAU,CAAS,GAAG,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,iEAAiE;IACjE,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;IAE5D,mDAAmD;IACnD,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAA;IAEtF,mDAAmD;IACnD,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;AAClG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tooee/choose",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Terminal selection with fuzzy filtering",
5
5
  "keywords": [
6
6
  "cli",
@@ -37,12 +37,12 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@tooee/commands": "0.2.0",
41
- "@tooee/fuzzy": "0.2.0",
42
- "@tooee/layout": "0.2.0",
43
- "@tooee/overlays": "0.2.0",
44
- "@tooee/shell": "0.2.0",
45
- "@tooee/themes": "0.2.0"
40
+ "@tooee/commands": "0.2.2",
41
+ "@tooee/fuzzy": "0.2.2",
42
+ "@tooee/layout": "0.2.2",
43
+ "@tooee/overlays": "0.2.2",
44
+ "@tooee/shell": "0.2.2",
45
+ "@tooee/themes": "0.2.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opentui/core": "^0.4.3",
package/src/index.ts CHANGED
@@ -19,6 +19,14 @@ export type {
19
19
  UseChooseOptions,
20
20
  UseChooseResult,
21
21
  } from "./use-choose.js"
22
+ export { useChooseDialog } from "./use-choose-dialog.js"
23
+ export type {
24
+ ChooseDialogHandle,
25
+ ChooseDialogItems,
26
+ ChooseDialogOptions,
27
+ ChooseDialogOptionsBase,
28
+ ChooseDialogToItem,
29
+ } from "./use-choose-dialog.js"
22
30
  export { loadChooseSource } from "./source.js"
23
31
  export { launch } from "./launch.js"
24
32
  export type { ChooseLaunchOptions } from "./launch.js"
package/src/launch.tsx CHANGED
@@ -1,8 +1,4 @@
1
- import * as fs from "node:fs"
2
- import * as tty from "node:tty"
3
- import { createCliRenderer } from "@opentui/core"
4
- import { createRoot } from "@opentui/react"
5
- import { TooeeProvider, guardTerminalHealth } from "@tooee/shell"
1
+ import { runCliSession } from "@tooee/shell"
6
2
  import type { ActionDefinition } from "@tooee/commands"
7
3
  import { Choose } from "./Choose.js"
8
4
  import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js"
@@ -14,53 +10,20 @@ export interface ChooseLaunchOptions {
14
10
  }
15
11
 
16
12
  export async function launch(opts: ChooseLaunchOptions): Promise<ChooseResult | null> {
17
- // When stdin is piped (not a TTY), open /dev/tty for keyboard input
18
- // so the renderer doesn't consume the piped data stream.
19
- const stdinIsPiped = !process.stdin.isTTY
20
- let ttyStdin: tty.ReadStream | undefined
21
- if (stdinIsPiped) {
22
- const fd = fs.openSync("/dev/tty", "r")
23
- ttyStdin = new tty.ReadStream(fd)
24
- }
25
-
26
- return new Promise<ChooseResult | null>((resolve) => {
27
- let renderer: Awaited<ReturnType<typeof createCliRenderer>> | null = null
28
-
29
- const cleanup = (result: ChooseResult | null) => {
30
- if (renderer) {
31
- renderer.destroy()
32
- }
33
- if (ttyStdin) {
34
- ttyStdin.destroy()
35
- }
36
- resolve(result)
37
- }
38
-
39
- createCliRenderer({
13
+ return runCliSession<ChooseResult>(
14
+ ({ resolve, cancel }) => (
15
+ <Choose
16
+ contentProvider={opts.contentProvider}
17
+ options={opts.options}
18
+ actions={opts.actions}
19
+ onConfirm={resolve}
20
+ onCancel={cancel}
21
+ />
22
+ ),
23
+ {
40
24
  exitOnCtrlC: false,
41
- ...(ttyStdin ? { stdin: ttyStdin as unknown as NodeJS.ReadStream } : {}),
42
- })
43
- .then((r) => {
44
- renderer = r
45
- guardTerminalHealth(r)
46
- createRoot(r).render(
47
- <TooeeProvider initialMode="insert">
48
- <Choose
49
- contentProvider={opts.contentProvider}
50
- options={opts.options}
51
- actions={opts.actions}
52
- onConfirm={(result) => cleanup(result)}
53
- onCancel={() => cleanup(null)}
54
- />
55
- </TooeeProvider>,
56
- )
57
- })
58
- .catch(() => {
59
- // Renderer init failed — release /dev/tty and resolve so the caller never hangs.
60
- if (ttyStdin) {
61
- ttyStdin.destroy()
62
- }
63
- resolve(null)
64
- })
65
- })
25
+ provider: { initialMode: "insert" },
26
+ stdinPolicy: "tty-if-piped",
27
+ },
28
+ )
66
29
  }
@@ -0,0 +1,210 @@
1
+ import { useEffect, useRef, type ReactNode } from "react"
2
+ import type { ActionDefinition } from "@tooee/commands"
3
+ import { useOverlay, type OverlayHandle } from "@tooee/overlays"
4
+ import { ChooseOverlay } from "./ChooseOverlay.js"
5
+ import type { ChooseListProps } from "./ChooseList.js"
6
+ import type { ChoosePanelProps } from "./ChoosePanel.js"
7
+ import type { ChooseItem, ChooseSource } from "./types.js"
8
+
9
+ /**
10
+ * Per-open unique overlay id. A module-level sequence guarantees that
11
+ * concurrent dialogs — including dialogs opened from separate providers in the
12
+ * same process — never share an overlay id, so one dialog can never displace
13
+ * another through same-id replacement.
14
+ */
15
+ let chooseDialogSequence = 0
16
+
17
+ /** Typed item source: a fixed list, or a loader invoked when the dialog opens. */
18
+ export type ChooseDialogItems<T> = readonly T[] | (() => readonly T[] | Promise<readonly T[]>)
19
+
20
+ export interface ChooseDialogOptionsBase<T> {
21
+ items: ChooseDialogItems<T>
22
+ /** Multi-select: Tab toggles, Enter submits the selection (default false). */
23
+ multi?: boolean
24
+ prompt?: string
25
+ placeholder?: string
26
+ emptyMessage?: string
27
+ /**
28
+ * Extra commands registered on the dialog's own command surface. Handlers
29
+ * may open further dialogs; the nested dialog suspends this one until it
30
+ * settles.
31
+ */
32
+ commands?: ActionDefinition[]
33
+ /** Chrome pass-throughs (see ChooseOverlay). */
34
+ renderItem?: ChooseListProps["renderItem"]
35
+ hints?: ChoosePanelProps["hints"]
36
+ statusRight?: ReactNode
37
+ footer?: ReactNode
38
+ inset?: ChoosePanelProps["inset"]
39
+ }
40
+
41
+ /**
42
+ * Projection from a typed item to the displayed `ChooseItem`. Optional when
43
+ * `T` is itself a `ChooseItem`; required otherwise, which is what keeps the
44
+ * public generic cast-free for arbitrary item types.
45
+ */
46
+ export type ChooseDialogToItem<T> = [T] extends [ChooseItem]
47
+ ? { toItem?: (item: T) => ChooseItem }
48
+ : { toItem: (item: T) => ChooseItem }
49
+
50
+ export type ChooseDialogOptions<T> = ChooseDialogOptionsBase<T> & ChooseDialogToItem<T>
51
+
52
+ export interface ChooseDialogHandle<T> {
53
+ /**
54
+ * Open a modal chooser and resolve with the chosen item(s), or `null` when
55
+ * the dialog is cancelled, replaced, or unmounted. Settles exactly once per
56
+ * call. Result shape follows Choose selection semantics: single-select
57
+ * resolves the confirmed item; multi-select resolves the toggled items
58
+ * (falling back to the active item when nothing is toggled).
59
+ */
60
+ open(options: ChooseDialogOptions<T> & { multi: true }): Promise<T[] | null>
61
+ open(options: ChooseDialogOptions<T> & { multi?: false }): Promise<T | null>
62
+ }
63
+
64
+ /**
65
+ * Promise-based modal chooser dialog on the overlay stack.
66
+ *
67
+ * Each `open()` owns one modal overlay record and one owned command surface
68
+ * (via `ownCommands`), so the host app's commands are suspended and its global
69
+ * mode is never touched while the dialog is up. The returned promise settles
70
+ * exactly once: with the typed selection, or with `null` on cancel (Escape in
71
+ * cursor mode, `q`, or the close button), same-id replacement, or unmount of
72
+ * the owning component.
73
+ *
74
+ * The overlay renders wherever the host presents overlay content
75
+ * (`AppLayout` does this automatically; custom hosts render
76
+ * `useCurrentOverlay()`).
77
+ */
78
+ export function useChooseDialog<T>(): ChooseDialogHandle<T> {
79
+ const overlay = useOverlay()
80
+ const overlayRef = useRef(overlay)
81
+ overlayRef.current = overlay
82
+
83
+ // Open dialogs by overlay id, so unmounting the owner closes (and thereby
84
+ // settles) every dialog it opened.
85
+ const openHandlesRef = useRef(new Map<string, OverlayHandle<undefined>>())
86
+ const unmountedRef = useRef(false)
87
+
88
+ useEffect(() => {
89
+ unmountedRef.current = false
90
+ const handles = openHandlesRef.current
91
+ return () => {
92
+ unmountedRef.current = true
93
+ // Map iteration tolerates deletion: settle() removes entries as each
94
+ // close lands.
95
+ for (const handle of handles.values()) handle.close("unmounted")
96
+ handles.clear()
97
+ }
98
+ }, [])
99
+
100
+ // Stable identity so command handlers and effects can capture the handle.
101
+ const handleRef = useRef<ChooseDialogHandle<T> | null>(null)
102
+ if (handleRef.current === null) {
103
+ const open = (
104
+ options: ChooseDialogOptionsBase<T> & { toItem?: (item: T) => ChooseItem },
105
+ ): Promise<T | T[] | null> => {
106
+ return new Promise<T | T[] | null>((resolvePromise) => {
107
+ if (unmountedRef.current) {
108
+ resolvePromise(null)
109
+ return
110
+ }
111
+
112
+ const id = `choose-dialog-${++chooseDialogSequence}`
113
+ let settled = false
114
+ const settle = (value: T | T[] | null) => {
115
+ if (settled) return
116
+ settled = true
117
+ openHandlesRef.current.delete(id)
118
+ resolvePromise(value)
119
+ }
120
+
121
+ // Displayed rows map back to typed items by identity: every mapped
122
+ // row is a fresh object (spread copy), so duplicates in `items` and
123
+ // `toItem` results that share references stay unambiguous.
124
+ // Safe default: `toItem` may only be omitted when T is a ChooseItem
125
+ // (enforced by ChooseDialogToItem).
126
+ const toItem = options.toItem ?? ((item: T) => item as unknown as ChooseItem)
127
+ const rowToValue = new Map<ChooseItem, T>()
128
+ const mapValues = (values: readonly T[]): ChooseItem[] => {
129
+ rowToValue.clear()
130
+ return values.map((value) => {
131
+ const row = { ...toItem(value) }
132
+ rowToValue.set(row, value)
133
+ return row
134
+ })
135
+ }
136
+
137
+ // Created once per open() so the source identity is stable across
138
+ // overlay re-renders (a fresh source each render would reload forever).
139
+ const items = options.items
140
+ const source: ChooseSource = Array.isArray(items)
141
+ ? mapValues(items as readonly T[])
142
+ : () => {
143
+ const loaded = (items as () => readonly T[] | Promise<readonly T[]>)()
144
+ return loaded instanceof Promise ? loaded.then(mapValues) : mapValues(loaded)
145
+ }
146
+
147
+ const multi = options.multi === true
148
+ const shared = {
149
+ items: source,
150
+ prompt: options.prompt,
151
+ placeholder: options.placeholder,
152
+ emptyMessage: options.emptyMessage,
153
+ commands: options.commands,
154
+ renderItem: options.renderItem,
155
+ hints: options.hints,
156
+ statusRight: options.statusRight,
157
+ footer: options.footer,
158
+ inset: options.inset,
159
+ }
160
+
161
+ const handle = overlayRef.current.open(
162
+ id,
163
+ () =>
164
+ multi ? (
165
+ <ChooseOverlay
166
+ {...shared}
167
+ multi
168
+ onSubmit={(result) => {
169
+ if (settled) return
170
+ const values = result.items.flatMap((row) => {
171
+ const value = rowToValue.get(row)
172
+ return value === undefined ? [] : [value]
173
+ })
174
+ settle(values)
175
+ handle.close("close")
176
+ }}
177
+ onCancel={() => handle.close("escape")}
178
+ />
179
+ ) : (
180
+ <ChooseOverlay
181
+ {...shared}
182
+ onSelect={(row) => {
183
+ if (settled) return
184
+ const value = rowToValue.get(row)
185
+ settle(value === undefined ? null : value)
186
+ handle.close("close")
187
+ }}
188
+ onCancel={() => handle.close("escape")}
189
+ />
190
+ ),
191
+ undefined,
192
+ {
193
+ ownCommands: true,
194
+ role: "modal",
195
+ surfaceMode: "insert",
196
+ // Single settlement funnel: every close path (cancel, escape,
197
+ // replacement, unmount, external closeTop) lands here. Select and
198
+ // submit settle first, making this a no-op.
199
+ onClose: () => settle(null),
200
+ },
201
+ )
202
+ openHandlesRef.current.set(id, handle)
203
+ })
204
+ }
205
+
206
+ handleRef.current = { open: open as ChooseDialogHandle<T>["open"] }
207
+ }
208
+
209
+ return handleRef.current
210
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Compile-time regression tests for the typed dialog generics. This module has
3
+ * no runtime behaviour and is never imported; it exists so that `tsc -b`
4
+ * (which only checks `src/`) fails if the public generic contract is ever
5
+ * loosened.
6
+ *
7
+ * The contract (roadmap item 4): `useChooseDialog<T>().open(...)` retains the
8
+ * consumer's item type without casts — single-select resolves `T | null`,
9
+ * multi-select resolves `T[] | null` — and `toItem` may only be omitted when
10
+ * `T` is itself a `ChooseItem`.
11
+ */
12
+ import type { ChooseItem } from "./types.js"
13
+ import type { ChooseDialogHandle } from "./use-choose-dialog.js"
14
+
15
+ interface Model {
16
+ id: string
17
+ label: string
18
+ }
19
+
20
+ declare const modelDialog: ChooseDialogHandle<Model>
21
+ declare const rowDialog: ChooseDialogHandle<ChooseItem>
22
+ declare function expectType<T>(value: T): void
23
+
24
+ // Never called; exists only to be typechecked.
25
+ export async function chooseDialogTypeChecks(): Promise<void> {
26
+ // --- Single select resolves T | null, cast-free ---------------------------
27
+ const single = await modelDialog.open({
28
+ items: [{ id: "a", label: "A" }],
29
+ toItem: (model) => ({ text: model.label }),
30
+ })
31
+ if (single !== null) {
32
+ expectType<string>(single.id)
33
+ }
34
+
35
+ // --- Multi select resolves T[] | null, cast-free ---------------------------
36
+ const multi = await modelDialog.open({
37
+ items: [{ id: "a", label: "A" }],
38
+ multi: true,
39
+ toItem: (model) => ({ text: model.label }),
40
+ })
41
+ if (multi !== null) {
42
+ expectType<string[]>(multi.map((model) => model.id))
43
+ }
44
+
45
+ // --- toItem may be omitted only when T is a ChooseItem ---------------------
46
+ const row = await rowDialog.open({ items: [{ text: "one" }] })
47
+ if (row !== null) {
48
+ expectType<string>(row.text)
49
+ }
50
+
51
+ // @ts-expect-error toItem is required when T is not a ChooseItem
52
+ await modelDialog.open({ items: [{ id: "a", label: "A" }] })
53
+
54
+ // @ts-expect-error toItem must return a ChooseItem
55
+ await modelDialog.open({ items: [{ id: "a", label: "A" }], toItem: (model) => model })
56
+
57
+ // @ts-expect-error items must be T, not ChooseItem
58
+ await modelDialog.open({ items: [{ text: "raw" }], toItem: (model) => ({ text: model.label }) })
59
+ }