@tooee/choose 0.2.3 → 0.3.0

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.
Files changed (94) hide show
  1. package/README.md +2 -2
  2. package/dist/{ChooseFilter.d.ts → choose-filter.d.ts} +2 -2
  3. package/dist/choose-filter.d.ts.map +1 -0
  4. package/dist/choose-filter.js +11 -0
  5. package/dist/choose-filter.js.map +1 -0
  6. package/dist/choose-highlighted-text.d.ts +9 -0
  7. package/dist/choose-highlighted-text.d.ts.map +1 -0
  8. package/dist/{ChooseHighlightedText.js → choose-highlighted-text.js} +10 -8
  9. package/dist/choose-highlighted-text.js.map +1 -0
  10. package/dist/{ChooseList.d.ts → choose-list.d.ts} +3 -3
  11. package/dist/choose-list.d.ts.map +1 -0
  12. package/dist/{ChooseList.js → choose-list.js} +20 -15
  13. package/dist/choose-list.js.map +1 -0
  14. package/dist/{ChooseOverlay.d.ts → choose-overlay.d.ts} +6 -6
  15. package/dist/choose-overlay.d.ts.map +1 -0
  16. package/dist/{ChooseOverlay.js → choose-overlay.js} +15 -13
  17. package/dist/choose-overlay.js.map +1 -0
  18. package/dist/{ChoosePanel.d.ts → choose-panel.d.ts} +5 -5
  19. package/dist/choose-panel.d.ts.map +1 -0
  20. package/dist/choose-panel.js +31 -0
  21. package/dist/choose-panel.js.map +1 -0
  22. package/dist/choose.d.ts +28 -0
  23. package/dist/choose.d.ts.map +1 -0
  24. package/dist/{Choose.js → choose.js} +25 -18
  25. package/dist/choose.js.map +1 -0
  26. package/dist/default-provider.d.ts +2 -2
  27. package/dist/default-provider.d.ts.map +1 -1
  28. package/dist/default-provider.js +4 -4
  29. package/dist/default-provider.js.map +1 -1
  30. package/dist/fuzzy.d.ts +1 -1
  31. package/dist/fuzzy.d.ts.map +1 -1
  32. package/dist/fuzzy.js +5 -5
  33. package/dist/fuzzy.js.map +1 -1
  34. package/dist/index.d.ts +12 -12
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/launch.d.ts +1 -1
  39. package/dist/launch.d.ts.map +1 -1
  40. package/dist/launch.js +5 -4
  41. package/dist/launch.js.map +1 -1
  42. package/dist/source.d.ts +2 -2
  43. package/dist/source.d.ts.map +1 -1
  44. package/dist/source.js +8 -6
  45. package/dist/source.js.map +1 -1
  46. package/dist/types.d.ts +1 -1
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/use-choose-dialog.d.ts +12 -10
  49. package/dist/use-choose-dialog.d.ts.map +1 -1
  50. package/dist/use-choose-dialog.js +98 -77
  51. package/dist/use-choose-dialog.js.map +1 -1
  52. package/dist/use-choose-dialog.typecheck.d.ts +1 -1
  53. package/dist/use-choose-dialog.typecheck.d.ts.map +1 -1
  54. package/dist/use-choose-dialog.typecheck.js +5 -5
  55. package/dist/use-choose-dialog.typecheck.js.map +1 -1
  56. package/dist/use-choose.d.ts +16 -16
  57. package/dist/use-choose.d.ts.map +1 -1
  58. package/dist/use-choose.js +176 -138
  59. package/dist/use-choose.js.map +1 -1
  60. package/package.json +7 -7
  61. package/src/{ChooseFilter.tsx → choose-filter.tsx} +18 -16
  62. package/src/choose-highlighted-text.tsx +49 -0
  63. package/src/{ChooseList.tsx → choose-list.tsx} +73 -60
  64. package/src/choose-overlay.tsx +96 -0
  65. package/src/{ChoosePanel.tsx → choose-panel.tsx} +41 -34
  66. package/src/choose.tsx +142 -0
  67. package/src/default-provider.ts +18 -15
  68. package/src/fuzzy.ts +16 -16
  69. package/src/index.ts +23 -23
  70. package/src/launch.tsx +15 -12
  71. package/src/source.ts +15 -9
  72. package/src/types.ts +12 -12
  73. package/src/use-choose-dialog.tsx +168 -139
  74. package/src/use-choose-dialog.typecheck.ts +19 -18
  75. package/src/use-choose.ts +349 -302
  76. package/dist/Choose.d.ts +0 -21
  77. package/dist/Choose.d.ts.map +0 -1
  78. package/dist/Choose.js.map +0 -1
  79. package/dist/ChooseFilter.d.ts.map +0 -1
  80. package/dist/ChooseFilter.js +0 -9
  81. package/dist/ChooseFilter.js.map +0 -1
  82. package/dist/ChooseHighlightedText.d.ts +0 -9
  83. package/dist/ChooseHighlightedText.d.ts.map +0 -1
  84. package/dist/ChooseHighlightedText.js.map +0 -1
  85. package/dist/ChooseList.d.ts.map +0 -1
  86. package/dist/ChooseList.js.map +0 -1
  87. package/dist/ChooseOverlay.d.ts.map +0 -1
  88. package/dist/ChooseOverlay.js.map +0 -1
  89. package/dist/ChoosePanel.d.ts.map +0 -1
  90. package/dist/ChoosePanel.js +0 -25
  91. package/dist/ChoosePanel.js.map +0 -1
  92. package/src/Choose.tsx +0 -126
  93. package/src/ChooseHighlightedText.tsx +0 -42
  94. package/src/ChooseOverlay.tsx +0 -88
