@tooee/choose 0.7.3 → 0.8.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.
- package/dist/choose-filter.d.ts.map +1 -1
- package/dist/choose-filter.js +11 -2
- package/dist/choose-filter.js.map +1 -1
- package/dist/choose-list.d.ts.map +1 -1
- package/dist/choose-list.js +10 -1
- package/dist/choose-list.js.map +1 -1
- package/dist/choose-panel.d.ts +7 -9
- package/dist/choose-panel.d.ts.map +1 -1
- package/dist/choose-panel.js +5 -8
- package/dist/choose-panel.js.map +1 -1
- package/dist/choose-store.d.ts +57 -0
- package/dist/choose-store.d.ts.map +1 -0
- package/dist/choose-store.js +102 -0
- package/dist/choose-store.js.map +1 -0
- package/dist/choose.d.ts +5 -5
- package/dist/choose.d.ts.map +1 -1
- package/dist/choose.js +26 -12
- package/dist/choose.js.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +2 -14
- package/dist/fuzzy.js.map +1 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +17 -5
- package/dist/source.js.map +1 -1
- package/dist/use-choose-dialog.d.ts.map +1 -1
- package/dist/use-choose-dialog.js +22 -84
- package/dist/use-choose-dialog.js.map +1 -1
- package/dist/use-choose.d.ts +1 -1
- package/dist/use-choose.d.ts.map +1 -1
- package/dist/use-choose.js +236 -264
- package/dist/use-choose.js.map +1 -1
- package/package.json +10 -7
- package/src/choose-filter.tsx +10 -2
- package/src/choose-list.tsx +9 -1
- package/src/choose-panel.tsx +18 -71
- package/src/choose-store.ts +146 -0
- package/src/choose.tsx +43 -27
- package/src/fuzzy.ts +2 -17
- package/src/launch.tsx +7 -2
- package/src/source.ts +24 -6
- package/src/use-choose-dialog.tsx +30 -98
- package/src/use-choose.ts +273 -278
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import type { ActionDefinition } from "@tooee/commands";
|
|
4
|
-
import {
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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 =
|
|
152
|
-
? mapValues(items
|
|
109
|
+
const source: ChooseSource = isDialogItemArray(items)
|
|
110
|
+
? mapValues(items)
|
|
153
111
|
: (): ChooseItem[] | Promise<ChooseItem[]> => {
|
|
154
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return handleRef.current;
|
|
169
|
+
return { open: open as ChooseDialogHandle<T>["open"] };
|
|
170
|
+
}, [dialog]);
|
|
239
171
|
};
|