@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.
- package/README.md +2 -2
- package/dist/{ChooseFilter.d.ts → choose-filter.d.ts} +2 -2
- package/dist/choose-filter.d.ts.map +1 -0
- package/dist/choose-filter.js +11 -0
- package/dist/choose-filter.js.map +1 -0
- package/dist/choose-highlighted-text.d.ts +9 -0
- package/dist/choose-highlighted-text.d.ts.map +1 -0
- package/dist/{ChooseHighlightedText.js → choose-highlighted-text.js} +10 -8
- package/dist/choose-highlighted-text.js.map +1 -0
- package/dist/{ChooseList.d.ts → choose-list.d.ts} +3 -3
- package/dist/choose-list.d.ts.map +1 -0
- package/dist/{ChooseList.js → choose-list.js} +20 -15
- package/dist/choose-list.js.map +1 -0
- package/dist/{ChooseOverlay.d.ts → choose-overlay.d.ts} +6 -6
- package/dist/choose-overlay.d.ts.map +1 -0
- package/dist/{ChooseOverlay.js → choose-overlay.js} +15 -13
- package/dist/choose-overlay.js.map +1 -0
- package/dist/{ChoosePanel.d.ts → choose-panel.d.ts} +5 -5
- package/dist/choose-panel.d.ts.map +1 -0
- package/dist/choose-panel.js +31 -0
- package/dist/choose-panel.js.map +1 -0
- package/dist/choose.d.ts +28 -0
- package/dist/choose.d.ts.map +1 -0
- package/dist/{Choose.js → choose.js} +25 -18
- package/dist/choose.js.map +1 -0
- package/dist/default-provider.d.ts +2 -2
- package/dist/default-provider.d.ts.map +1 -1
- package/dist/default-provider.js +4 -4
- package/dist/default-provider.js.map +1 -1
- package/dist/fuzzy.d.ts +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +5 -5
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts +1 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +5 -4
- package/dist/launch.js.map +1 -1
- package/dist/source.d.ts +2 -2
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +8 -6
- package/dist/source.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/use-choose-dialog.d.ts +12 -10
- package/dist/use-choose-dialog.d.ts.map +1 -1
- package/dist/use-choose-dialog.js +98 -77
- package/dist/use-choose-dialog.js.map +1 -1
- package/dist/use-choose-dialog.typecheck.d.ts +1 -1
- package/dist/use-choose-dialog.typecheck.d.ts.map +1 -1
- package/dist/use-choose-dialog.typecheck.js +5 -5
- package/dist/use-choose-dialog.typecheck.js.map +1 -1
- package/dist/use-choose.d.ts +16 -16
- package/dist/use-choose.d.ts.map +1 -1
- package/dist/use-choose.js +176 -138
- package/dist/use-choose.js.map +1 -1
- package/package.json +7 -7
- package/src/{ChooseFilter.tsx → choose-filter.tsx} +18 -16
- package/src/choose-highlighted-text.tsx +49 -0
- package/src/{ChooseList.tsx → choose-list.tsx} +73 -60
- package/src/choose-overlay.tsx +96 -0
- package/src/{ChoosePanel.tsx → choose-panel.tsx} +41 -34
- package/src/choose.tsx +142 -0
- package/src/default-provider.ts +18 -15
- package/src/fuzzy.ts +16 -16
- package/src/index.ts +23 -23
- package/src/launch.tsx +15 -12
- package/src/source.ts +15 -9
- package/src/types.ts +12 -12
- package/src/use-choose-dialog.tsx +168 -139
- package/src/use-choose-dialog.typecheck.ts +19 -18
- package/src/use-choose.ts +349 -302
- package/dist/Choose.d.ts +0 -21
- package/dist/Choose.d.ts.map +0 -1
- package/dist/Choose.js.map +0 -1
- package/dist/ChooseFilter.d.ts.map +0 -1
- package/dist/ChooseFilter.js +0 -9
- package/dist/ChooseFilter.js.map +0 -1
- package/dist/ChooseHighlightedText.d.ts +0 -9
- package/dist/ChooseHighlightedText.d.ts.map +0 -1
- package/dist/ChooseHighlightedText.js.map +0 -1
- package/dist/ChooseList.d.ts.map +0 -1
- package/dist/ChooseList.js.map +0 -1
- package/dist/ChooseOverlay.d.ts.map +0 -1
- package/dist/ChooseOverlay.js.map +0 -1
- package/dist/ChoosePanel.d.ts.map +0 -1
- package/dist/ChoosePanel.js +0 -25
- package/dist/ChoosePanel.js.map +0 -1
- package/src/Choose.tsx +0 -126
- package/src/ChooseHighlightedText.tsx +0 -42
- 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,
|
|
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,
|
|
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 "./
|
|
2
|
-
export type { ChooseProps } from "./
|
|
3
|
-
export { ChooseOverlay } from "./
|
|
4
|
-
export type { ChooseOverlayProps } from "./
|
|
5
|
-
export { ChoosePanel, buildChooseHints } from "./
|
|
6
|
-
export type { ChoosePanelProps, ChoosePanelInset, ChoosePanelInsetValue } from "./
|
|
7
|
-
export { ChooseFilter } from "./
|
|
8
|
-
export type { ChooseFilterProps } from "./
|
|
9
|
-
export { ChooseList } from "./
|
|
10
|
-
export type { ChooseItemRenderContext, ChooseListProps } from "./
|
|
11
|
-
export { ChooseHighlightedText } from "./
|
|
12
|
-
export type { ChooseHighlightedTextProps } from "./
|
|
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 "./
|
|
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(
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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()
|
|
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
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import
|
|
7
|
-
import type {
|
|
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
|
|
61
|
-
|
|
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())
|
|
96
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
rowToValue.
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
};
|