@tooee/choose 0.7.3 → 0.9.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 (62) hide show
  1. package/dist/choose-filter.d.ts +2 -11
  2. package/dist/choose-filter.d.ts.map +1 -1
  3. package/dist/choose-filter.js +1 -10
  4. package/dist/choose-filter.js.map +1 -1
  5. package/dist/choose-highlighted-text.d.ts +2 -8
  6. package/dist/choose-highlighted-text.d.ts.map +1 -1
  7. package/dist/choose-highlighted-text.js +1 -31
  8. package/dist/choose-highlighted-text.js.map +1 -1
  9. package/dist/choose-list.d.ts +2 -27
  10. package/dist/choose-list.d.ts.map +1 -1
  11. package/dist/choose-list.js +1 -56
  12. package/dist/choose-list.js.map +1 -1
  13. package/dist/choose-panel.d.ts +7 -9
  14. package/dist/choose-panel.d.ts.map +1 -1
  15. package/dist/choose-panel.js +5 -8
  16. package/dist/choose-panel.js.map +1 -1
  17. package/dist/choose-store.d.ts +3 -0
  18. package/dist/choose-store.d.ts.map +1 -0
  19. package/dist/choose-store.js +2 -0
  20. package/dist/choose-store.js.map +1 -0
  21. package/dist/choose.d.ts +3 -7
  22. package/dist/choose.d.ts.map +1 -1
  23. package/dist/choose.js +10 -12
  24. package/dist/choose.js.map +1 -1
  25. package/dist/choose.public-contract.typecheck.d.ts +11 -0
  26. package/dist/choose.public-contract.typecheck.d.ts.map +1 -0
  27. package/dist/choose.public-contract.typecheck.js +2 -0
  28. package/dist/choose.public-contract.typecheck.js.map +1 -0
  29. package/dist/fuzzy.d.ts +2 -8
  30. package/dist/fuzzy.d.ts.map +1 -1
  31. package/dist/fuzzy.js +1 -16
  32. package/dist/fuzzy.js.map +1 -1
  33. package/dist/launch.d.ts.map +1 -1
  34. package/dist/launch.js +2 -1
  35. package/dist/launch.js.map +1 -1
  36. package/dist/source.d.ts +1 -4
  37. package/dist/source.d.ts.map +1 -1
  38. package/dist/source.js +1 -13
  39. package/dist/source.js.map +1 -1
  40. package/dist/types.d.ts +1 -21
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/use-choose-dialog.d.ts.map +1 -1
  43. package/dist/use-choose-dialog.js +22 -84
  44. package/dist/use-choose-dialog.js.map +1 -1
  45. package/dist/use-choose.d.ts +2 -79
  46. package/dist/use-choose.d.ts.map +1 -1
  47. package/dist/use-choose.js +1 -402
  48. package/dist/use-choose.js.map +1 -1
  49. package/package.json +8 -7
  50. package/src/choose-filter.tsx +2 -47
  51. package/src/choose-highlighted-text.tsx +2 -49
  52. package/src/choose-list.tsx +2 -148
  53. package/src/choose-panel.tsx +18 -71
  54. package/src/choose-store.ts +12 -0
  55. package/src/choose.public-contract.typecheck.ts +12 -0
  56. package/src/choose.tsx +20 -24
  57. package/src/fuzzy.ts +2 -28
  58. package/src/launch.tsx +8 -2
  59. package/src/source.ts +1 -18
  60. package/src/types.ts +7 -28
  61. package/src/use-choose-dialog.tsx +30 -98
  62. package/src/use-choose.ts +9 -526
@@ -1,21 +1,12 @@
1
- import { useEffect, useRef } from "react";
1
+ import { useMemo } from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import type { ActionDefinition } from "@tooee/commands";
4
- import { useOverlay } from "@tooee/overlays";
5
- import type { OverlayHandle } from "@tooee/overlays";
4
+ import { useOverlayDialog } from "@tooee/overlays";
6
5
  import { ChooseOverlay } from "./choose-overlay.js";
7
6
  import type { ChooseListProps } from "./choose-list.js";
8
7
  import type { ChoosePanelProps } from "./choose-panel.js";
9
8
  import type { ChooseItem, ChooseSource } from "./types.js";
10
9
 
11
- /**
12
- * Per-open unique overlay id. A module-level sequence guarantees that
13
- * concurrent dialogs — including dialogs opened from separate providers in the
14
- * same process — never share an overlay id, so one dialog can never displace
15
- * another through same-id replacement.
16
- */
17
- let chooseDialogSequence = 0;
18
-
19
10
  /** Typed item source: a fixed list, or a loader invoked when the dialog opens. */
20
11
  export type ChooseDialogItems<T> = readonly T[] | (() => readonly T[] | Promise<readonly T[]>);
21
12
 
@@ -65,6 +56,19 @@ export interface ChooseDialogHandle<T> {
65
56
  };
66
57
  }
67
58
 
