@tooee/choose 0.8.0 → 0.9.1

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 (53) 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 -19
  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 -65
  12. package/dist/choose-list.js.map +1 -1
  13. package/dist/choose-store.d.ts +2 -56
  14. package/dist/choose-store.d.ts.map +1 -1
  15. package/dist/choose-store.js +1 -101
  16. package/dist/choose-store.js.map +1 -1
  17. package/dist/choose.d.ts +1 -5
  18. package/dist/choose.d.ts.map +1 -1
  19. package/dist/choose.js +3 -19
  20. package/dist/choose.js.map +1 -1
  21. package/dist/choose.public-contract.typecheck.d.ts +11 -0
  22. package/dist/choose.public-contract.typecheck.d.ts.map +1 -0
  23. package/dist/choose.public-contract.typecheck.js +2 -0
  24. package/dist/choose.public-contract.typecheck.js.map +1 -0
  25. package/dist/fuzzy.d.ts +2 -8
  26. package/dist/fuzzy.d.ts.map +1 -1
  27. package/dist/fuzzy.js +1 -4
  28. package/dist/fuzzy.js.map +1 -1
  29. package/dist/launch.d.ts.map +1 -1
  30. package/dist/launch.js +1 -0
  31. package/dist/launch.js.map +1 -1
  32. package/dist/source.d.ts +1 -4
  33. package/dist/source.d.ts.map +1 -1
  34. package/dist/source.js +1 -25
  35. package/dist/source.js.map +1 -1
  36. package/dist/types.d.ts +1 -21
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/use-choose.d.ts +2 -79
  39. package/dist/use-choose.d.ts.map +1 -1
  40. package/dist/use-choose.js +1 -374
  41. package/dist/use-choose.js.map +1 -1
  42. package/package.json +7 -9
  43. package/src/choose-filter.tsx +2 -55
  44. package/src/choose-highlighted-text.tsx +2 -49
  45. package/src/choose-list.tsx +2 -156
  46. package/src/choose-store.ts +12 -146
  47. package/src/choose.public-contract.typecheck.ts +12 -0
  48. package/src/choose.tsx +15 -35
  49. package/src/fuzzy.ts +2 -13
  50. package/src/launch.tsx +1 -0
  51. package/src/source.ts +1 -36
  52. package/src/types.ts +7 -28
  53. package/src/use-choose.ts +9 -521
