@tooee/choose 0.2.1 → 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 +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +5 -40
- package/dist/launch.js.map +1 -1
- package/dist/use-choose-dialog.d.ts +69 -0
- package/dist/use-choose-dialog.d.ts.map +1 -0
- package/dist/use-choose-dialog.js +132 -0
- package/dist/use-choose-dialog.js.map +1 -0
- package/dist/use-choose-dialog.typecheck.d.ts +2 -0
- package/dist/use-choose-dialog.typecheck.d.ts.map +1 -0
- package/dist/use-choose-dialog.typecheck.js +32 -0
- package/dist/use-choose-dialog.typecheck.js.map +1 -0
- package/package.json +7 -7
- package/src/index.ts +8 -0
- package/src/launch.tsx +16 -53
- package/src/use-choose-dialog.tsx +210 -0
- package/src/use-choose-dialog.typecheck.ts +59 -0
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/launch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":"
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
package/dist/launch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":";AAAA,OAAO,
|
|
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 @@
|
|
|
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.
|
|
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.
|
|
41
|
-
"@tooee/fuzzy": "0.2.
|
|
42
|
-
"@tooee/layout": "0.2.
|
|
43
|
-
"@tooee/overlays": "0.2.
|
|
44
|
-
"@tooee/shell": "0.2.
|
|
45
|
-
"@tooee/themes": "0.2.
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|