@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.
@@ -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
  }
@@ -4,15 +4,16 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Thin wrappers around @opentui-ui/toast with Hoox color token defaults.
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
- * All helpers use the global toast singleton from @opentui-ui/toast.
12
- * A ToasterRenderable must be initialized in the app root before calling
13
- * these functions (see app.tsx).
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
- // ── Hoox-styled toast style overrides ──────────────────────────────────────
38
-
39
- /** Base style shared across all toast types */
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
- /** Per-type border color overrides using Hoox design tokens */
49
- const hooxTypeStyles = {
50
- success: { borderColor: Colors.success },
51
- error: { borderColor: Colors.error },
52
- warning: { borderColor: Colors.warning },
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
- * Uses green border (Colors.success) and 3s auto-dismiss.
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(message: string, options?: ToastOptions): ToastId {
67
- return toast.success(message, {
68
- ...options,
69
- style: { ...baseStyle, ...hooxTypeStyles.success },
70
- duration: options?.duration ?? TOAST_DURATION.SHORT,
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
- * Uses red border (Colors.error) and 6s auto-dismiss.
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(message: string, options?: ToastOptions): ToastId {
82
- return toast.error(message, {
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
- * Uses amber border (Colors.warning) and 6s auto-dismiss.
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(message: string, options?: ToastOptions): ToastId {
97
- return toast.warning(message, {
98
- ...options,
99
- style: { ...baseStyle, ...hooxTypeStyles.warning },
100
- duration: options?.duration ?? TOAST_DURATION.LONG,
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
- * Uses accent border (Colors.accent, orange) and 4s auto-dismiss.
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(message: string, options?: ToastOptions): ToastId {
112
- return toast.info(message, {
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
- * Uses muted border (Colors.muted) and does NOT auto-dismiss.
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
- message: string,
135
- options?: Omit<ToastOptions, "duration">
95
+ _message: string,
96
+ _options?: Omit<ToastOptions, "duration">
136
97
  ): ToastId {
137
- return toast.loading(message, {
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(id?: ToastId): void {
153
- toast.dismiss(id as string | number | undefined);
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
- 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. */
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
- // Mount toast host so connection/auth toasts actually render
107
- try {
108
- const root = (
109
- renderer as unknown as {
110
- root: { add: (node: unknown) => void };
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(