@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,148 +1,2 @@
1
- import { useEffect, useRef } from "react";
2
- import type { ReactNode } from "react";
3
- import type { ScrollBoxRenderable } from "@opentui/core";
4
- import { useTheme } from "@tooee/themes";
5
- import { ChooseHighlightedText } from "./choose-highlighted-text.js";
6
- import type { FuzzyMatch } from "./fuzzy.js";
7
- import type { ChooseItem } from "./types.js";
8
- import type { UseChooseResult } from "./use-choose.js";
9
-
10
- export interface ChooseItemRenderContext {
11
- item: ChooseItem;
12
- match: FuzzyMatch;
13
- index: number;
14
- originalIndex: number;
15
- positions: number[];
16
- isActive: boolean;
17
- isSelected: boolean;
18
- defaultContent: ReactNode;
19
- }
20
-
21
- export interface ChooseListProps {
22
- choose: UseChooseResult;
23
- /** Fullscreen defaults to activate; picker overlays default to submit. */
24
- rowClick?: "activate" | "submit" | "none";
25
- renderItem?: (context: ChooseItemRenderContext) => ReactNode;
26
- loadingContent?: ReactNode;
27
- errorContent?: (error: string) => ReactNode;
28
- emptyContent?: ReactNode;
29
- /** Mouse guard for visible rows covered by another surface. */
30
- suspended?: boolean;
31
- }
32
-
33
- const ThemedLine = function ThemedLine({
34
- content,
35
- color,
36
- }: {
37
- content: ReactNode;
38
- color: string;
39
- }): ReactNode {
40
- return (
41
- <box height={1} style={{ paddingLeft: 2 }}>
42
- {typeof content === "string" ? <text content={content} fg={color} /> : content}
43
- </box>
44
- );
45
- };
46
-
47
- const defaultErrorContent = function defaultErrorContent(error: string): ReactNode {
48
- return `Error: ${error}`;
49
- };
50
-
51
- /** Scrollable chooser rows plus loading/error/empty presentation. */
52
- export const ChooseList = function ChooseList({
53
- choose,
54
- rowClick = "activate",
55
- renderItem,
56
- loadingContent = "Loading...",
57
- errorContent = defaultErrorContent,
58
- emptyContent,
59
- suspended,
60
- }: ChooseListProps): ReactNode {
61
- const { theme } = useTheme();
62
- const scrollRef = useRef<ScrollBoxRenderable>(null);
63
- const { state, controller } = choose;
64
- const interactionSuspended = choose.view.suspended || (suspended ?? false);
65
-
66
- useEffect(() => {
67
- const activeMatch = state.matches[state.activeIndex];
68
- if (scrollRef.current && activeMatch !== undefined) {
69
- scrollRef.current.scrollChildIntoView(`choose-item-${activeMatch.originalIndex}`);
70
- }
71
- }, [state.activeIndex, state.matches]);
72
-
73
- return (
74
- <scrollbox ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
75
- {state.loading && <ThemedLine content={loadingContent} color={theme.textMuted} />}
76
-
77
- {!state.loading && state.error !== null && state.error !== "" && (
78
- <ThemedLine content={errorContent(state.error)} color={theme.error} />
79
- )}
80
-
81
- {!state.loading &&
82
- (state.error?.length ?? 0) === 0 &&
83
- state.matches.length === 0 &&
84
- emptyContent !== null &&
85
- emptyContent !== undefined && <ThemedLine content={emptyContent} color={theme.textMuted} />}
86
-
87
- {state.matches.map((match, index): ReactNode => {
88
- const isActive = index === state.activeIndex;
89
- const isSelected = state.selectedOriginalIndices.has(match.originalIndex);
90
- const defaultContent = (
91
- <text fg={isActive ? theme.primary : theme.text} style={{ flexGrow: 1 }}>
92
- {state.multi && (
93
- <span fg={isSelected ? theme.accent : theme.textMuted}>
94
- {isSelected ? "✓ " : " "}
95
- </span>
96
- )}
97
- {(match.item.icon?.length ?? 0) > 0 && (
98
- <span fg={theme.textMuted}>{`${match.item.icon} `}</span>
99
- )}
100
- <ChooseHighlightedText
101
- text={match.item.text}
102
- positions={match.positions}
103
- highlightColor={theme.warning}
104
- />
105
- {(match.item.description?.length ?? 0) > 0 && (
106
- <span fg={theme.textMuted}>{` ${match.item.description}`}</span>
107
- )}
108
- </text>
109
- );
110
- const context: ChooseItemRenderContext = {
111
- defaultContent,
112
- index,
113
- isActive,
114
- isSelected,
115
- item: match.item,
116
- match,
117
- originalIndex: match.originalIndex,
118
- positions: match.positions,
119
- };
120
-
121
- return (
122
- <box
123
- key={match.originalIndex}
124
- id={`choose-item-${match.originalIndex}`}
125
- flexDirection="row"
126
- backgroundColor={isActive ? theme.backgroundElement : undefined}
127
- style={{ paddingLeft: 1, paddingRight: 1 }}
128
- onMouseDown={(event) => {
129
- if (interactionSuspended || event.button !== 0 || rowClick === "none") {
130
- return;
131
- }
132
- if (rowClick === "submit") {
133
- event.preventDefault();
134
- event.stopPropagation();
135
- controller.setActiveIndex(index);
136
- controller.submit();
137
- } else {
138
- controller.setActiveIndex(index);
139
- }
140
- }}
141
- >
142
- {renderItem ? renderItem(context) : defaultContent}
143
- </box>
144
- );
145
- })}
146
- </scrollbox>
147
- );
148
- };
1
+ export { ChooseList } from "@tooee/renderers";
2
+ export type { ChooseItemRenderContext, ChooseListProps } from "@tooee/renderers";
@@ -1,7 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { MouseEvent } from "@opentui/core";
2
3
  import { useMode } from "@tooee/commands";