package/src/fuzzy.ts CHANGED
@@ -1,28 +1,28 @@
1
- import { fuzzyMatchPositions } from "@tooee/fuzzy"
2
- import type { ChooseItem } from "./types.js"
1
+ import { fuzzyMatchPositions } from "@tooee/fuzzy";
2
+ import type { ChooseItem } from "./types.js";
3
3
 
4
4
  export interface FuzzyMatch {
5
- item: ChooseItem
6
- originalIndex: number
7
- score: number
8
- positions: number[]
5
+ item: ChooseItem;
6
+ originalIndex: number;
7
+ score: number;
8
+ positions: number[];
9
9
  }
10
10
 
11
- export function fuzzyFilter(items: ChooseItem[], query: string): FuzzyMatch[] {
11
+ export const fuzzyFilter = function fuzzyFilter(items: ChooseItem[], query: string): FuzzyMatch[] {
12
12
  if (!query) {
13
- return items.map((item, i) => ({ item, originalIndex: i, score: 0, positions: [] }))
13
+ return items.map((item, i) => ({ item, originalIndex: i, positions: [], score: 0 }));
14
14
  }
15
15
 
16
- const results: FuzzyMatch[] = []
16
+ const results: FuzzyMatch[] = [];
17
17
 
18
- for (let i = 0; i < items.length; i++) {
19
- const item = items[i]
20
- const match = fuzzyMatchPositions(query, item.text)
18
+ for (let i = 0; i < items.length; i += 1) {
19
+ const item = items[i];
20
+ const match = fuzzyMatchPositions(query, item.text);
21
21
  if (match) {
22
- results.push({ item, originalIndex: i, score: match.score, positions: match.positions })
22
+ results.push({ item, originalIndex: i, positions: match.positions, score: match.score });
23
23
  }
24
24
  }
25
25
 
26
- results.sort((a, b) => b.score - a.score)
27
- return results
28
- }
26
+ results.sort((a, b) => b.score - a.score);
27
+ return results;
28
+ };
package/src/index.ts CHANGED
@@ -1,16 +1,16 @@
1
- export { Choose } from "./Choose.js"
2
- export type { ChooseProps } from "./Choose.js"
3
- export { ChooseOverlay } from "./ChooseOverlay.js"
4
- export type { ChooseOverlayProps } from "./ChooseOverlay.js"
5
- export { ChoosePanel, buildChooseHints } from "./ChoosePanel.js"
6
- export type { ChoosePanelProps, ChoosePanelInset, ChoosePanelInsetValue } from "./ChoosePanel.js"
7
- export { ChooseFilter } from "./ChooseFilter.js"
8
- export type { ChooseFilterProps } from "./ChooseFilter.js"
9
- export { ChooseList } from "./ChooseList.js"
10
- export type { ChooseItemRenderContext, ChooseListProps } from "./ChooseList.js"
11
- export { ChooseHighlightedText } from "./ChooseHighlightedText.js"
12
- export type { ChooseHighlightedTextProps } from "./ChooseHighlightedText.js"
13
- export { useChoose } from "./use-choose.js"
1
+ export { Choose } from "./choose.js";
2
+ export type { ChooseProps } from "./choose.js";
3
+ export { ChooseOverlay } from "./choose-overlay.js";
4
+ export type { ChooseOverlayProps } from "./choose-overlay.js";
5
+ export { ChoosePanel, buildChooseHints } from "./choose-panel.js";
6
+ export type { ChoosePanelProps, ChoosePanelInset, ChoosePanelInsetValue } from "./choose-panel.js";
7
+ export { ChooseFilter } from "./choose-filter.js";
8
+ export type { ChooseFilterProps } from "./choose-filter.js";
9
+ export { ChooseList } from "./choose-list.js";
10
+ export type { ChooseItemRenderContext, ChooseListProps } from "./choose-list.js";
11
+ export { ChooseHighlightedText } from "./choose-highlighted-text.js";
12
+ export type { ChooseHighlightedTextProps } from "./choose-highlighted-text.js";
13
+ export { useChoose } from "./use-choose.js";
14
14
  export type {
15
15
  ChooseCommandGroup,
16
16
  ChooseController,
@@ -18,25 +18,25 @@ export type {
18
18
  ChooseViewModel,
19
19
  UseChooseOptions,
20
20
  UseChooseResult,
21
- } from "./use-choose.js"
22
- export { useChooseDialog } from "./use-choose-dialog.js"
21
+ } from "./use-choose.js";
22
+ export { useChooseDialog } from "./use-choose-dialog.js";
23
23
  export type {
24
24
  ChooseDialogHandle,
25
25
  ChooseDialogItems,
26
26
  ChooseDialogOptions,
27
27
  ChooseDialogOptionsBase,
28
28
  ChooseDialogToItem,
29
- } from "./use-choose-dialog.js"
30
- export { loadChooseSource } from "./source.js"
31
- export { launch } from "./launch.js"
32
- export type { ChooseLaunchOptions } from "./launch.js"
33
- export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js"
34
- export { fuzzyFilter } from "./fuzzy.js"
35
- export type { FuzzyMatch } from "./fuzzy.js"
29
+ } from "./use-choose-dialog.js";
30
+ export { loadChooseSource } from "./source.js";
31
+ export { launch } from "./launch.js";
32
+ export type { ChooseLaunchOptions } from "./launch.js";
33
+ export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js";
34
+ export { fuzzyFilter } from "./fuzzy.js";
35
+ export type { FuzzyMatch } from "./fuzzy.js";
36
36
  export type {
37
37
  ChooseItem,
38
38
  ChooseContentProvider,
39
39
  ChooseSource,
40
40
  ChooseResult,
41
41
  ChooseOptions,
42
- } from "./types.js"
42
+ } from "./types.js";
package/src/launch.tsx CHANGED
@@ -1,17 +1,19 @@
1
- import { runCliSession } from "@tooee/shell"
2
- import type { ActionDefinition } from "@tooee/commands"
3
- import { Choose } from "./Choose.js"
4
- import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js"
1
+ import { runCliSession } from "@tooee/shell";
2
+ import type { ActionDefinition } from "@tooee/commands";
3
+ import { Choose } from "./choose.js";
4
+ import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js";
5
5
 