59
+ const isDialogItemArray = function isDialogItemArray<T>(
60
+ items: ChooseDialogItems<T>,
61
+ ): items is readonly T[] {
62
+ return Array.isArray(items);
63
+ };
64
+
65
+ // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- T carries the conditional public option evidence for an omitted mapper
66
+ const defaultDialogItem = function defaultDialogItem<T>(item: T): ChooseItem {
67
+ // SAFETY: ChooseDialogToItem permits an omitted mapper only when T extends ChooseItem.
68
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- conditional public options enforce the ChooseItem arm
69
+ return item as ChooseItem;
70
+ };
71
+
68
72
  /**
69
73
  * Promise-based modal chooser dialog on the overlay stack.
70
74
  *
@@ -80,61 +84,15 @@ export interface ChooseDialogHandle<T> {
80
84
  * `useCurrentOverlay()`).
81
85
  */
82
86
  export const useChooseDialog = function useChooseDialog<T>(): ChooseDialogHandle<T> {
83
- const overlay = useOverlay();
84
- const overlayRef = useRef(overlay);
85
- overlayRef.current = overlay;
86
-
87
- // Open dialogs by overlay id, so unmounting the owner closes (and thereby
88
- // settles) every dialog it opened.
89
- const openHandlesRef = useRef(new Map<string, OverlayHandle<undefined>>());
90
- const unmountedRef = useRef(false);
91
-
92
- useEffect(() => {
93
- unmountedRef.current = false;
94
- const handles = openHandlesRef.current;
95
- return () => {
96
- unmountedRef.current = true;
97
- // Map iteration tolerates deletion: settle() removes entries as each
98
- // close lands.
99
- for (const handle of handles.values()) {
100
- handle.close("unmounted");
101
- }
102
- handles.clear();
103
- };
104
- }, []);
105
-
106
- // Stable identity so command handlers and effects can capture the handle.
107
- const handleRef = useRef<ChooseDialogHandle<T> | null>(null);
108
- if (handleRef.current === null) {
87
+ const dialog = useOverlayDialog<T | T[]>();
88
+ return useMemo<ChooseDialogHandle<T>>(() => {
109
89
  const open = async (
110
90
  options: ChooseDialogOptionsBase<T> & { toItem?: (item: T) => ChooseItem },
111
91
  ): Promise<T | T[] | null> => {
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;
124
- }
125
- settled = true;
126
- openHandlesRef.current.delete(id);
127
- resolve(value);
128
- };
129
-
130
92
  // Displayed rows map back to typed items by identity: every mapped
131
93
  // row is a fresh object (spread copy), so duplicates in `items` and
132
94
  // `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);
95
+ const toItem = options.toItem ?? defaultDialogItem;
138
96
  const rowToValue = new Map<ChooseItem, T>();
139
97
  const mapValues = (values: readonly T[]): ChooseItem[] => {
140
98
  rowToValue.clear();
@@ -148,12 +106,10 @@ export const useChooseDialog = function useChooseDialog<T>(): ChooseDialogHandle
148
106
  // Created once per open() so the source identity is stable across
149
107
  // overlay re-renders (a fresh source each render would reload forever).
150
108
  const { items } = options;
151
- const source: ChooseSource = Array.isArray(items)
152
- ? mapValues(items as readonly T[])
109
+ const source: ChooseSource = isDialogItemArray(items)
110
+ ? mapValues(items)
153
111
  : (): 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[]>)();
112
+ const loaded = items();
157
113
  if (loaded instanceof Promise) {
158
114
  return (async () => mapValues(await loaded))();
159
115
  }
@@ -174,66 +130,42 @@ export const useChooseDialog = function useChooseDialog<T>(): ChooseDialogHandle
174
130
  statusRight: options.statusRight,
175
131
  };
176
132
 
177
- const handle = overlayRef.current.open(
178
- id,
179
- (): ReactNode =>
133
+ return await dialog.open(
134
+ "choose-dialog",
135
+ (settle): ReactNode =>
180
136
  multi ? (
181
137
  <ChooseOverlay
182
138
  {...shared}
183
139
  multi
184
140
  onSubmit={(result) => {
185
- if (settled) {
186
- return;
187
- }
188
141
  const values = result.items.flatMap((row) => {
189
142
  const value = rowToValue.get(row);
190
143
  return value === undefined ? [] : [value];
191
144
  });
192
145
  settle(values);
193
- handle.close("close");
194
146
  }}
195
147
  onCancel={() => {
196
- handle.close("escape");
148
+ settle(null);
197
149
  }}
198
150
  />
199
151
  ) : (
200
152
  <ChooseOverlay
201
153
  {...shared}
202
154
  onSelect={(row) => {
203
- if (settled) {
204
- return;
205
- }
206
155
  const value = rowToValue.get(row);
207
156
  settle(value ?? null);
208
- handle.close("close");
209
157
  }}
210
158
  onCancel={() => {
211
- handle.close("escape");
159
+ settle(null);
212
160
  }}
213
161
  />
214
162
  ),
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);
222
- },
223
- ownCommands: true,
224
- role: "modal",
225
- surfaceMode: "insert",
226
- },
227
163
  );
228
- openHandlesRef.current.set(id, handle);
229
- const result = await promise;
230
- return result;
231
164
  };
232
165
 
233
- // Deferred(lint-sweep): replace the conditional generic API boundary with schema-safe overload implementation typing.
166
+ // SAFETY: The implementation returns the single or multi result selected by the same options.multi
167
+ // discriminator as the public overloads.
234
168
  // 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"] };
236
- }
237
-
238
- return handleRef.current;
169
+ return { open: open as ChooseDialogHandle<T>["open"] };
170
+ }, [dialog]);
239
171
  };