@hoox-sh/hoox-tui 0.2.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 (104) hide show
  1. package/README.md +247 -0
  2. package/bin/hoox-tui.js +35 -0
  3. package/dist/main.js +17594 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +696 -0
  6. package/src/cli-bridge-test-double.ts +212 -0
  7. package/src/components/layout/layout.test.tsx +205 -0
  8. package/src/components/layout/sidebar.tsx +69 -0
  9. package/src/components/layout/statusbar.test.tsx +423 -0
  10. package/src/components/layout/statusbar.tsx +348 -0
  11. package/src/components/shared/animated-border.tsx +58 -0
  12. package/src/components/shared/chrome.test.tsx +29 -0
  13. package/src/components/shared/command-palette.tsx +361 -0
  14. package/src/components/shared/cool-brackets.tsx +75 -0
  15. package/src/components/shared/crash-screen.tsx +167 -0
  16. package/src/components/shared/error-boundary.tsx +99 -0
  17. package/src/components/shared/keybinding-hint.tsx +37 -0
  18. package/src/components/shared/panel.tsx +61 -0
  19. package/src/components/shared/quit-modal.tsx +65 -0
  20. package/src/components/shared/shared.test.tsx +538 -0
  21. package/src/components/shared/spinner.tsx +85 -0
  22. package/src/components/shared/status-dot.tsx +54 -0
  23. package/src/components/shared/view-header.tsx +53 -0
  24. package/src/components/ui/connection-toasts.test.ts +50 -0
  25. package/src/components/ui/connection-toasts.ts +215 -0
  26. package/src/components/ui/dialog.tsx +281 -0
  27. package/src/components/ui/select.tsx +197 -0
  28. package/src/components/ui/toast.tsx +185 -0
  29. package/src/components/views/ai-chat.test.tsx +81 -0
  30. package/src/components/views/ai-chat.tsx +576 -0
  31. package/src/components/views/config-editor/action-bar.tsx +102 -0
  32. package/src/components/views/config-editor/code-editor.tsx +597 -0
  33. package/src/components/views/config-editor/file-tree.tsx +113 -0
  34. package/src/components/views/config-editor/types.ts +37 -0
  35. package/src/components/views/config-editor.test.tsx +465 -0
  36. package/src/components/views/config-editor.tsx +709 -0
  37. package/src/components/views/dashboard/alerts-panel.tsx +154 -0
  38. package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
  39. package/src/components/views/dashboard/model-health-section.tsx +276 -0
  40. package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
  41. package/src/components/views/dashboard.test.tsx +347 -0
  42. package/src/components/views/dashboard.tsx +647 -0
  43. package/src/components/views/db-query.test.tsx +115 -0
  44. package/src/components/views/db-query.tsx +849 -0
  45. package/src/components/views/edge-topology.test.tsx +198 -0
  46. package/src/components/views/edge-topology.tsx +572 -0
  47. package/src/components/views/kv-viewer.test.tsx +241 -0
  48. package/src/components/views/kv-viewer.tsx +717 -0
  49. package/src/components/views/logs-viewer.test.tsx +273 -0
  50. package/src/components/views/logs-viewer.tsx +616 -0
  51. package/src/components/views/queue-depth.test.tsx +221 -0
  52. package/src/components/views/queue-depth.tsx +376 -0
  53. package/src/components/views/secrets-viewer.test.tsx +65 -0
  54. package/src/components/views/secrets-viewer.tsx +548 -0
  55. package/src/components/views/service-manager.test.tsx +358 -0
  56. package/src/components/views/service-manager.tsx +946 -0
  57. package/src/components/views/settings.test.tsx +648 -0
  58. package/src/components/views/settings.tsx +1497 -0
  59. package/src/components/views/setup-wizard.test.tsx +297 -0
  60. package/src/components/views/setup-wizard.tsx +1321 -0
  61. package/src/components/views/trade-monitor.test.tsx +592 -0
  62. package/src/components/views/trade-monitor.tsx +745 -0
  63. package/src/components/views/worker-detail.test.tsx +289 -0
  64. package/src/components/views/worker-detail.tsx +676 -0
  65. package/src/components/views/worker-settings.test.tsx +43 -0
  66. package/src/components/views/worker-settings.tsx +563 -0
  67. package/src/components/views/workers-overview.test.tsx +260 -0
  68. package/src/components/views/workers-overview.tsx +631 -0
  69. package/src/hooks/hooks.test.ts +72 -0
  70. package/src/hooks/index.ts +11 -0
  71. package/src/hooks/renderer-ref.ts +22 -0
  72. package/src/hooks/use-keyboard.ts +130 -0
  73. package/src/hooks/use-polling.ts +73 -0
  74. package/src/hooks/use-service-data.ts +18 -0
  75. package/src/main.tsx +117 -0
  76. package/src/network-test-double.ts +133 -0
  77. package/src/opentui-augment.d.ts +45 -0
  78. package/src/opentui-jsx.d.ts +70 -0
  79. package/src/services/cli-bridge/index.ts +1662 -0
  80. package/src/services/cli-bridge/standalone.ts +315 -0
  81. package/src/services/cli-bridge/types.ts +307 -0
  82. package/src/services/cli-bridge.test.ts +406 -0
  83. package/src/services/clipboard.test.ts +57 -0
  84. package/src/services/clipboard.ts +180 -0
  85. package/src/services/dashboard-settings-loader.ts +31 -0
  86. package/src/services/dev-log.test.ts +168 -0
  87. package/src/services/dev-log.ts +194 -0
  88. package/src/services/hoox-path-service.test.ts +183 -0
  89. package/src/services/hoox-path-service.ts +185 -0
  90. package/src/services/tui-connection.test.ts +212 -0
  91. package/src/services/tui-connection.ts +236 -0
  92. package/src/services/tui-storage.test.ts +92 -0
  93. package/src/services/tui-storage.ts +86 -0
  94. package/src/stores/config-store.test.ts +360 -0
  95. package/src/stores/service-store.test.ts +1026 -0
  96. package/src/stores/session-restore.test.ts +134 -0
  97. package/src/stores/ui-store.test.ts +276 -0
  98. package/src/test-setup.ts +265 -0
  99. package/src/test-utils.ts +179 -0
  100. package/src/types.ts +153 -0
  101. package/src/utils/colors.test.ts +326 -0
  102. package/src/utils/formatters.test.ts +236 -0
  103. package/src/view-registry.test.ts +132 -0
  104. package/src/view-registry.tsx +368 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Shared test utilities for TUI components.