6
6
  export interface ChooseLaunchOptions {
7
- contentProvider: ChooseContentProvider
8
- options?: ChooseOptions
9
- actions?: ActionDefinition[]
7
+ contentProvider: ChooseContentProvider;
8
+ options?: ChooseOptions;
9
+ actions?: ActionDefinition[];
10
10
  }
11
11
 
12
- export async function launch(opts: ChooseLaunchOptions): Promise<ChooseResult | null> {
13
- return runCliSession<ChooseResult>(
14
- ({ resolve, cancel }) => (
12
+ export const launch = async function launch(
13
+ opts: ChooseLaunchOptions,
14
+ ): Promise<ChooseResult | null> {
15
+ const result = await runCliSession<ChooseResult>(
16
+ ({ resolve, cancel }): React.ReactNode => (
15
17
  <Choose
16
18
  contentProvider={opts.contentProvider}
17
19
  options={opts.options}
@@ -25,5 +27,6 @@ export async function launch(opts: ChooseLaunchOptions): Promise<ChooseResult |
25
27
  provider: { initialMode: "insert" },
26
28
  stdinPolicy: "tty-if-piped",
27
29
  },
28
- )
29
- }
30
+ );
31
+ return result;
32
+ };
package/src/source.ts CHANGED
@@ -1,12 +1,18 @@
1
- import type { ChooseItem, ChooseSource } from "./types.js"
1
+ import type { ChooseItem, ChooseSource } from "./types.js";
2
2
 
3
3
  /** Resolve every public Choose source shape through one compatibility boundary. */
4
- export function loadChooseSource(source: ChooseSource): ChooseItem[] | Promise<ChooseItem[]> {
5
- if (Array.isArray(source)) return source
6
- if (typeof source === "function") return source()
7
- return source.load()
8
- }
4
+ export const loadChooseSource = function loadChooseSource(
5
+ source: ChooseSource,
6
+ ): ChooseItem[] | Promise<ChooseItem[]> {
7
+ if (Array.isArray(source)) {
8
+ return source;
9
+ }
10
+ if (typeof source === "function") {
11
+ return source();
12
+ }
13
+ return source.load();
14
+ };
9
15
 
10
- export function chooseSourceError(error: unknown): string {
11
- return error instanceof Error ? error.message : String(error)
12
- }
16
+ export const chooseSourceError = function chooseSourceError(error: unknown): string {
17
+ return error instanceof Error ? error.message : String(error);
18
+ };
package/src/types.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  export interface ChooseItem {
2
- text: string
3
- value?: string
4
- icon?: string
5
- description?: string
2
+ text: string;
3
+ value?: string;
4
+ icon?: string;
5
+ description?: string;
6
6
  }
7
7
 
8
8
  export interface ChooseContentProvider {
9
- load(): Promise<ChooseItem[]> | ChooseItem[]
9
+ load: () => Promise<ChooseItem[]> | ChooseItem[];
10
10
  }
11
11
 
12
12
  /** Any item source accepted by the shared chooser core. */
13
13
  export type ChooseSource =
14
14
  | ChooseItem[]
15
15
  | ChooseContentProvider
16
- | (() => ChooseItem[] | Promise<ChooseItem[]>)
16
+ | (() => ChooseItem[] | Promise<ChooseItem[]>);
17
17
 
18
18
  export interface ChooseResult {
19
- items: ChooseItem[]
19
+ items: ChooseItem[];
20
20
  }
21
21
 
22
22
  export interface ChooseOptions {
23
- multi?: boolean
24
- title?: string
25
- prompt?: string
26
- placeholder?: string
27
- emptyMessage?: string
23
+ multi?: boolean;
24
+ title?: string;
25
+ prompt?: string;
26
+ placeholder?: string;
27
+ emptyMessage?: string;
28
28
  }
@@ -1,10 +1,12 @@
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"
1
+ import { useEffect, useRef } from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { ActionDefinition } from "@tooee/commands";
4
+ import { useOverlay } from "@tooee/overlays";
5
+ import type { OverlayHandle } from "@tooee/overlays";
6
+ import { ChooseOverlay } from "./choose-overlay.js";
7
+ import type { ChooseListProps } from "./choose-list.js";
8
+ import type { ChoosePanelProps } from "./choose-panel.js";
9
+ import type { ChooseItem, ChooseSource } from "./types.js";
8
10
 
9
11
  /**
10
12
  * Per-open unique overlay id. A module-level sequence guarantees that
@@ -12,30 +14,30 @@ import type { ChooseItem, ChooseSource } from "./types.js"
12
14
  * same process — never share an overlay id, so one dialog can never displace
13
15
  * another through same-id replacement.
14
16
  */
15
- let chooseDialogSequence = 0
17
+ let chooseDialogSequence = 0;
16
18
 
17
19
  /** 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[]>)
20
+ export type ChooseDialogItems<T> = readonly T[] | (() => readonly T[] | Promise<readonly T[]>);
19
21
 
20
22
  export interface ChooseDialogOptionsBase<T> {
21
- items: ChooseDialogItems<T>
23
+ items: ChooseDialogItems<T>;
22
24
  /** Multi-select: Tab toggles, Enter submits the selection (default false). */
23
- multi?: boolean
24
- prompt?: string
25
- placeholder?: string
26
- emptyMessage?: string
25
+ multi?: boolean;
26
+ prompt?: string;
27
+ placeholder?: string;
28
+ emptyMessage?: string;
27
29
  /**
28
30
  * Extra commands registered on the dialog's own command surface. Handlers
29
31
  * may open further dialogs; the nested dialog suspends this one until it
30
32
  * settles.
31
33
  */
32
- commands?: ActionDefinition[]
34
+ commands?: ActionDefinition[];
33
35
  /** Chrome pass-throughs (see ChooseOverlay). */
34
- renderItem?: ChooseListProps["renderItem"]
35
- hints?: ChoosePanelProps["hints"]
36
- statusRight?: ReactNode
37
- footer?: ReactNode
38
- inset?: ChoosePanelProps["inset"]
36
+ renderItem?: ChooseListProps["renderItem"];
37
+ hints?: ChoosePanelProps["hints"];
38
+ statusRight?: ReactNode;
39
+ footer?: ReactNode;
40
+ inset?: ChoosePanelProps["inset"];
39
41
  }