package/src/use-choose.ts CHANGED
@@ -1,521 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useRef } from "react";
2
- import type { InputRenderable, KeyEvent } from "@opentui/core";
3
- import { useSelector } from "@xstate/store-react";
4
- import {
5
- useActiveCommandSurface,
6
- useActions,
7
- useCommandSurfaceId,
8
- useLatest,
9
- useLazyRef,
10
- useMode,
11
- useProvideCommandContext,
12
- useSetMode,
13
- } from "@tooee/commands";
14
- import type { ActionDefinition, CommandContext, Mode } from "@tooee/commands";
15
- import {
16
- createChooseStore,
17
- selectActiveIndex,
18
- selectError,
19
- selectFilterQuery,
20
- selectItems,
21
- selectLoading,
22
- selectMatches,
23
- selectReloadRevision,
24
- selectSelectedOriginalIndices,
25
- } from "./choose-store.js";
26
- import type { FuzzyMatch } from "./fuzzy.js";
27
- import { chooseSourceError, loadChooseSource } from "./source.js";
28
- import type { ChooseItem, ChooseResult, ChooseSource } from "./types.js";
29
-
30
- declare module "@tooee/commands" {
31
- interface CommandContext {
32
- /** Contributed by chooser primitives: current filtering and selection state. */
33
- choose?: {
34
- activeItem: ChooseItem | undefined;
35
- selectedItems: ChooseItem[];
36
- filterQuery: string;
37
- };
38
- }
39
- }
40
-
41
- export type ChooseCommandGroup = "navigation" | "mode" | "submit" | "cancel" | "multi-select";
42
-
43
- export interface UseChooseOptions {
44
- source: ChooseSource;
45
- multi?: boolean;
46
- initialFilter?: string;
47
- onSubmit: (result: ChooseResult) => void | Promise<void>;
48
- onCancel?: () => void;
49
- commands?: ActionDefinition[];
50
- /** Prefix for built-in command ids (default `choose`). */
51
- commandScope?: string;
52
- /** Disable a built-in command group when a host owns those keys. */
53
- disable?: ChooseCommandGroup[];
54
- /** Explicitly blur/disable mouse views while a legacy covering surface is open. */
55
- suspended?: boolean;
56
- }
57
-
58
- export interface ChooseController {
59
- getFilter: () => string;
60
- setFilter: (query: string) => void;
61
- clearFilter: () => void;
62
- moveUp: () => void;
63
- moveDown: () => void;
64
- setActiveIndex: (index: number) => void;
65
- getActiveItem: () => ChooseItem | undefined;
66
- getSelectedItems: () => ChooseItem[];
67
- toggleActive: () => void;
68
- submit: () => void;
69
- cancel: () => void;
70
- reload: () => void;
71
- readonly mode: Mode;
72
- setMode: (mode: Mode) => void;
73
- }
74
-
75
- export interface ChooseState {
76
- items: ChooseItem[];
77
- matches: FuzzyMatch[];
78
- filterQuery: string;
79
- activeIndex: number;
80
- activeItem: ChooseItem | undefined;
81
- selectedOriginalIndices: ReadonlySet<number>;
82
- selectedItems: ChooseItem[];
83
- loading: boolean;
84
- error: string | null;
85
- multi: boolean;
86
- }
87
-
88
- export interface ChooseViewModel {
89
- filterRef: { current: InputRenderable | null };
90
- mode: Mode;
91
- /** True while another modal surface (or an explicit host guard) owns interaction. */
92
- suspended: boolean;
93
- filterFocused: boolean;
94
- onFilterInput: (query: string) => void;
95
- /** Input-level Tab bridge; OpenTUI consumes Tab before global dispatch. */
96
- onFilterKeyDown: (event: KeyEvent) => void;
97
- }
98
-
99
- export interface UseChooseResult {
100
- controller: ChooseController;
101
- state: ChooseState;
102
- view: ChooseViewModel;
103
- }
104
-
105
- interface ChooseCommandContextContribution {
106
- choose: CommandContext["choose"];
107
- exit?: () => void;
108
- }
109
-
110
- interface ChooseKeymapDefinition extends Omit<ActionDefinition, "group" | "when"> {
111
- group: () => ChooseCommandGroup;
112
- when?: () => boolean;
113
- }
114
-
115
- /**
116
- * Shared headless chooser: source lifecycle, fuzzy matches, selection,
117
- * command context, controller, and all built-in keyboard commands.
118
- */
119
- export const useChoose = function useChoose(options: UseChooseOptions): UseChooseResult {
120
- const { source, multi = false, initialFilter = "", commandScope = "choose" } = options;
121
- const initialItems = Array.isArray(source) ? source : [];
122
- const store = useLazyRef(() =>
123
- createChooseStore({
124
- initialFilter,
125
- items: initialItems,
126
- loading: !Array.isArray(source),
127
- }),
128
- ).current;
129
- const filterRef = useRef<InputRenderable>(null);
130
- const initialArraySourceRef = useLazyRef<ChooseItem[] | null>(() =>
131
- Array.isArray(source) ? source : null,
132
- );
133
- const didHandleInitialSourceRef = useRef(false);
134
-
135
- const items = useSelector(store, (snapshot) => selectItems(snapshot.context));
136
- const matches = useSelector(store, (snapshot) => selectMatches(snapshot.context));
137
- const filterQuery = useSelector(store, (snapshot) => selectFilterQuery(snapshot.context));
138
- const activeIndex = useSelector(store, (snapshot) => selectActiveIndex(snapshot.context));
139
- const selectedOriginalIndices = useSelector(store, (snapshot) =>
140
- selectSelectedOriginalIndices(snapshot.context),
141
- );
142
- const loading = useSelector(store, (snapshot) => selectLoading(snapshot.context));
143
- const error = useSelector(store, (snapshot) => selectError(snapshot.context));
144
- const reloadRevision = useSelector(store, (snapshot) => selectReloadRevision(snapshot.context));
145
- const activeItem = matches[activeIndex]?.item;
146
- const selectedItems = useMemo(() => {
147
- if (!multi) {
148
- return activeItem === undefined ? [] : [activeItem];
149
- }
150
- const selected = [...selectedOriginalIndices].flatMap((index) => {
151
- const item = items[index];
152
- return item === undefined ? [] : [item];
153
- });
154
- if (selected.length > 0) {
155
- return selected;
156
- }
157
- return activeItem === undefined ? [] : [activeItem];
158
- }, [multi, activeItem, selectedOriginalIndices, items]);
159
-
160
- const mode = useMode();
161
- const setMode = useSetMode();
162
- const optionsRef = useLatest(options);
163
- const multiRef = useLatest(multi);
164
- const modeRef = useLatest(mode);
165
-
166
- useEffect(() => {
167
- let active = true;
168
- const deactivate = () => {
169
- active = false;
170
- };
171
-
172
- // Direct arrays seed the store before the first render. Avoid replacing
173
- // state after mount, which could overwrite immediate controller calls.
174
- if (
175
- !didHandleInitialSourceRef.current &&
176
- Array.isArray(source) &&
177
- source === initialArraySourceRef.current
178
- ) {
179
- didHandleInitialSourceRef.current = true;
180
- return deactivate;
181
- }
182
- didHandleInitialSourceRef.current = true;
183
-
184
- store.trigger.requestStarted({});
185
- const { requestId } = store.getSnapshot().context;
186
- let result: ChooseItem[] | Promise<ChooseItem[]>;
187
- try {
188
- result = loadChooseSource(source);
189
- } catch (loadError) {
190
- store.trigger.loadFailed({ error: chooseSourceError(loadError), requestId });
191
- return deactivate;
192
- }
193
-
194
- if (result instanceof Promise) {
195
- store.trigger.loadingStarted({ requestId });
196
- void (async () => {
197
- try {
198
- const loaded = await result;
199
- if (active) {
200
- store.trigger.loadSucceeded({ items: loaded, requestId });
201
- }
202
- } catch (loadError) {
203
- if (active) {
204
- store.trigger.loadFailed({ error: chooseSourceError(loadError), requestId });
205
- }
206
- }
207
- })();
208
- return deactivate;
209
- }
210
-
211
- store.trigger.loadSucceeded({ items: result, requestId });
212
- return deactivate;
213
- }, [source, reloadRevision, store, initialArraySourceRef]);
214
-
215
- const setFilter = useCallback(
216
- (query: string) => {
217
- store.trigger.filterChanged({ query });
218
- },
219
- [store],
220
- );
221
- const updateActiveIndex = useCallback(
222
- (index: number) => {
223
- store.trigger.activeIndexSet({ index });
224
- },
225
- [store],
226
- );
227
- const moveUp = useCallback(() => {
228
- store.trigger.moved({ delta: -1 });
229
- }, [store]);
230
- const moveDown = useCallback(() => {
231
- store.trigger.moved({ delta: 1 });
232
- }, [store]);
233
- const getActiveItem = useCallback(() => {
234
- const { context } = store.getSnapshot();
235
- return context.matches[context.activeIndex]?.item;
236
- }, [store]);
237
- const getSelectedItems = useCallback((): ChooseItem[] => {
238
- const { context } = store.getSnapshot();
239
- const active = context.matches[context.activeIndex]?.item;
240
- if (!multiRef.current) {
241
- return active === undefined ? [] : [active];
242
- }
243
- const selected = [...context.selectedOriginalIndices].flatMap((index) => {
244
- const item = context.items[index];
245
- return item === undefined ? [] : [item];
246
- });
247
- if (selected.length > 0) {
248
- return selected;
249
- }
250
- return active === undefined ? [] : [active];
251
- }, [multiRef, store]);
252
- const toggleActive = useCallback(() => {
253
- if (multiRef.current) {
254
- store.trigger.activeToggled({});
255
- }
256
- }, [multiRef, store]);
257
- const submit = useCallback(() => {
258
- void optionsRef.current.onSubmit({ items: getSelectedItems() });
259
- }, [getSelectedItems, optionsRef]);
260
- const cancel = useCallback(() => optionsRef.current.onCancel?.(), [optionsRef]);
261
- const reload = useCallback(() => {
262
- store.trigger.reloadRequested({});
263
- }, [store]);
264
- const setModeExternal = useCallback(
265
- (nextMode: Mode) => {
266
- setMode(nextMode);
267
- },
268
- [setMode],
269
- );
270
-
271
- useProvideCommandContext(() => {
272
- const context: ChooseCommandContextContribution = {
273
- choose: {
274
- activeItem: getActiveItem(),
275
- filterQuery: store.getSnapshot().context.filterQuery,
276
- selectedItems: getSelectedItems(),
277
- },
278
- };
279
- if (optionsRef.current.onCancel !== undefined) {
280
- context.exit = cancel;
281
- }
282
- return context;
283
- });
284
-
285
- useActions(options.commands);
286
-
287
- const enabled = useCallback(
288
- (group: ChooseCommandGroup) => !(optionsRef.current.disable?.includes(group) ?? false),
289
- [optionsRef],
290
- );
291
- const builtInActions = useMemo<ActionDefinition[]>(() => {
292
- const definitions: ChooseKeymapDefinition[] = [
293
- {
294
- group: () => (modeRef.current === "insert" ? "mode" : "cancel"),
295
- handler: () => {
296
- if (modeRef.current === "insert") {
297
- setMode("cursor");
298
- } else {
299
- cancel();
300
- }
301
- },
302
- hidden: true,
303
- hotkey: "Escape",
304
- id: `${commandScope}:escape`,
305
- modes: ["insert", "cursor"],
306
- title: "Back / cancel",
307
- when: () =>
308
- modeRef.current === "insert" ? true : optionsRef.current.onCancel !== undefined,
309
- },
310
- {
311
- group: () => "cancel",
312
- handler: cancel,
313
- hidden: true,
314
- hotkey: "q",
315
- id: `${commandScope}:cancel`,
316
- modes: ["cursor"],
317
- title: "Cancel",
318
- when: () => optionsRef.current.onCancel !== undefined,
319
- },
320
- {
321
- group: () => "mode",
322
- handler: () => {
323
- setMode("insert");
324
- },
325
- hidden: true,
326
- hotkey: "i",
327
- id: `${commandScope}:insert-mode-i`,
328
- modes: ["cursor"],
329
- title: "Insert mode",
330
- },
331
- {
332
- group: () => "mode",
333
- handler: () => {
334
- setMode("insert");
335
- },
336
- hidden: true,
337
- hotkey: "a",
338
- id: `${commandScope}:insert-mode-a`,
339
- modes: ["cursor"],
340
- title: "Insert mode",
341
- },
342
- {
343
- group: () => "navigation",
344
- handler: moveDown,
345
- hidden: true,
346
- hotkey: "j",
347
- id: `${commandScope}:move-down-vim`,
348
- modes: ["cursor"],
349
- title: "Move down",
350
- },
351
- {
352
- group: () => "navigation",
353
- handler: moveUp,
354
- hidden: true,
355
- hotkey: "k",
356
- id: `${commandScope}:move-up-vim`,
357
- modes: ["cursor"],
358
- title: "Move up",
359
- },
360
- {
361
- group: () => "submit",
362
- handler: submit,
363
- hidden: true,
364
- hotkey: "Enter",
365
- id: `${commandScope}:confirm`,
366
- modes: ["insert", "cursor"],
367
- title: "Confirm",
368
- },
369
- {
370
- group: () => "navigation",
371
- handler: moveUp,
372
- hidden: true,
373
- hotkey: "up",
374
- id: `${commandScope}:move-up`,
375
- modes: ["insert", "cursor"],
376
- title: "Move up",
377
- },
378
- {
379
- group: () => "navigation",
380
- handler: moveUp,
381
- hidden: true,
382
- hotkey: "ctrl+p",
383
- id: `${commandScope}:move-up-ctrl-p`,
384
- modes: ["insert", "cursor"],
385
- title: "Move up",
386
- },
387
- {
388
- group: () => "navigation",
389
- handler: moveDown,
390
- hidden: true,
391
- hotkey: "down",
392
- id: `${commandScope}:move-down`,
393
- modes: ["insert", "cursor"],
394
- title: "Move down",
395
- },
396
- {
397
- group: () => "navigation",
398
- handler: moveDown,
399
- hidden: true,
400
- hotkey: "ctrl+n",
401
- id: `${commandScope}:move-down-ctrl-n`,
402
- modes: ["insert", "cursor"],
403
- title: "Move down",
404
- },
405
- {
406
- group: () => "multi-select",
407
- handler: () => {
408
- toggleActive();
409
- moveDown();
410
- },
411
- hidden: true,
412
- hotkey: "Tab",
413
- id: `${commandScope}:toggle-next`,
414
- modes: ["insert", "cursor"],
415
- title: "Toggle selection and move down",
416
- when: () => multiRef.current,
417
- },
418
- {
419
- group: () => "multi-select",
420
- handler: () => {
421
- toggleActive();
422
- moveUp();
423
- },
424
- hidden: true,
425
- hotkey: "shift+Tab",
426
- id: `${commandScope}:toggle-previous`,
427
- modes: ["insert", "cursor"],
428
- title: "Toggle selection and move up",
429
- when: () => multiRef.current,
430
- },
431
- ];
432
- return definitions.map(({ group, when, ...definition }) => ({
433
- ...definition,
434
- when: () => enabled(group()) && (when?.() ?? true),
435
- }));
436
- }, [
437
- cancel,
438
- commandScope,
439
- enabled,
440
- modeRef,
441
- moveDown,
442
- moveUp,
443
- multiRef,
444
- optionsRef,
445
- setMode,
446
- submit,
447
- toggleActive,
448
- ]);
449
- useActions(builtInActions);
450
-
451
- const surfaceId = useCommandSurfaceId();
452
- const activeSurface = useActiveCommandSurface();
453
- const suspended =
454
- (options.suspended ?? false) || (activeSurface !== null && activeSurface.id !== surfaceId);
455
-
456
- const onFilterKeyDown = useCallback(
457
- (event: KeyEvent) => {
458
- if (
459
- event.name !== "tab" ||
460
- !multiRef.current ||
461
- !enabled("multi-select") ||
462
- modeRef.current !== "insert"
463
- ) {
464
- return;
465
- }
466
- event.preventDefault();
467
- toggleActive();
468
- if (event.shift) {
469
- moveUp();
470
- } else {
471
- moveDown();
472
- }
473
- },
474
- [enabled, modeRef, moveDown, moveUp, multiRef, toggleActive],
475
- );
476
-
477
- const controller = useLazyRef<ChooseController>(() => ({
478
- cancel,
479
- clearFilter: () => {
480
- setFilter("");
481
- },
482
- getActiveItem,
483
- getFilter: () => store.getSnapshot().context.filterQuery,
484
- getSelectedItems,
485
- get mode() {
486
- return modeRef.current;
487
- },
488
- moveDown,
489
- moveUp,
490
- reload,
491
- setActiveIndex: updateActiveIndex,
492
- setFilter,
493
- setMode: setModeExternal,
494
- submit,
495
- toggleActive,
496
- })).current;
497
-
498
- return {
499
- controller,
500
- state: {
501
- activeIndex,
502
- activeItem,
503
- error,
504
- filterQuery,
505
- items,
506
- loading,
507
- matches,
508
- multi,
509
- selectedItems,
510
- selectedOriginalIndices,
511
- },
512
- view: {
513
- filterFocused: mode === "insert" && !suspended,
514
- filterRef,
515
- mode,
516
- onFilterInput: setFilter,
517
- onFilterKeyDown,
518
- suspended,
519
- },
520
- };
521
- };
1
+ export { useChoose } from "@tooee/renderers";
2
+ export type {
3
+ ChooseCommandGroup,
4
+ ChooseController,
5
+ ChooseState,
6
+ ChooseViewModel,
7
+ UseChooseOptions,
8
+ UseChooseResult,
9
+ } from "@tooee/renderers";