@hoox-sh/hoox-tui 0.3.1 → 0.3.2

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.
@@ -5,23 +5,11 @@
5
5
 
6
6
  /** @jsxImportSource @opentui/react */
7
7
  /**
8
- * SelectModal — a thin wrapper combining @opentui-ui/dialog modal behavior
9
- * with keyboard-navigable option selection, styled with Hoox color tokens.
10
- *
11
- * Keyboard navigation:
12
- * - Up/Down (or j/k): move selection
13
- * - Enter: confirm selection
14
- * - Escape: dismiss
15
- *
16
- * All colors use Colors tokens from @hoox-sh/hoox-shared (no hardcoded hex).
8
+ * SelectModal — keyboard-navigable option picker via in-house DialogHandle.
9
+ * Does not depend on @opentui-ui/dialog (nested OpenTUI crash on global install).
17
10
  */
18
- import { useState } from "react";
19
- import {
20
- type DialogId,
21
- type PromptContext,
22
- useDialogKeyboard,
23
- } from "@opentui-ui/dialog/react";
24
- import { Colors } from "@hoox-sh/hoox-shared";
11
+ import type { DialogHandle } from "./dialog";
12
+ import { showChoice } from "./dialog";
25
13
 
26
14
  // ── Types ──────────────────────────────────────────────────────────────────
27
15
 
@@ -45,153 +33,17 @@ export interface SelectModalOptions {
45
33
  closeOnClickOutside?: boolean;
46
34
  }
47
35
 
48
- /** Dialog handle — the subset of useDialog() needed by SelectModal */
49
- export interface SelectDialogHandle {
50
- prompt<T>(options: {
51
- content: (ctx: PromptContext<T>) => unknown;
52
- fallback?: T;
53
- closeOnClickOutside?: boolean;
54
- }): Promise<T | undefined>;
55
- }
56
-
57
- // ── Internal: keyboard-navigable option list ───────────────────────────────
58
-
59
- /** Props for the selectable option list rendered inside the dialog */
60
- interface SelectListProps {
61
- options: SelectOption[];
62
- accentColor: string;
63
- foregroundColor: string;
64
- mutedColor: string;
65
- highlightColor: string;
66
- resolve: (value: string) => void;
67
- dismiss: () => void;
68
- dialogId: DialogId;
69
- }
70
-
71
- /**
72
- * Internal component that renders a keyboard-navigable option list.
73
- * Uses useDialogKeyboard scoped to the dialog so that only the topmost
74
- * dialog receives keyboard events.
75
- */
76
- function SelectList({
77
- options,
78
- accentColor,
79
- foregroundColor,
80
- mutedColor,
81
- highlightColor,
82
- resolve,
83
- dismiss,
84
- dialogId,
85
- }: SelectListProps) {
86
- const [selectedIndex, setSelectedIndex] = useState(0);
87
-
88
- useDialogKeyboard((key) => {
89
- if (key.name === "down" || key.name === "j") {
90
- setSelectedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
91
- }
92
- if (key.name === "up" || key.name === "k") {
93
- setSelectedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
94
- }
95
- if (key.name === "return") {
96
- const selected = options[selectedIndex];
97
- if (selected) resolve(selected.key);
98
- }
99
- if (key.name === "escape") {
100
- dismiss();
101
- }
102
- }, dialogId);
103
-
104
- return (
105
- <box flexDirection="column" gap={0}>
106
- {options.map((opt, idx) => (
107
- <box flexDirection="row" gap={2} paddingLeft={1} paddingRight={1}>
108
- {/* Selection indicator */}
109
- <text fg={idx === selectedIndex ? accentColor : mutedColor}>
110
- {idx === selectedIndex ? "▶" : " "}
111
- </text>
112
- {/* Option label */}
113
- <text
114
- fg={idx === selectedIndex ? foregroundColor : mutedColor}
115
- bg={idx === selectedIndex ? highlightColor : undefined}
116
- onMouseUp={() => resolve(opt.key)}
117
- >
118
- {opt.label}
119
- </text>
120
- {/* Optional description */}
121
- {opt.description && (
122
- <text dim fg={mutedColor}>
123
- — {opt.description}
124
- </text>
125
- )}
126
- </box>
127
- ))}
128
- </box>
129
- );
130
- }
131
-
132
- // ── Public API ─────────────────────────────────────────────────────────────
133
-
134
36
  /**
135
37
  * Show a modal select dialog with keyboard navigation.
136
- *
137
- * Returns the selected option's `key`, or `undefined` if dismissed
138
- * (via Escape, clicking outside, or pressing Cancel).
139
- *
140
- * Keyboard controls:
141
- * - Up/Down or j/k: move selection highlight
142
- * - Enter: confirm current selection
143
- * - Escape: dismiss
144
- *
145
- * @example
146
- * const chosen = await showSelectModal(dialog, {
147
- * title: "Select Worker",
148
- * options: [
149
- * { key: "worker-a", label: "Alpha Worker", description: "US-East" },
150
- * { key: "worker-b", label: "Beta Worker", description: "EU-West" },
151
- * ],
152
- * })
38
+ * Returns the selected option's `key`, or `undefined` if dismissed.
153
39
  */