40
42
 
41
43
  /**
@@ -45,9 +47,9 @@ export interface ChooseDialogOptionsBase<T> {
45
47
  */
46
48
  export type ChooseDialogToItem<T> = [T] extends [ChooseItem]
47
49
  ? { toItem?: (item: T) => ChooseItem }
48
- : { toItem: (item: T) => ChooseItem }
50
+ : { toItem: (item: T) => ChooseItem };
49
51
 
50
- export type ChooseDialogOptions<T> = ChooseDialogOptionsBase<T> & ChooseDialogToItem<T>
52
+ export type ChooseDialogOptions<T> = ChooseDialogOptionsBase<T> & ChooseDialogToItem<T>;
51
53
 
52
54
  export interface ChooseDialogHandle<T> {
53
55
  /**
@@ -57,8 +59,10 @@ export interface ChooseDialogHandle<T> {
57
59
  * resolves the confirmed item; multi-select resolves the toggled items
58
60
  * (falling back to the active item when nothing is toggled).
59
61
  */
60
- open(options: ChooseDialogOptions<T> & { multi: true }): Promise<T[] | null>
61
- open(options: ChooseDialogOptions<T> & { multi?: false }): Promise<T | null>
62
+ open: {
63
+ (options: ChooseDialogOptions<T> & { multi: true }): Promise<T[] | null>;
64
+ (options: ChooseDialogOptions<T> & { multi?: false }): Promise<T | null>;
65
+ };
62
66
  }
