@hoox-sh/hoox-tui 0.3.0 → 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.
- package/dist/main.js +597 -327
- package/package.json +3 -5
- package/src/app.tsx +7 -9
- package/src/components/ui/dialog-state.ts +15 -0
- package/src/components/ui/dialog.tsx +447 -172
- package/src/components/ui/select.tsx +10 -158
- package/src/components/ui/toast.tsx +44 -92
- package/src/components/views/service-manager.test.tsx +8 -27
- package/src/hooks/shell-overlay.ts +3 -2
- package/src/main.tsx +5 -14
|
@@ -5,23 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
/** @jsxImportSource @opentui/react */
|
|
7
7
|
/**
|
|
8
|
-
* SelectModal —
|
|
9
|
-
*
|
|
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 {
|
|
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:
|
|
41
|
+
dialog: DialogHandle,
|
|
156
42
|
opts: SelectModalOptions
|
|
157
43
|
): Promise<string | undefined> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
}
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* Provides toast helper functions (success, error, warning, info, loading)
|
|
9
|
-
* pre-configured with Hoox design system colors and durations.
|
|
7
|
+
* Fail-closed toast helpers.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* `@opentui-ui/toast@0.0.5` peers on old OpenTUI and often installs a nested
|
|
10
|
+
* `@opentui/core` that re-registers env vars (e.g. OPENTUI_FORCE_WCWIDTH)
|
|
11
|
+
* with a conflicting schema — that hard-crashes `hoox-tui` on global install.
|
|
12
|
+
*
|
|
13
|
+
* We therefore never import `@opentui-ui/toast` at module load. Helpers are
|
|
14
|
+
* pure no-ops (return a stub id). Connection/auth UX uses the status bar
|
|
15
|
+
* and service-store alerts instead.
|
|
14
16
|
*/
|
|
15
|
-
import { toast, TOAST_DURATION } from "@opentui-ui/toast";
|
|
16
17
|
import { Colors } from "@hoox-sh/hoox-shared";
|
|
17
18
|
|
|
18
19
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
@@ -29,128 +30,79 @@ export interface ToastOptions {
|
|
|
29
30
|
action?: { label: string; onClick: () => void };
|
|
30
31
|
/** Show a close button on the toast (default: false) */
|
|
31
32
|
closeButton?: boolean;
|
|
33
|
+
/** Optional id when updating an existing toast */
|
|
34
|
+
id?: ToastId;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** Returned toast ID for programmatic control */
|
|
35
38
|
export type ToastId = string | number;
|
|
36
39
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const baseStyle = {
|
|
41
|
-
backgroundColor: Colors.card,
|
|
42
|
-
foregroundColor: Colors.foreground,
|
|
43
|
-
borderColor: Colors.border,
|
|
44
|
-
paddingX: 1,
|
|
45
|
-
paddingY: 0,
|
|
46
|
-
};
|
|
40
|
+
// Keep style tokens referenced so callers/tests can still import Colors path
|
|
41
|
+
// and so future re-enable of a compatible toast lib has defaults ready.
|
|
42
|
+
void Colors;
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
info: { borderColor: Colors.highlight },
|
|
54
|
-
loading: { borderColor: Colors.dim },
|
|
55
|
-
};
|
|
44
|
+
let toastSeq = 0;
|
|
45
|
+
function nextId(): ToastId {
|
|
46
|
+
toastSeq += 1;
|
|
47
|
+
return `toast-noop-${toastSeq}`;
|
|
48
|
+
}
|
|
56
49
|
|
|
57
|
-
// ── Toast helper functions
|
|
50
|
+
// ── Toast helper functions (no-op, fail-closed) ────────────────────────────
|
|
58
51
|
|
|
59
52
|
/**
|
|
60
53
|
* Show a success toast notification.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* toastSuccess("Worker deployed successfully")
|
|
54
|
+
* Currently a no-op (see module doc) — status bar / alerts carry feedback.
|
|
65
55
|
*/
|
|
66
|
-
export function toastSuccess(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
56
|
+
export function toastSuccess(
|
|
57
|
+
_message: string,
|
|
58
|
+
_options?: ToastOptions
|
|
59
|
+
): ToastId {
|
|
60
|
+
return nextId();
|
|
72
61
|
}
|
|
73
62
|
|
|
74
63
|
/**
|
|
75
64
|
* Show an error toast notification.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* toastError("Failed to connect to API", { description: "Check your network" })
|
|
65
|
+
* Currently a no-op (see module doc).
|
|
80
66
|
*/
|
|
81
|
-
export function toastError(
|
|
82
|
-
return
|
|
83
|
-
...options,
|
|
84
|
-
style: { ...baseStyle, ...hooxTypeStyles.error },
|
|
85
|
-
duration: options?.duration ?? TOAST_DURATION.LONG,
|
|
86
|
-
});
|
|
67
|
+
export function toastError(_message: string, _options?: ToastOptions): ToastId {
|
|
68
|
+
return nextId();
|
|
87
69
|
}
|
|
88
70
|
|
|
89
71
|
/**
|
|
90
72
|
* Show a warning toast notification.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* toastWarning("Rate limit approaching", { description: "Slow down requests" })
|
|
73
|
+
* Currently a no-op (see module doc).
|
|
95
74
|
*/
|
|
96
|
-
export function toastWarning(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
75
|
+
export function toastWarning(
|
|
76
|
+
_message: string,
|
|
77
|
+
_options?: ToastOptions
|
|
78
|
+
): ToastId {
|
|
79
|
+
return nextId();
|
|
102
80
|
}
|
|
103
81
|
|
|
104
82
|
/**
|
|
105
83
|
* Show an info toast notification.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* toastInfo("New version available", { action: { label: "Update", onClick: () => {} } })
|
|
84
|
+
* Currently a no-op (see module doc).
|
|
110
85
|
*/
|
|
111
|
-
export function toastInfo(
|
|
112
|
-
return
|
|
113
|
-
...options,
|
|
114
|
-
style: { ...baseStyle, ...hooxTypeStyles.info },
|
|
115
|
-
duration: options?.duration ?? TOAST_DURATION.DEFAULT,
|
|
116
|
-
});
|
|
86
|
+
export function toastInfo(_message: string, _options?: ToastOptions): ToastId {
|
|
87
|
+
return nextId();
|
|
117
88
|
}
|
|
118
89
|
|
|
119
90
|
/**
|
|
120
91
|
* Show a persistent loading toast notification.
|
|
121
|
-
*
|
|
122
|
-
* Returns the toast ID so it can be updated to success/error later.
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* const id = toastLoading("Deploying worker...")
|
|
126
|
-
* try {
|
|
127
|
-
* await deploy()
|
|
128
|
-
* toastSuccess("Deployed!", { id })
|
|
129
|
-
* } catch {
|
|
130
|
-
* toastError("Deploy failed", { id })
|
|
131
|
-
* }
|
|
92
|
+
* Currently a no-op (see module doc).
|
|
132
93
|
*/
|
|
133
94
|
export function toastLoading(
|
|
134
|
-
|
|
135
|
-
|
|
95
|
+
_message: string,
|
|
96
|
+
_options?: Omit<ToastOptions, "duration">
|
|
136
97
|
): ToastId {
|
|
137
|
-
return
|
|
138
|
-
...options,
|
|
139
|
-
style: { ...baseStyle, ...hooxTypeStyles.loading },
|
|
140
|
-
duration: TOAST_DURATION.PERSISTENT,
|
|
141
|
-
} as ToastOptions);
|
|
98
|
+
return nextId();
|
|
142
99
|
}
|
|
143
100
|
|
|
144
101
|
/**
|
|
145
|
-
* Dismiss a specific toast or all toasts.
|
|
146
|
-
* Thin wrapper around toast.dismiss().
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* dismissToast(id) // dismiss specific
|
|
150
|
-
* dismissToast() // dismiss all
|
|
102
|
+
* Dismiss a specific toast or all toasts (no-op).
|
|
151
103
|
*/
|
|
152
|
-
export function dismissToast(
|
|
153
|
-
|
|
104
|
+
export function dismissToast(_id?: ToastId): void {
|
|
105
|
+
// no-op
|
|
154
106
|
}
|
|
155
107
|
|
|
156
108
|
// ── Clipboard copy feedback ────────────────────────────────────────────────
|
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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. */
|
package/src/main.tsx
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createCliRenderer } from "@opentui/core";
|
|
15
15
|
import { createRoot } from "@opentui/react";
|
|
16
|
-
import { ToasterRenderable } from "@opentui-ui/toast";
|
|
17
16
|
import { CrashRecoveryApp } from "./app";
|
|
18
17
|
import {
|
|
19
18
|
Colors,
|
|
@@ -103,19 +102,11 @@ async function main() {
|
|
|
103
102
|
// Set renderer ref so hooks + components can access it via getRendererRef()
|
|
104
103
|
setRendererRef(renderer);
|
|
105
104
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
).root;
|
|
113
|
-
root.add(new ToasterRenderable(renderer));
|
|
114
|
-
} catch (err) {
|
|
115
|
-
await tuiDevLog.warn("startup", "Toaster mount failed — toasts may no-op", {
|
|
116
|
-
error: err instanceof Error ? err.message : String(err),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
105
|
+
// Note: @opentui-ui/toast ToasterRenderable is intentionally NOT mounted.
|
|
106
|
+
// toast@0.0.5 peers on ancient @opentui and nests a second core that
|
|
107
|
+
// re-registers OPENTUI_FORCE_WCWIDTH (crash on global install). Connection
|
|
108
|
+
// feedback still surfaces via the status bar + service-store alerts.
|
|
109
|
+
// Toast helpers in components/ui/toast.tsx are fail-closed no-ops.
|
|
119
110
|
|
|
120
111
|
// Mouse drag-select → clipboard (OSC 52 + system tools)
|
|
121
112
|
enableAutoCopyOnSelection(
|