154
40
  export async function showSelectModal(
155
- dialog: SelectDialogHandle,
41
+ dialog: DialogHandle,
156
42
  opts: SelectModalOptions
157
43
  ): Promise<string | undefined> {
158
- const { title, options, closeOnClickOutside = true } = opts;
159
-
160
- return dialog.prompt<string>({
161
- content: (ctx: PromptContext<string>) => (
162
- <box flexDirection="column" padding={1} gap={1}>
163
- {/* Header */}
164
- <text fg={Colors.accent}>
165
- <b>{title}</b>
166
- </text>
167
- <text dim fg={Colors.muted}>
168
- Use ↑↓ to navigate, Enter to select, Esc to cancel
169
- </text>
170
-
171
- {/* Divider */}
172
- <box height={1} />
173
-
174
- {/* Selectable list */}
175
- <SelectList
176
- options={options}
177
- accentColor={Colors.accent}
178
- foregroundColor={Colors.foreground}
179
- mutedColor={Colors.muted}
180
- highlightColor={Colors.card}
181
- resolve={ctx.resolve}
182
- dismiss={ctx.dismiss}
183
- dialogId={ctx.dialogId}
184
- />
185
-
186
- {/* Footer actions */}
187
- <box flexDirection="row" justifyContent="flex-end" paddingTop={1}>
188
- <text fg={Colors.muted} onMouseUp={ctx.dismiss}>
189
- {" Cancel "}
190
- </text>
191
- </box>
192
- </box>
193
- ),
194
- fallback: undefined,
195
- closeOnClickOutside,
44
+ return showChoice(dialog, {
45
+ title: opts.title,
46
+ choices: opts.options,
47
+ closeOnClickOutside: opts.closeOnClickOutside ?? true,
196
48
  });
197
49
  }
@@ -60,35 +60,16 @@ function makeWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
60
60
  /** Create a mock DialogHandle that records calls */
61
61
  function mockDialog(shouldConfirm: boolean = true): DialogHandle {
62
62
  return {
63
- confirm: mock(
64
- async (options: { content: unknown; closeOnClickOutside?: boolean }) => {
65
- // Execute content to validate it renders
66
- if (
67
- typeof (options.content as (ctx: unknown) => unknown) === "function"
68
- ) {
69
- (
70
- options.content as (ctx: {
71
- resolve: (v: boolean) => void;
72
- }) => unknown
73
- )({
74
- resolve: () => {},
75
- });
76
- }
77
- return shouldConfirm;
78
- }
79
- ),
63
+ confirm: mock(async () => shouldConfirm),
80
64
  choice: async <K extends string>(options: {
81
- content: unknown;
82
65
  fallback?: K;
83
- closeOnClickOutside?: boolean;
84
- }): Promise<K | undefined> => {
85
- return options.fallback;
86
- },
87
- show: mock(
88
- (_options: { content: () => unknown; id?: string | number }) =>
89
- "loading-1"
90
- ),
91
- close: mock((_id?: string | number) => {}),
66
+ }): Promise<K | undefined> => options.fallback,
67
+ show: mock(() => "loading-1"),
68
+ close: mock(() => {}),
69
+ confirmSimple: mock(async () => shouldConfirm),
70
+ choiceSimple: async <K extends string>(options: {
71
+ fallback?: K;
72
+ }): Promise<K | undefined> => options.fallback,
92
73
  };
93
74
  }
94
75
 
@@ -10,11 +10,12 @@
10
10
  */
11
11
  import { useKeyboard } from "@opentui/react";
12
12
  import { useUIStore } from "@hoox-sh/hoox-shared";
13
+ import { isHooxDialogOpen } from "../components/ui/dialog-state";
13
14
 
14
- /** True when palette or a UI-store modal owns the keyboard. */
15
+ /** True when palette, UI modal, or in-house DialogProvider owns the keyboard. */
15
16
  export function isShellOverlayOpen(): boolean {
16
17
  const s = useUIStore.getState();
17
- return Boolean(s.commandPaletteOpen || s.modal);
18
+ return Boolean(s.commandPaletteOpen || s.modal || isHooxDialogOpen());
18
19
  }
19
20
 
20
21
  /** Key shape from OpenTUI; fields optional at the type boundary. */