3
4
  import type { Mode } from "@tooee/commands";
4
- import { CloseButton, useTheme } from "@tooee/themes";
5
+ import { OverlayPanel } from "@tooee/layout";
6
+ import type { PanelInset, PanelInsetValue } from "@tooee/layout";
5
7
 
6
8
  export const buildChooseHints = function buildChooseHints(
7
9
  mode: Mode,
@@ -25,14 +27,8 @@ export const buildChooseHints = function buildChooseHints(
25
27
  return options.extra ? [...base, ...options.extra] : base;
26
28
  };
27
29
 
28
- export type ChoosePanelInsetValue = number | "auto" | `${number}%`;
29
-
30
- export interface ChoosePanelInset {
31
- left?: ChoosePanelInsetValue;
32
- right?: ChoosePanelInsetValue;
33
- top?: ChoosePanelInsetValue;
34
- bottom?: ChoosePanelInsetValue;
35
- }
30
+ export type ChoosePanelInsetValue = PanelInsetValue;
31
+ export type ChoosePanelInset = PanelInset;
36
32
 
37
33
  export interface ChoosePanelProps {
38
34
  title?: ReactNode;
@@ -44,9 +40,10 @@ export interface ChoosePanelProps {
44
40
  footer?: ReactNode;
45
41
  onClose?: () => void;
46
42
  inset?: ChoosePanelInset;
43
+ onMouseDown?: (event: MouseEvent) => void;
47
44
  }
48
45
 
49
- /** Bordered chooser chrome with filter, content, hint/status, and footer slots. */
46
+ /** Chooser-specific overlay panel with mode-aware default hints. */
50
47
  export const ChoosePanel = function ChoosePanel({
51
48
  title,
52
49
  filter,
@@ -57,74 +54,24 @@ export const ChoosePanel = function ChoosePanel({
57
54
  footer,
58
55
  onClose,
59
56
  inset,
57
+ onMouseDown,
60
58
  }: ChoosePanelProps): ReactNode {
61
- const { theme } = useTheme();
62
59
  const mode = useMode();
63
60
  const defaults = buildChooseHints(mode, { multi });
64
61
  const hintContent = hints ? hints({ defaults, mode }) : defaults.join(" ");
65
62
 
66
63
  return (
67
- <box
68
- position="absolute"
69
- left={inset?.left ?? "20%"}
70
- right={inset?.right ?? "20%"}
71
- top={inset?.top ?? "20%"}
72
- bottom={inset?.bottom ?? "20%"}
73
- flexDirection="column"
74
- backgroundColor={theme.backgroundPanel}
75
- border
76
- borderColor={theme.borderActive}
64
+ <OverlayPanel
65
+ title={title}
66
+ prompt={filter}
67
+ hints={hintContent}
68
+ statusRight={statusRight}
69
+ footer={footer}
70
+ onClose={onClose}
71
+ inset={inset}
72
+ onMouseDown={onMouseDown}
77
73
  >
78
- {title !== null && title !== undefined && (
79
- <box
80
- flexDirection="row"
81
- height={1}
82
- paddingLeft={1}
83
- paddingRight={1}
84
- backgroundColor={theme.backgroundElement}
85
- >
86
- {typeof title === "string" ? (
87
- <text content={title} fg={theme.accent} style={{ flexGrow: 1 }} />
88
- ) : (
89
- <box flexDirection="row" style={{ flexGrow: 1 }}>
90
- {title}
91
- </box>
92
- )}
93
- {onClose && <CloseButton onClose={onClose} />}
94
- </box>
95
- )}
96
-
97
- {filter}
98
74
  {children}
99
-
100
- {footer !== null && footer !== undefined && (
101
- <box height={1} paddingLeft={1} paddingRight={1}>
102
- {footer}
103
- </box>
104
- )}
105
-
106
- <box
107
- flexDirection="row"
108
- height={1}
109
- paddingLeft={1}
110
- paddingRight={1}
111
- backgroundColor={theme.backgroundElement}
112
- >
113
- <box flexDirection="row" style={{ flexGrow: 1 }}>
114
- {typeof hintContent === "string" ? (
115
- <text content={hintContent} fg={theme.textMuted} />
116
- ) : (
117
- hintContent
118
- )}
119
- </box>
120
- {statusRight !== null &&
121
- statusRight !== undefined &&
122
- (typeof statusRight === "string" ? (
123
- <text content={statusRight} fg={theme.textMuted} />
124
- ) : (
125
- statusRight
126
- ))}
127
- </box>
128
- </box>
75
+ </OverlayPanel>
129
76
  );
130
77
  };
@@ -0,0 +1,12 @@
1
+ export {
2
+ createChooseStore,
3
+ selectActiveIndex,
4
+ selectError,
5
+ selectFilterQuery,
6
+ selectItems,
7
+ selectLoading,
8
+ selectMatches,
9
+ selectReloadRevision,
10
+ selectSelectedOriginalIndices,
11
+ } from "@tooee/renderers";
12
+ export type { ChooseStore, ChooseStoreContext, ChooseStoreEvents } from "@tooee/renderers";
@@ -0,0 +1,12 @@
1
+ /** Compile-time checks for the Choose component's 0.9 public props. */
2
+ import type { ChooseProps } from "./choose.js";
3
+
4
+ type Assert<T extends true> = T;
5
+ type IsNever<T> = [T] extends [never] ? true : false;
6
+ type Has<TKeys, TName> = [Extract<TKeys, TName>] extends [never] ? false : true;
7
+
8
+ type Props = keyof ChooseProps;
9
+ type RetainedProps = Assert<Has<Props, "actions" | "multi" | "prompt" | "title">>;
10
+ type RemovedProps = Assert<IsNever<Extract<Props, "commands" | "options">>>;
11
+
12
+ export type ChoosePublicContractChecks = [RetainedProps, RemovedProps];
package/src/choose.tsx CHANGED
@@ -1,10 +1,10 @@
1
1
  import { useImperativeHandle } from "react";
2
- import type { Ref } from "react";
2
+ import type { ReactNode, Ref } from "react";
3
3
  import { AppLayout } from "@tooee/layout";
4
4
  import { useHasOverlay, useHasModalOverlay } from "@tooee/overlays";
5
5
  import { useTheme } from "@tooee/themes";
6
6
  import { useThemeCommands, useQuitCommand } from "@tooee/shell";
7
- import { useCommandContext } from "@tooee/commands";
7
+ import { useSurfaceInvoke } from "@tooee/commands";
8
8
  import type { ActionDefinition } from "@tooee/commands";
9
9
  import { ChooseFilter } from "./choose-filter.js";
10
10
  import { ChooseList } from "./choose-list.js";
@@ -14,13 +14,9 @@ import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types
14
14
  import { useChoose } from "./use-choose.js";
15
15
  import type { ChooseController } from "./use-choose.js";
16
16
 
17
- export interface ChooseProps {
17
+ export interface ChooseProps extends ChooseOptions {
18
18
  contentProvider: ChooseContentProvider;
19
- options?: ChooseOptions;
20
- /** Legacy name retained for source compatibility. */
21
19
  actions?: ActionDefinition[];
22
- /** Additive alias used by new chooser compositions. */
23
- commands?: ActionDefinition[];
24
20
  controllerRef?: Ref<ChooseController>;
25
21
  renderItem?: ChooseListProps["renderItem"];
26
22
  /**
@@ -38,18 +34,19 @@ export interface ChooseProps {
38
34
 
39
35
  export const Choose = function Choose({
40
36
  contentProvider,
41
- options,
42
37
  actions,
43
- commands,
44
38
  controllerRef,
45
- renderItem,
46
- onConfirm,
39
+ emptyMessage,
40
+ multi = false,
47
41
  onCancel,
48
- }: ChooseProps): React.ReactNode {
42
+ onConfirm,
43
+ placeholder,
44
+ prompt,
45
+ renderItem,
46
+ title,
47
+ }: ChooseProps): ReactNode {
49
48
  const { theme } = useTheme();
50
- const { invoke } = useCommandContext();
51
- const effectiveCommands = commands ?? actions;
52
- const multi = options?.multi ?? false;
49
+ const { invoke } = useSurfaceInvoke();
53
50
 
54
51
  const { name: themeName } = useThemeCommands();
55
52
  useQuitCommand({ onQuit: () => onCancel?.() });
@@ -58,13 +55,13 @@ export const Choose = function Choose({
58
55
  const hasModalOverlay = useHasModalOverlay();
59
56
 
60
57
  const choose = useChoose({
61
- commands: effectiveCommands,
58
+ commands: actions,
62
59
  multi,
63
60
  onCancel,
64
61
  onSubmit: (result) => {
65
62
  // Standalone behaviour: a command named `submit` wins over `onConfirm`, so
66
63
  // action-driven CLIs keep their own submit semantics.
67
- if (effectiveCommands?.some((action) => action.id === "submit") === true) {
64
+ if (actions?.some((action) => action.id === "submit") === true) {
68
65
  invoke("submit");
69
66
  return;
70
67
  }
@@ -98,10 +95,10 @@ export const Choose = function Choose({
98
95
 
99
96
  const hints = buildChooseHints(choose.view.mode, { multi });
100
97
  let titleBar: { title: string } | undefined;
101
- if (options?.title !== undefined && options.title !== "") {
102
- titleBar = { title: options.title };
103
- } else if (options?.prompt !== undefined && options.prompt !== "") {
104
- titleBar = { title: options.prompt };
98
+ if (title !== undefined && title !== "") {
99
+ titleBar = { title };
100
+ } else if (prompt !== undefined && prompt !== "") {
101
+ titleBar = { title: prompt };
105
102
  }
106
103
 
107
104
  return (
@@ -125,15 +122,14 @@ export const Choose = function Choose({
125
122
  { label: "", value: hints.join(" ") },
126
123
  ],
127
124
  }}
128
- scrollProps={{ focused: false }}
129
125
  >
130
126
  <box flexDirection="column" style={{ flexGrow: 1 }}>
131
- <ChooseFilter choose={choose} placeholder={options?.placeholder} />
127
+ <ChooseFilter choose={choose} placeholder={placeholder} />
132
128
  <ChooseList
133
129
  choose={choose}
134
130
  rowClick="activate"
135
131
  renderItem={renderItem}
136
- emptyContent={options?.emptyMessage}
132
+ emptyContent={emptyMessage}
137
133
  suspended={hasModalOverlay}
138
134
  />
139
135
  </box>
package/src/fuzzy.ts CHANGED
@@ -1,28 +1,2 @@
1
- import { fuzzyMatchPositions } from "@tooee/fuzzy";
2
- import type { ChooseItem } from "./types.js";
3
-
4
- export interface FuzzyMatch {
5
- item: ChooseItem;
6
- originalIndex: number;
7
- score: number;
8
- positions: number[];
9
- }
10
-
11
- export const fuzzyFilter = function fuzzyFilter(items: ChooseItem[], query: string): FuzzyMatch[] {
12
- if (!query) {
13
- return items.map((item, i) => ({ item, originalIndex: i, positions: [], score: 0 }));
14
- }
15
-
16
- const results: FuzzyMatch[] = [];
17
-
18
- for (let i = 0; i < items.length; i += 1) {
19
- const item = items[i];
20
- const match = fuzzyMatchPositions(query, item.text);
21
- if (match) {
22
- results.push({ item, originalIndex: i, positions: match.positions, score: match.score });
23
- }
24
- }
25
-
26
- results.sort((a, b) => b.score - a.score);
27
- return results;
28
- };
1
+ export { fuzzyFilter } from "@tooee/renderers";
2
+ export type { FuzzyMatch } from "@tooee/renderers";
package/src/launch.tsx CHANGED
@@ -2,6 +2,7 @@ import { runCliSession } from "@tooee/shell";
2
2
  import type { ActionDefinition } from "@tooee/commands";
3
3
  import { Choose } from "./choose.js";
4
4
  import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js";
5
+ import type { ReactNode } from "react";
5
6
 
6
7
  export interface ChooseLaunchOptions {
7
8
  contentProvider: ChooseContentProvider;
@@ -13,10 +14,14 @@ export const launch = async function launch(
13
14
  opts: ChooseLaunchOptions,
14
15
  ): Promise<ChooseResult | null> {
15
16
  const result = await runCliSession<ChooseResult>(
16
- ({ resolve, cancel }): React.ReactNode => (
17
+ ({ resolve, cancel }): ReactNode => (
17
18
  <Choose
18
19
  contentProvider={opts.contentProvider}
19
- options={opts.options}
20
+ title={opts.options?.title}
21
+ prompt={opts.options?.prompt}
22
+ placeholder={opts.options?.placeholder}
23
+ multi={opts.options?.multi}
24
+ emptyMessage={opts.options?.emptyMessage}
20
25
  actions={opts.actions}
21
26
  onConfirm={resolve}
22
27
  onCancel={cancel}
@@ -25,6 +30,7 @@ export const launch = async function launch(
25
30
  {
26
31
  provider: { initialMode: "insert" },
27
32
  stdinPolicy: "tty-if-piped",
33
+ stdoutPolicy: "tty-if-redirected",
28
34
  },
29
35
  );
30
36
  return result;
package/src/source.ts CHANGED
@@ -1,18 +1 @@
1
- import type { ChooseItem, ChooseSource } from "./types.js";
2
-
3
- /** Resolve every public Choose source shape through one compatibility boundary. */
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
- };
15
-
16
- export const chooseSourceError = function chooseSourceError(error: unknown): string {
17
- return error instanceof Error ? error.message : String(error);
18
- };
1
+ export { loadChooseSource } from "@tooee/renderers";
package/src/types.ts CHANGED
@@ -1,28 +1,7 @@
1
- export interface ChooseItem {
2
- text: string;
3
- value?: string;
4
- icon?: string;
5
- description?: string;
6
- }
7
-
8
- export interface ChooseContentProvider {
9
- load: () => Promise<ChooseItem[]> | ChooseItem[];
10
- }
11
-
12
- /** Any item source accepted by the shared chooser core. */
13
- export type ChooseSource =
14
- | ChooseItem[]
15
- | ChooseContentProvider
16
- | (() => ChooseItem[] | Promise<ChooseItem[]>);
17
-
18
- export interface ChooseResult {
19
- items: ChooseItem[];
20
- }
21
-
22
- export interface ChooseOptions {
23
- multi?: boolean;
24
- title?: string;
25
- prompt?: string;
26
- placeholder?: string;
27
- emptyMessage?: string;
28
- }
1
+ export type {
2
+ ChooseContentProvider,
3
+ ChooseItem,
4
+ ChooseOptions,
5
+ ChooseResult,
6
+ ChooseSource,
7
+ } from "@tooee/renderers";