63
67
 
64
68
  /**
@@ -75,136 +79,161 @@ export interface ChooseDialogHandle<T> {
75
79
  * (`AppLayout` does this automatically; custom hosts render
76
80
  * `useCurrentOverlay()`).
77
81
  */
78
- export function useChooseDialog<T>(): ChooseDialogHandle<T> {
79
- const overlay = useOverlay()
80
- const overlayRef = useRef(overlay)
81
- overlayRef.current = overlay
82
+ export const useChooseDialog = function useChooseDialog<T>(): ChooseDialogHandle<T> {
83
+ const overlay = useOverlay();
84
+ const overlayRef = useRef(overlay);
85
+ overlayRef.current = overlay;
82
86
 
83
87
  // Open dialogs by overlay id, so unmounting the owner closes (and thereby
84
88
  // settles) every dialog it opened.
85
- const openHandlesRef = useRef(new Map<string, OverlayHandle<undefined>>())
86
- const unmountedRef = useRef(false)
89
+ const openHandlesRef = useRef(new Map<string, OverlayHandle<undefined>>());
90
+ const unmountedRef = useRef(false);
87
91
 
88
92
  useEffect(() => {
89
- unmountedRef.current = false
90
- const handles = openHandlesRef.current
93
+ unmountedRef.current = false;
94
+ const handles = openHandlesRef.current;
91
95
  return () => {
92
- unmountedRef.current = true
96
+ unmountedRef.current = true;
93
97
  // Map iteration tolerates deletion: settle() removes entries as each
94
98
  // close lands.
95
- for (const handle of handles.values()) handle.close("unmounted")
96
- handles.clear()
97
- }
98
- }, [])
99
+ for (const handle of handles.values()) {
100
+ handle.close("unmounted");
101
+ }
102
+ handles.clear();
103
+ };
104
+ }, []);
99
105
 