8
+ *
9
+ * Provides:
10
+ * - Type aliases matching TUI test expectations
11
+ * - Factory functions (makeWorker, makeLog, makeTrade)
12
+ * - Mock ErrorBoundary pass-through
13
+ * - Re-exports of the shared CLI bridge test double
14
+ *
15
+ * Centralises duplicated type definitions + factory functions
16
+ * that were previously copy-pasted across test files.
17
+ *
18
+ * Process-wide `cli-bridge` mocking is installed once in `test-setup.ts`.
19
+ * Do **not** call `mock.module` for cli-bridge or shared stores in view tests.
20
+ */
21
+
22
+ import type { ReactNode } from "react";
23
+ import type { ViewId } from "@hoox-sh/hoox-shared";
24
+
25
+ export {
26
+ cliBridgeDouble,
27
+ resetCliBridgeDouble,
28
+ okCliResult,
29
+ failCliResult,
30
+ } from "./cli-bridge-test-double";
31
+
32
+ export {
33
+ hooxFetchMock,
34
+ subscribeSSEMock,
35
+ resetNetworkDoubles,
36
+ setMockApiData,
37
+ setMockApiFailure,
38
+ setMockApiDelay,
39
+ emitSseEvent,
40
+ abortAllSseSubscriptions,
41
+ mockApiData,
42
+ sseCallbacks,
43
+ sseSubscriptions,
44
+ } from "./network-test-double";
45
+
46
+ /** Canonical 16 ViewIds — keep aligned with types.ts + view-registry. */
47
+ export const ALL_VIEW_IDS: ViewId[] = [
48
+ "dashboard",
49
+ "workers",
50
+ "worker-detail",
51
+ "trade-monitor",
52
+ "logs-viewer",
53
+ "service-manager",
54
+ "config-editor",
55
+ "setup-wizard",
56
+ "settings",
57
+ "queue-depth",
58
+ "kv-viewer",
59
+ "secrets-viewer",
60
+ "db-query",
61
+ "ai-chat",
62
+ "edge-topology",
63
+ "worker-settings",
64
+ ];
65
+
66
+ // ── Type Aliases ─────────────────────────────────────────────────────────────
67
+
68
+ export type WorkerStatus = "operational" | "degraded" | "down";
69
+ export type LogLevel = "debug" | "info" | "warn" | "error";
70
+ export type ConnectionStatus =
71
+ | "connected"
72
+ | "polling"
73
+ | "offline"
74
+ | "reconnecting";
75
+ export type TradeSide = "buy" | "sell";
76
+
77
+ export interface TestWorkerInfo {
78
+ id: string;
79
+ name: string;
80
+ status: WorkerStatus;
81
+ uptime: number;
82
+ cpu: number;
83
+ memory: number;
84
+ requests: number;
85
+ durableObjectCount: number;
86
+ edgeCount: number;
87
+ version?: string;
88
+ lastDeployed?: number;
89
+ }
90
+
91
+ export interface TestLogEntry {
92
+ id: string;
93
+ level: LogLevel;
94
+ message: string;
95
+ timestamp: number;
96
+ workerId?: string;
97
+ source?: string;
98
+ metadata?: Record<string, unknown>;
99
+ }
100
+
101
+ export interface TestTrade {
102
+ id: string;
103
+ symbol: string;
104
+ side: TradeSide;
105
+ price: number;
106
+ quantity: number;
107
+ timestamp: number;
108
+ exchange: string;
109
+ pnl?: number;
110
+ }
111
+
112
+ // ── Factory Functions ────────────────────────────────────────────────────────
113
+
114
+ /** Create a test worker fixture with sensible defaults. */
115
+ export function makeWorker(
116
+ overrides: Partial<TestWorkerInfo> = {}
117
+ ): TestWorkerInfo {
118
+ return {
119
+ id: "test-worker-1",
120
+ name: "trade-executor",
121
+ status: "operational",
122
+ uptime: 86400,
123
+ cpu: 42.5,
124
+ memory: 64,
125
+ requests: 15000,
126
+ durableObjectCount: 3,
127
+ edgeCount: 12,
128
+ version: "1.2.3",
129
+ lastDeployed: Date.now() - 3600000,
130
+ ...overrides,
131
+ };
132
+ }
133
+
134
+ /** Create a test log entry with sensible defaults. */
135
+ export function makeLog(overrides: Partial<TestLogEntry> = {}): TestLogEntry {
136
+ return {
137
+ id: crypto.randomUUID?.() ?? "log-1",
138
+ level: "info",
139
+ message: "Test message",
140
+ timestamp: Date.now(),
141
+ workerId: undefined,
142
+ source: undefined,
143
+ ...overrides,
144
+ };
145
+ }
146
+
147
+ /** Create a test trade with sensible defaults. */
148
+ export function makeTrade(overrides: Partial<TestTrade> = {}): TestTrade {
149
+ return {
150
+ id: `trade-${Math.random().toString(36).slice(2, 10)}`,
151
+ symbol: "BTC",
152
+ side: "buy",
153
+ price: 87432.5,
154
+ quantity: 0.15,
155
+ timestamp: Date.now() - 60000,
156
+ exchange: "binance",
157
+ pnl: 125.5,
158
+ ...overrides,
159
+ };
160
+ }
161
+
162
+ // ── Mock ErrorBoundary ───────────────────────────────────────────────────────
163
+
164
+ /**
165
+ * Pass-through ErrorBoundary for tests.
166
+ *
167
+ * Usage:
168
+ * mock.module("../shared/error-boundary", () => ({
169
+ * ErrorBoundary: MockErrorBoundary,
170
+ * }))
171
+ */
172
+ export function MockErrorBoundary({
173
+ children,
174
+ }: {
175
+ viewName: string;
176
+ children: ReactNode;
177
+ }): ReactNode {
178
+ return children;
179
+ }
package/src/types.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * TUI-specific types — Navigation, modals, keyboard shortcuts.
8
+ */
9
+
10
+ import type { CliErrorType, CliErrorDetails } from "@hoox-sh/hoox-shared";
11
+
12
+ // Re-export shared CLI error types for convenience so consumers can
13
+ // import everything from the tui/types module.
14
+ export type { CliErrorType, CliErrorDetails };
15
+
16
+ /** All primary views registered with the TUI. */
17
+ export const ALL_VIEWS = [
18
+ "dashboard",
19
+ "workers",
20
+ "worker-detail",
21
+ "trade-monitor",
22
+ "logs-viewer",
23
+ "service-manager",
24
+ "config-editor",
25
+ "setup-wizard",
26
+ "settings",
27
+ "queue-depth",
28
+ "kv-viewer",
29
+ "secrets-viewer",
30
+ "db-query",
31
+ "ai-chat",
32
+ "edge-topology",
33
+ "worker-settings",
34
+ ] as const;
35
+
36
+ export type ViewId = (typeof ALL_VIEWS)[number];
37
+
38
+ export const VIEW_LABELS: Record<ViewId, string> = {
39
+ dashboard: "DASHBOARD",
40
+ workers: "WORKERS",
41
+ "worker-detail": "WORKER DETAIL",
42
+ "trade-monitor": "TRADES",
43
+ "logs-viewer": "LOGS",
44
+ "service-manager": "SERVICES",
45
+ "config-editor": "CONFIG",
46
+ "setup-wizard": "SETUP",
47
+ settings: "SETTINGS",
48
+ "queue-depth": "QUEUES",
49
+ "kv-viewer": "KV",
50
+ "secrets-viewer": "SECRETS",
51
+ "db-query": "DB QUERY",
52
+ "ai-chat": "AI CHAT",
53
+ "edge-topology": "TOPOLOGY",
54
+ "worker-settings": "W SETTINGS",
55
+ };
56
+
57
+ export const VIEW_ORDER: ViewId[] = [
58
+ "dashboard",
59
+ "workers",
60
+ "worker-detail",
61
+ "trade-monitor",
62
+ "logs-viewer",
63
+ "service-manager",
64
+ "config-editor",
65
+ "setup-wizard",
66
+ "settings",
67
+ "queue-depth",
68
+ "kv-viewer",
69
+ "secrets-viewer",
70
+ "db-query",
71
+ "ai-chat",
72
+ "edge-topology",
73
+ "worker-settings",
74
+ ];
75
+
76
+ export function viewIndex(view: ViewId): number {
77
+ return VIEW_ORDER.indexOf(view);
78
+ }
79
+
80
+ export type ModalType = "confirm" | "choice" | "loading" | "prompt";
81
+
82
+ export interface ModalState {
83
+ type: ModalType;
84
+ id: string;
85
+ title?: string;
86
+ message?: string;
87
+ resolve?: (value: unknown) => void;
88
+ }
89
+
90
+ export interface ShortcutMap {
91
+ [keys: string]: string;
92
+ }
93
+
94
+ export const DEFAULT_SHORTCUTS: ShortcutMap = {
95
+ "Ctrl+1": "DASHBOARD",
96
+ "Ctrl+2": "WORKERS",
97
+ "Ctrl+3": "WORKER DETAIL",
98
+ "Ctrl+4": "TRADES",
99
+ "Ctrl+5": "LOGS",
100
+ "Ctrl+6": "SERVICES",
101
+ "Ctrl+7": "CONFIG",
102
+ "Ctrl+8": "SETUP",
103
+ "Ctrl+9": "SETTINGS",
104
+ "Ctrl+0": "QUEUES",
105
+ "Ctrl+Alt+K": "KV",
106
+ "Ctrl+Alt+S": "SECRETS",
107
+ "Ctrl+Alt+C": "AI CHAT",
108
+ "Ctrl+Alt+Q": "DB QUERY",
109
+ "Ctrl+Alt+E": "TOPOLOGY",
110
+ "Ctrl+Alt+W": "W SETTINGS",
111
+ "Ctrl+P": "COMMAND PALETTE",
112
+ "Ctrl+B": "TOGGLE SIDEBAR",
113
+ "Ctrl+R": "REFRESH DATA",
114
+ "Ctrl+Q": "QUIT",
115
+ Esc: "BACK / CLOSE",
116
+ Tab: "NEXT FOCUS",
117
+ "Shift+Tab": "PREVIOUS FOCUS",
118
+ Space: "TOGGLE / PAUSE",
119
+ "/": "SEARCH",
120
+ Enter: "SELECT / CONFIRM",
121
+ };
122
+
123
+ // ─── CliBridge Types ─────────────────────────────────────────────
124
+
125
+ export interface CliResult<T = unknown> {
126
+ success: boolean;
127
+ exitCode: number;
128
+ stdout: string;
129
+ stderr: string;
130
+ data: T | null;
131
+ duration: number;
132
+ /**
133
+ * The full command string that was executed, e.g. `"hoox check health"`.
134
+ * Captured by `cli-bridge.exec()` so the status bar can show exactly
135
+ * which command failed. Empty string when the binary itself was not found.
136
+ */
137
+ command: string;
138
+ /**
139
+ * Classification of the failure, or `null` on success.
140
+ * Drives the icon and recovery hint shown in the expanded error panel.
141
+ */
142
+ errorType: CliErrorType | null;
143
+ }
144
+
145
+ export type CliCommandStatus = "idle" | "running" | "success" | "failure";
146
+
147
+ export interface CliCommandState {
148
+ tag: string;
149
+ description: string;
150
+ status: CliCommandStatus;
151
+ result: CliResult | null;
152
+ startedAt: number | null;
153
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Colors Utility Tests — Design token validation.
8
+ *
9
+ * Tests that the Hoox design system colors are correctly defined,
10
+ * follow the black / cool-indigo DNA (#050508 bg, #818CF8 accent),
11
+ * and provide adequate contrast ratios for TUI readability.
12
+ *
13
+ * Source of truth: @hoox-sh/hoox-shared (Colors + status maps).
14
+ * Uses Bun test runner.
15
+ */
16
+ import { describe, it, expect } from "bun:test";
17
+ import {
18
+ Colors,
19
+ CoolBracketPalette,
20
+ ConnectionStatusColor,
21
+ WorkerStatusColor,
22
+ LogLevelColor,
23
+ AlertSeverityColor,
24
+ } from "@hoox-sh/hoox-shared";
25
+
26
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ /** Parse a hex color string into RGB components */
29
+ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
30
+ const match = hex.match(
31
+ /^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/
32
+ );
33
+ if (!match) return null;
34
+ return {
35
+ r: parseInt(match[1], 16),
36
+ g: parseInt(match[2], 16),
37
+ b: parseInt(match[3], 16),
38
+ };
39
+ }
40
+
41
+ /** Calculate relative luminance (per WCAG 2.0) */
42
+ function relativeLuminance(hex: string): number {
43
+ const rgb = hexToRgb(hex);
44
+ if (!rgb) return 0;
45
+ const toLinear = (c: number): number => {
46
+ const s = c / 255;
47
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
48
+ };
49
+ return (
50
+ 0.2126 * toLinear(rgb.r) +
51
+ 0.7152 * toLinear(rgb.g) +
52
+ 0.0722 * toLinear(rgb.b)
53
+ );
54
+ }
55
+
56
+ /** Calculate contrast ratio between two hex colors */
57
+ function contrastRatio(hex1: string, hex2: string): number {
58
+ const l1 = relativeLuminance(hex1);
59
+ const l2 = relativeLuminance(hex2);
60
+ const lighter = Math.max(l1, l2);
61
+ const darker = Math.min(l1, l2);
62
+ return (lighter + 0.05) / (darker + 0.05);
63
+ }
64
+
65
+ // ─── Test Suite ───────────────────────────────────────────────────────────────
66
+
67
+ describe("Colors Design System", () => {
68
+ // ── All tokens are valid hex colors ─────────────────────────────────────
69
+
70
+ describe("valid hex colors", () => {
71
+ it("all color tokens are valid 6-digit hex", () => {
72
+ for (const [name, value] of Object.entries(Colors)) {
73
+ expect(value).toMatch(/^#[0-9A-Fa-f]{6}$/);
74
+ }
75
+ });
76
+
77
+ it("all tokens can be parsed to RGB", () => {
78
+ for (const [name, value] of Object.entries(Colors)) {
79
+ const rgb = hexToRgb(value);
80
+ expect(rgb).not.toBeNull();
81
+ expect(rgb!.r).toBeGreaterThanOrEqual(0);
82
+ expect(rgb!.r).toBeLessThanOrEqual(255);
83
+ expect(rgb!.g).toBeGreaterThanOrEqual(0);
84
+ expect(rgb!.g).toBeLessThanOrEqual(255);
85
+ expect(rgb!.b).toBeGreaterThanOrEqual(0);
86
+ expect(rgb!.b).toBeLessThanOrEqual(255);
87
+ }
88
+ });
89
+ });
90
+
91
+ // ── Black / cool DNA ────────────────────────────────────────────────────
92
+
93
+ describe("black / cool DNA", () => {
94
+ it("background is near-black (#050508)", () => {
95
+ expect(Colors.background).toBe("#050508");
96
+ const rgb = hexToRgb(Colors.background)!;
97
+ expect(rgb.r).toBeLessThan(12);
98
+ expect(rgb.g).toBeLessThan(12);
99
+ expect(rgb.b).toBeLessThan(16);
100
+ });
101
+
102
+ it("accent is cool indigo (#818CF8)", () => {
103
+ expect(Colors.accent).toBe("#818CF8");
104
+ const rgb = hexToRgb(Colors.accent)!;
105
+ // Indigo: blue-ish, not warm orange
106
+ expect(rgb.b).toBeGreaterThan(rgb.r);
107
+ expect(rgb.b).toBeGreaterThan(200);
108
+ });
109
+
110
+ it("highlight is cool cyan (distinct from accent)", () => {
111
+ expect(Colors.highlight).toBe("#22D3EE");
112
+ expect(Colors.highlight).not.toBe(Colors.accent);
113
+ const rgb = hexToRgb(Colors.highlight)!;
114
+ expect(rgb.b).toBeGreaterThan(200);
115
+ expect(rgb.g).toBeGreaterThan(180);
116
+ });
117
+
118
+ it("cards are near-black elevated (#0A0A0F)", () => {
119
+ expect(Colors.card).toBe("#0A0A0F");
120
+ const bg = hexToRgb(Colors.background)!;
121
+ const card = hexToRgb(Colors.card)!;
122
+ // Slightly elevated vs background
123
+ expect(card.r + card.g + card.b).toBeGreaterThan(bg.r + bg.g + bg.b);
124
+ });
125
+
126
+ it("borders are cool dark, not mid-grey", () => {
127
+ const rgb = hexToRgb(Colors.border)!;
128
+ expect(rgb.r).toBeLessThan(40);
129
+ expect(rgb.g).toBeLessThan(40);
130
+ expect(rgb.b).toBeLessThan(50);
131
+ });
132
+ });
133
+
134
+ describe("CoolBracketPalette", () => {
135
+ it("has multiple cool hues for animation", () => {
136
+ expect(CoolBracketPalette.length).toBeGreaterThanOrEqual(6);
137
+ for (const c of CoolBracketPalette) {
138
+ expect(c).toMatch(/^#[0-9A-Fa-f]{6}$/);
139
+ }
140
+ });
141
+
142
+ it("starts in cyan/sky range (cool, not orange)", () => {
143
+ const first = hexToRgb(CoolBracketPalette[0]!)!;
144
+ expect(first.b).toBeGreaterThan(first.r);
145
+ });
146
+ });
147
+
148
+ // ── Semantic Colors ─────────────────────────────────────────────────────
149
+
150
+ describe("semantic colors", () => {
151
+ it("success is cool emerald", () => {
152
+ expect(Colors.success).toBe("#34D399");
153
+ const rgb = hexToRgb(Colors.success)!;
154
+ expect(rgb.g).toBeGreaterThan(180);
155
+ });
156
+
157
+ it("error is cool rose", () => {
158
+ expect(Colors.error).toBe("#FB7185");
159
+ const rgb = hexToRgb(Colors.error)!;
160
+ expect(rgb.r).toBeGreaterThan(200);
161
+ });
162
+
163
+ it("warning is amber", () => {
164
+ expect(Colors.warning).toBe("#FBBF24");
165
+ const rgb = hexToRgb(Colors.warning)!;
166
+ expect(rgb.r).toBeGreaterThan(200);
167
+ expect(rgb.g).toBeGreaterThan(150);
168
+ });
169
+
170
+ it("info is sky blue", () => {
171
+ expect(Colors.info).toBe("#38BDF8");
172
+ const rgb = hexToRgb(Colors.info)!;
173
+ expect(rgb.b).toBeGreaterThan(200);
174
+ });
175
+ });
176
+
177
+ // ── Contrast Ratios ─────────────────────────────────────────────────────
178
+
179
+ describe("contrast ratios", () => {
180
+ it("foreground on background has sufficient contrast (WCAG AA)", () => {
181
+ const ratio = contrastRatio(Colors.foreground, Colors.background);
182
+ // WCAG AA requires 4.5:1 for normal text
183
+ expect(ratio).toBeGreaterThan(4.5);
184
+ });
185
+
186
+ it("accent on background has sufficient contrast", () => {
187
+ const ratio = contrastRatio(Colors.accent, Colors.background);
188
+ // Large/bold text requires 3:1
189
+ expect(ratio).toBeGreaterThan(3.0);
190
+ });
191
+
192
+ it("success on background has sufficient contrast", () => {
193
+ const ratio = contrastRatio(Colors.success, Colors.background);
194
+ expect(ratio).toBeGreaterThan(3.0);
195
+ });
196
+
197
+ it("error on background has sufficient contrast", () => {
198
+ const ratio = contrastRatio(Colors.error, Colors.background);
199
+ expect(ratio).toBeGreaterThan(3.0);
200
+ });
201
+
202
+ it("muted on background has at least some contrast (visible)", () => {
203
+ const ratio = contrastRatio(Colors.muted, Colors.background);
204
+ // Muted text should be readable but not dominant (>= 3.0)
205
+ expect(ratio).toBeGreaterThan(3.0);
206
+ });
207
+
208
+ it("foreground on card has sufficient contrast", () => {
209
+ const ratio = contrastRatio(Colors.foreground, Colors.card);
210
+ expect(ratio).toBeGreaterThan(4.5);
211
+ });
212
+
213
+ it("accent on card has sufficient contrast", () => {
214
+ const ratio = contrastRatio(Colors.accent, Colors.card);
215
+ expect(ratio).toBeGreaterThan(3.0);
216
+ });
217
+ });
218
+
219
+ // ── Token completeness ──────────────────────────────────────────────────
220
+
221
+ describe("token completeness", () => {
222
+ it("all required semantic tokens exist", () => {
223
+ const required = [
224
+ "background",
225
+ "foreground",
226
+ "card",
227
+ "accent",
228
+ "border",
229
+ "muted",
230
+ "muted-foreground",
231
+ "dim",
232
+ "success",
233
+ "error",
234
+ "warning",
235
+ "info",
236
+ "highlight",
237
+ "backdrop",
238
+ ] as const;
239
+ for (const key of required) {
240
+ expect(Colors[key]).toBeDefined();
241
+ }
242
+ });
243
+
244
+ it("no two core semantic colors share the same hex value", () => {
245
+ const semanticKeys = [
246
+ "background",
247
+ "foreground",
248
+ "card",
249
+ "accent",
250
+ "border",
251
+ "muted",
252
+ "muted-foreground",
253
+ "dim",
254
+ "success",
255
+ "error",
256
+ "warning",
257
+ "info",
258
+ "backdrop",
259
+ ] as const;
260
+ const hexes = semanticKeys.map((k) => Colors[k]);
261
+ // Core tokens must be unique (aliases like highlight/text may duplicate)
262
+ const uniqueHexes = new Set(hexes);
263
+ expect(uniqueHexes.size).toBe(semanticKeys.length);
264
+ });
265
+ });
266
+
267
+ // ── Edge Cases ──────────────────────────────────────────────────────────
268
+
269
+ describe("edge cases", () => {
270
+ it("hexToRgb returns null for invalid hex", () => {
271
+ expect(hexToRgb("not-a-color")).toBeNull();
272
+ expect(hexToRgb("#FFF")).toBeNull(); // 3-digit not supported
273
+ expect(hexToRgb("#GGGGGG")).toBeNull();
274
+ expect(hexToRgb("")).toBeNull();
275
+ });
276
+
277
+ it("contrast ratio of same color is 1.0", () => {
278
+ expect(contrastRatio(Colors.background, Colors.background)).toBeCloseTo(
279
+ 1.0,
280
+ 1
281
+ );
282
+ });
283
+
284
+ it("contrast ratio is symmetric", () => {
285
+ const r1 = contrastRatio(Colors.foreground, Colors.background);
286
+ const r2 = contrastRatio(Colors.background, Colors.foreground);
287
+ expect(r1).toBeCloseTo(r2, 5);
288
+ });
289
+
290
+ it("white on black has maximum contrast (~21:1)", () => {
291
+ const ratio = contrastRatio("#FFFFFF", "#000000");
292
+ expect(ratio).toBeCloseTo(21, 0);
293
+ });
294
+ });
295
+ });
296
+
297
+ // ─── Status Color Mappings ───────────────────────────────────────────────────
298
+
299
+ describe("status color mappings", () => {
300
+ it("connection status maps to correct colors", () => {
301
+ expect(ConnectionStatusColor.connected).toBe(Colors.success);
302
+ expect(ConnectionStatusColor.polling).toBe(Colors.highlight);
303
+ expect(ConnectionStatusColor.reconnecting).toBe(Colors.warning);
304
+ expect(ConnectionStatusColor.offline).toBe(Colors.error);
305
+ });
306
+
307
+ it("alert severity maps to correct colors", () => {
308
+ expect(AlertSeverityColor.info).toBe(Colors.info);
309
+ expect(AlertSeverityColor.warning).toBe(Colors.warning);
310
+ expect(AlertSeverityColor.error).toBe(Colors.error);
311
+ expect(AlertSeverityColor.critical).toBe(Colors.error);
312
+ });
313
+
314
+ it("worker status maps to correct colors", () => {
315
+ expect(WorkerStatusColor.operational).toBe(Colors.success);
316
+ expect(WorkerStatusColor.degraded).toBe(Colors.warning);
317
+ expect(WorkerStatusColor.down).toBe(Colors.error);
318
+ });
319
+
320
+ it("log level maps to correct colors", () => {
321
+ expect(LogLevelColor.debug).toBe(Colors.muted);
322
+ expect(LogLevelColor.info).toBe(Colors.foreground);
323
+ expect(LogLevelColor.warn).toBe(Colors.warning);
324
+ expect(LogLevelColor.error).toBe(Colors.error);
325
+ });
326
+ });