100
106
  // Stable identity so command handlers and effects can capture the handle.
101
- const handleRef = useRef<ChooseDialogHandle<T> | null>(null)
107
+ const handleRef = useRef<ChooseDialogHandle<T> | null>(null);
102
108
  if (handleRef.current === null) {
103
- const open = (
109
+ const open = async (
104
110
  options: ChooseDialogOptionsBase<T> & { toItem?: (item: T) => ChooseItem },
105
111
  ): Promise<T | T[] | null> => {
106
- return new Promise<T | T[] | null>((resolvePromise) => {
107
- if (unmountedRef.current) {
108
- resolvePromise(null)
109
- return
112
+ const { promise, resolve } = Promise.withResolvers<T | T[] | null>();
113
+ if (unmountedRef.current) {
114
+ resolve(null);
115
+ return null;
116
+ }
117
+
118
+ chooseDialogSequence += 1;
119
+ const id = `choose-dialog-${chooseDialogSequence}`;
120
+ let settled = false;
121
+ const settle = (value: T | T[] | null) => {
122
+ if (settled) {
123
+ return;
110
124
  }
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)
125
+ settled = true;
126
+ openHandlesRef.current.delete(id);
127
+ resolve(value);
128
+ };
129
+
130
+ // Displayed rows map back to typed items by identity: every mapped
131
+ // row is a fresh object (spread copy), so duplicates in `items` and
132
+ // `toItem` results that share references stay unambiguous.
133
+ // Safe default: `toItem` may only be omitted when T is a ChooseItem
134
+ // (enforced by ChooseDialogToItem).
135
+ // Deferred(lint-sweep): replace the conditional generic API boundary with schema-safe overload implementation typing.
136
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- default mapper is enforced by ChooseDialogToItem's public type
137
+ const toItem = options.toItem ?? ((item: T) => item as unknown as ChooseItem);
138
+ const rowToValue = new Map<ChooseItem, T>();
139
+ const mapValues = (values: readonly T[]): ChooseItem[] => {
140
+ rowToValue.clear();
141
+ return values.map((value) => {
142
+ const row = { ...toItem(value) };
143
+ rowToValue.set(row, value);
144
+ return row;
145
+ });
146
+ };
147
+
148
+ // Created once per open() so the source identity is stable across
149
+ // overlay re-renders (a fresh source each render would reload forever).
150
+ const { items } = options;
151
+ const source: ChooseSource = Array.isArray(items)
152
+ ? mapValues(items as readonly T[])
153
+ : (): ChooseItem[] | Promise<ChooseItem[]> => {
154
+ // Deferred(lint-sweep): replace the conditional generic API boundary with schema-safe overload implementation typing.
155
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- implementation narrows the public source union after the branch
156
+ const loaded = (items as () => readonly T[] | Promise<readonly T[]>)();
157
+ if (loaded instanceof Promise) {
158
+ return (async () => mapValues(await loaded))();
145
159
  }
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),
160
+ return mapValues(loaded);
161
+ };
162
+
163
+ const multi = options.multi === true;
164
+ const shared = {
165
+ commands: options.commands,
166
+ emptyMessage: options.emptyMessage,
167
+ footer: options.footer,
168
+ hints: options.hints,
169
+ inset: options.inset,
170
+ items: source,
171
+ placeholder: options.placeholder,
172
+ prompt: options.prompt,
173
+ renderItem: options.renderItem,
174
+ statusRight: options.statusRight,
175
+ };
176
+
177
+ const handle = overlayRef.current.open(
178
+ id,
179
+ (): ReactNode =>
180
+ multi ? (
181
+ <ChooseOverlay
182
+ {...shared}
183
+ multi
184
+ onSubmit={(result) => {
185
+ if (settled) {
186
+ return;
187
+ }
188
+ const values = result.items.flatMap((row) => {
189
+ const value = rowToValue.get(row);
190
+ return value === undefined ? [] : [value];
191
+ });
192
+ settle(values);
193
+ handle.close("close");
194
+ }}
195
+ onCancel={() => {
196
+ handle.close("escape");
197
+ }}
198
+ />
199
+ ) : (
200
+ <ChooseOverlay
201
+ {...shared}
202
+ onSelect={(row) => {
203
+ if (settled) {
204
+ return;
205
+ }
206
+ const value = rowToValue.get(row);
207
+ settle(value ?? null);
208
+ handle.close("close");
209
+ }}
210
+ onCancel={() => {
211
+ handle.close("escape");
212
+ }}
213
+ />
214
+ ),
215
+ undefined,
216
+ {
217
+ // Single settlement funnel: every close path (cancel, escape,
218
+ // replacement, unmount, external closeTop) lands here. Select and
219
+ // submit settle first, making this a no-op.
220
+ onClose: () => {
221
+ settle(null);
200
222
  },
201
- )
202
- openHandlesRef.current.set(id, handle)
203
- })
204
- }
205
-
206
- handleRef.current = { open: open as ChooseDialogHandle<T>["open"] }
223
+ ownCommands: true,
224
+ role: "modal",
225
+ surfaceMode: "insert",
226
+ },
227
+ );
228
+ openHandlesRef.current.set(id, handle);
229
+ const result = await promise;
230
+ return result;
231
+ };
232
+
233
+ // Deferred(lint-sweep): replace the conditional generic API boundary with schema-safe overload implementation typing.
234
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- implementation preserves the public overloaded handle contract
235
+ handleRef.current = { open: open as ChooseDialogHandle<T>["open"] };
207
236
  }
208
237
 
209
- return handleRef.current
210
- }
238
+ return handleRef.current;
239
+ };