tlc-claude-code 1.4.1 → 1.4.4
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/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/dashboard/dist/components/accessibility.test.d.ts +1 -0
- package/dashboard/dist/components/accessibility.test.js +116 -0
- package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
- package/dashboard/dist/components/layout/MobileNav.js +31 -0
- package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
- package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
- package/dashboard/dist/components/performance.test.d.ts +1 -0
- package/dashboard/dist/components/performance.test.js +114 -0
- package/dashboard/dist/components/responsive.test.d.ts +1 -0
- package/dashboard/dist/components/responsive.test.js +114 -0
- package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
- package/dashboard/dist/components/ui/Dropdown.js +109 -0
- package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
- package/dashboard/dist/components/ui/Modal.d.ts +13 -0
- package/dashboard/dist/components/ui/Modal.js +25 -0
- package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Modal.test.js +91 -0
- package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
- package/dashboard/dist/components/ui/Skeleton.js +48 -0
- package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
- package/dashboard/dist/components/ui/Toast.d.ts +32 -0
- package/dashboard/dist/components/ui/Toast.js +21 -0
- package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Toast.test.js +118 -0
- package/dashboard/dist/hooks/useTheme.d.ts +37 -0
- package/dashboard/dist/hooks/useTheme.js +96 -0
- package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
- package/dashboard/dist/hooks/useTheme.test.js +94 -0
- package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
- package/dashboard/dist/hooks/useWebSocket.js +100 -0
- package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
- package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
- package/dashboard/dist/stores/projectStore.d.ts +44 -0
- package/dashboard/dist/stores/projectStore.js +76 -0
- package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
- package/dashboard/dist/stores/projectStore.test.js +114 -0
- package/dashboard/dist/stores/uiStore.d.ts +29 -0
- package/dashboard/dist/stores/uiStore.js +72 -0
- package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
- package/dashboard/dist/stores/uiStore.test.js +93 -0
- package/dashboard/package.json +3 -3
- package/docker-compose.dev.yml +6 -1
- package/package.json +5 -2
- package/server/dashboard/index.html +1336 -779
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- package/server/lib/router-setup-command.test.js +375 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { Skeleton } from './Skeleton.js';
|
|
5
|
+
describe('Skeleton', () => {
|
|
6
|
+
describe('Basic Rendering', () => {
|
|
7
|
+
it('renders placeholder content', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(Skeleton, {}));
|
|
9
|
+
expect(lastFrame()).toBeDefined();
|
|
10
|
+
// Should render some placeholder characters
|
|
11
|
+
expect(lastFrame()?.length).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
it('renders with custom width', () => {
|
|
14
|
+
const { lastFrame } = render(_jsx(Skeleton, { width: 20 }));
|
|
15
|
+
expect(lastFrame()).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
it('renders with custom height', () => {
|
|
18
|
+
const { lastFrame } = render(_jsx(Skeleton, { height: 3 }));
|
|
19
|
+
expect(lastFrame()).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('Variants', () => {
|
|
23
|
+
it('renders text variant', () => {
|
|
24
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "text" }));
|
|
25
|
+
expect(lastFrame()).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
it('renders text variant with multiple lines', () => {
|
|
28
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "text", lines: 3 }));
|
|
29
|
+
const output = lastFrame() || '';
|
|
30
|
+
// Should have multiple lines
|
|
31
|
+
const lineCount = output.split('\n').filter(l => l.trim()).length;
|
|
32
|
+
expect(lineCount).toBeGreaterThanOrEqual(1);
|
|
33
|
+
});
|
|
34
|
+
it('renders avatar variant (circle)', () => {
|
|
35
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "avatar" }));
|
|
36
|
+
expect(lastFrame()).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
it('renders avatar with size', () => {
|
|
39
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "avatar", size: 4 }));
|
|
40
|
+
expect(lastFrame()).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
it('renders card variant', () => {
|
|
43
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "card" }));
|
|
44
|
+
const output = lastFrame() || '';
|
|
45
|
+
// Card should have border
|
|
46
|
+
expect(output).toMatch(/[─│┌┐└┘╭╮╰╯┬┴┤├]/);
|
|
47
|
+
});
|
|
48
|
+
it('renders button variant', () => {
|
|
49
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "button" }));
|
|
50
|
+
const output = lastFrame() || '';
|
|
51
|
+
// Button should have brackets or border
|
|
52
|
+
expect(output).toMatch(/[\[\]│─]/);
|
|
53
|
+
});
|
|
54
|
+
it('renders table-row variant', () => {
|
|
55
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "table-row", columns: 3 }));
|
|
56
|
+
expect(lastFrame()).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('Animation', () => {
|
|
60
|
+
it('uses shimmer/pulse characters', () => {
|
|
61
|
+
const { lastFrame } = render(_jsx(Skeleton, {}));
|
|
62
|
+
const output = lastFrame() || '';
|
|
63
|
+
// Should use placeholder characters like ░, ▒, ▓, ▀, ▄, █, ▌, ▐, or spaces
|
|
64
|
+
expect(output).toMatch(/[░▒▓▀▄█▌▐ ]/);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('Rounded', () => {
|
|
68
|
+
it('accepts rounded prop', () => {
|
|
69
|
+
const { lastFrame } = render(_jsx(Skeleton, { rounded: true }));
|
|
70
|
+
expect(lastFrame()).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
it('uses rounded corners when rounded', () => {
|
|
73
|
+
const { lastFrame } = render(_jsx(Skeleton, { variant: "card", rounded: true }));
|
|
74
|
+
const output = lastFrame() || '';
|
|
75
|
+
// Should use rounded corner characters
|
|
76
|
+
expect(output).toMatch(/[╭╮╰╯┌┐└┘]/);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('Custom Styling', () => {
|
|
80
|
+
it('accepts className/style props', () => {
|
|
81
|
+
const { lastFrame } = render(_jsx(Skeleton, { width: 10, height: 2 }));
|
|
82
|
+
expect(lastFrame()).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('Skeleton.Text', () => {
|
|
87
|
+
it('renders text skeleton shorthand', () => {
|
|
88
|
+
const { lastFrame } = render(_jsx(Skeleton.Text, {}));
|
|
89
|
+
expect(lastFrame()).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
it('renders multiple lines', () => {
|
|
92
|
+
const { lastFrame } = render(_jsx(Skeleton.Text, { lines: 2 }));
|
|
93
|
+
expect(lastFrame()).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Skeleton.Avatar', () => {
|
|
97
|
+
it('renders avatar skeleton shorthand', () => {
|
|
98
|
+
const { lastFrame } = render(_jsx(Skeleton.Avatar, {}));
|
|
99
|
+
expect(lastFrame()).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
it('accepts size prop', () => {
|
|
102
|
+
const { lastFrame } = render(_jsx(Skeleton.Avatar, { size: 3 }));
|
|
103
|
+
expect(lastFrame()).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('Skeleton.Card', () => {
|
|
107
|
+
it('renders card skeleton shorthand', () => {
|
|
108
|
+
const { lastFrame } = render(_jsx(Skeleton.Card, {}));
|
|
109
|
+
expect(lastFrame()).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
it('accepts dimensions', () => {
|
|
112
|
+
const { lastFrame } = render(_jsx(Skeleton.Card, { width: 30, height: 5 }));
|
|
113
|
+
expect(lastFrame()).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('Skeleton.Button', () => {
|
|
117
|
+
it('renders button skeleton shorthand', () => {
|
|
118
|
+
const { lastFrame } = render(_jsx(Skeleton.Button, {}));
|
|
119
|
+
expect(lastFrame()).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
it('accepts width prop', () => {
|
|
122
|
+
const { lastFrame } = render(_jsx(Skeleton.Button, { width: 15 }));
|
|
123
|
+
expect(lastFrame()).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
|
|
2
|
+
export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
3
|
+
export interface ToastAction {
|
|
4
|
+
label: string;
|
|
5
|
+
onClick: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ToastProps {
|
|
8
|
+
variant: ToastVariant;
|
|
9
|
+
message: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
dismissable?: boolean;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
actions?: ToastAction[];
|
|
14
|
+
}
|
|
15
|
+
export interface ToastData {
|
|
16
|
+
id: string;
|
|
17
|
+
variant: ToastVariant;
|
|
18
|
+
message: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
dismissable?: boolean;
|
|
21
|
+
duration?: number;
|
|
22
|
+
actions?: ToastAction[];
|
|
23
|
+
}
|
|
24
|
+
export interface ToastContainerProps {
|
|
25
|
+
toasts: ToastData[];
|
|
26
|
+
onDismiss?: (id: string) => void;
|
|
27
|
+
position?: ToastPosition;
|
|
28
|
+
maxVisible?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function Toast({ variant, message, title, dismissable, onDismiss, actions, }: ToastProps): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export declare function ToastContainer({ toasts, onDismiss, position, maxVisible, }: ToastContainerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
32
|
+
export default Toast;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const variantConfig = {
|
|
4
|
+
success: { icon: '✓', color: 'green' },
|
|
5
|
+
error: { icon: '✕', color: 'red' },
|
|
6
|
+
warning: { icon: '⚠', color: 'yellow' },
|
|
7
|
+
info: { icon: 'ℹ', color: 'blue' },
|
|
8
|
+
};
|
|
9
|
+
export function Toast({ variant, message, title, dismissable = true, onDismiss, actions, }) {
|
|
10
|
+
const config = variantConfig[variant];
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: config.color, paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsxs(Text, { color: config.color, children: [config.icon, " "] }), title ? (_jsx(Text, { bold: true, color: config.color, children: title })) : (_jsx(Text, { bold: true, color: config.color, children: variant }))] }), dismissable && (_jsx(Text, { color: "gray", children: "\u00D7" }))] }), _jsx(Box, { marginTop: title ? 1 : 0, children: _jsx(Text, { children: message }) }), actions && actions.length > 0 && (_jsx(Box, { marginTop: 1, children: actions.map((action, idx) => (_jsx(Box, { marginRight: 2, children: _jsxs(Text, { color: "cyan", children: ["[", action.label, "]"] }) }, idx))) }))] }));
|
|
12
|
+
}
|
|
13
|
+
export function ToastContainer({ toasts, onDismiss, position = 'top-right', maxVisible = 5, }) {
|
|
14
|
+
if (toasts.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const visibleToasts = toasts.slice(0, maxVisible);
|
|
18
|
+
const hiddenCount = toasts.length - maxVisible;
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visibleToasts.map(toast => (_jsx(Toast, { variant: toast.variant, message: toast.message, title: toast.title, dismissable: toast.dismissable ?? true, onDismiss: () => onDismiss?.(toast.id), actions: toast.actions }, toast.id))), hiddenCount > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["+", hiddenCount, " more..."] }) }))] }));
|
|
20
|
+
}
|
|
21
|
+
export default Toast;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { Toast, ToastContainer } from './Toast.js';
|
|
5
|
+
describe('Toast', () => {
|
|
6
|
+
describe('Variants', () => {
|
|
7
|
+
it('renders success variant', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "success", message: "Success message" }));
|
|
9
|
+
expect(lastFrame()).toContain('Success message');
|
|
10
|
+
expect(lastFrame()).toMatch(/✓|✔|success/i);
|
|
11
|
+
});
|
|
12
|
+
it('renders error variant', () => {
|
|
13
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "error", message: "Error message" }));
|
|
14
|
+
expect(lastFrame()).toContain('Error message');
|
|
15
|
+
expect(lastFrame()).toMatch(/✕|✖|×|error/i);
|
|
16
|
+
});
|
|
17
|
+
it('renders warning variant', () => {
|
|
18
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "warning", message: "Warning message" }));
|
|
19
|
+
expect(lastFrame()).toContain('Warning message');
|
|
20
|
+
expect(lastFrame()).toMatch(/⚠|warning/i);
|
|
21
|
+
});
|
|
22
|
+
it('renders info variant', () => {
|
|
23
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Info message" }));
|
|
24
|
+
expect(lastFrame()).toContain('Info message');
|
|
25
|
+
expect(lastFrame()).toMatch(/ℹ|info/i);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('Dismiss', () => {
|
|
29
|
+
it('shows dismiss button when dismissable', () => {
|
|
30
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Test", dismissable: true }));
|
|
31
|
+
expect(lastFrame()).toMatch(/×|X|close|esc/i);
|
|
32
|
+
});
|
|
33
|
+
it('calls onDismiss when dismissed', () => {
|
|
34
|
+
const onDismiss = vi.fn();
|
|
35
|
+
render(_jsx(Toast, { variant: "info", message: "Test", dismissable: true, onDismiss: onDismiss }));
|
|
36
|
+
expect(onDismiss).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
it('hides dismiss button when not dismissable', () => {
|
|
39
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Test", dismissable: false }));
|
|
40
|
+
expect(lastFrame()).toContain('Test');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('Title', () => {
|
|
44
|
+
it('renders title when provided', () => {
|
|
45
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "success", message: "Body text", title: "My Title" }));
|
|
46
|
+
expect(lastFrame()).toContain('My Title');
|
|
47
|
+
expect(lastFrame()).toContain('Body text');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('Actions', () => {
|
|
51
|
+
it('renders action buttons', () => {
|
|
52
|
+
const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Test", actions: [{ label: 'Undo', onClick: () => { } }] }));
|
|
53
|
+
expect(lastFrame()).toContain('Undo');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('ToastContainer', () => {
|
|
58
|
+
describe('Rendering', () => {
|
|
59
|
+
it('renders multiple toasts', () => {
|
|
60
|
+
const toasts = [
|
|
61
|
+
{ id: '1', variant: 'success', message: 'Toast 1' },
|
|
62
|
+
{ id: '2', variant: 'error', message: 'Toast 2' },
|
|
63
|
+
];
|
|
64
|
+
const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts }));
|
|
65
|
+
expect(lastFrame()).toContain('Toast 1');
|
|
66
|
+
expect(lastFrame()).toContain('Toast 2');
|
|
67
|
+
});
|
|
68
|
+
it('renders empty when no toasts', () => {
|
|
69
|
+
const { lastFrame } = render(_jsx(ToastContainer, { toasts: [] }));
|
|
70
|
+
// Should render but be empty or minimal
|
|
71
|
+
expect(lastFrame()).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Position', () => {
|
|
75
|
+
it('renders in top-right by default', () => {
|
|
76
|
+
const toasts = [{ id: '1', variant: 'info', message: 'Test' }];
|
|
77
|
+
const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts }));
|
|
78
|
+
expect(lastFrame()).toContain('Test');
|
|
79
|
+
});
|
|
80
|
+
it('accepts position prop', () => {
|
|
81
|
+
const toasts = [{ id: '1', variant: 'info', message: 'Test' }];
|
|
82
|
+
const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts, position: "bottom-left" }));
|
|
83
|
+
expect(lastFrame()).toContain('Test');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('Stacking', () => {
|
|
87
|
+
it('stacks toasts vertically', () => {
|
|
88
|
+
const toasts = [
|
|
89
|
+
{ id: '1', variant: 'success', message: 'First' },
|
|
90
|
+
{ id: '2', variant: 'info', message: 'Second' },
|
|
91
|
+
{ id: '3', variant: 'warning', message: 'Third' },
|
|
92
|
+
];
|
|
93
|
+
const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts }));
|
|
94
|
+
const output = lastFrame() || '';
|
|
95
|
+
expect(output).toContain('First');
|
|
96
|
+
expect(output).toContain('Second');
|
|
97
|
+
expect(output).toContain('Third');
|
|
98
|
+
});
|
|
99
|
+
it('limits visible toasts', () => {
|
|
100
|
+
const toasts = Array.from({ length: 10 }, (_, i) => ({
|
|
101
|
+
id: String(i),
|
|
102
|
+
variant: 'info',
|
|
103
|
+
message: `Toast ${i}`,
|
|
104
|
+
}));
|
|
105
|
+
const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts, maxVisible: 3 }));
|
|
106
|
+
// Should show max toasts or "+N more" indicator
|
|
107
|
+
expect(lastFrame()).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('Callbacks', () => {
|
|
111
|
+
it('calls onDismiss with toast id', () => {
|
|
112
|
+
const onDismiss = vi.fn();
|
|
113
|
+
const toasts = [{ id: '1', variant: 'info', message: 'Test' }];
|
|
114
|
+
render(_jsx(ToastContainer, { toasts: toasts, onDismiss: onDismiss }));
|
|
115
|
+
expect(onDismiss).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export type Theme = 'dark' | 'light';
|
|
3
|
+
export interface ThemeColors {
|
|
4
|
+
bg: {
|
|
5
|
+
primary: string;
|
|
6
|
+
secondary: string;
|
|
7
|
+
tertiary: string;
|
|
8
|
+
};
|
|
9
|
+
text: {
|
|
10
|
+
primary: string;
|
|
11
|
+
secondary: string;
|
|
12
|
+
muted: string;
|
|
13
|
+
};
|
|
14
|
+
accent: string;
|
|
15
|
+
border: string;
|
|
16
|
+
success: string;
|
|
17
|
+
warning: string;
|
|
18
|
+
error: string;
|
|
19
|
+
info: string;
|
|
20
|
+
}
|
|
21
|
+
interface ThemeContextValue {
|
|
22
|
+
theme: Theme;
|
|
23
|
+
isDark: boolean;
|
|
24
|
+
isLight: boolean;
|
|
25
|
+
colors: ThemeColors;
|
|
26
|
+
toggleTheme: () => void;
|
|
27
|
+
setTheme: (theme: Theme) => void;
|
|
28
|
+
useSystemTheme: boolean;
|
|
29
|
+
setUseSystemTheme: (use: boolean) => void;
|
|
30
|
+
}
|
|
31
|
+
interface ThemeProviderProps {
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
defaultTheme?: Theme;
|
|
34
|
+
}
|
|
35
|
+
export declare function ThemeProvider({ children, defaultTheme }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
export declare function useTheme(): ThemeContextValue;
|
|
37
|
+
export default useTheme;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
const darkColors = {
|
|
4
|
+
bg: {
|
|
5
|
+
primary: '#0a0a0b',
|
|
6
|
+
secondary: '#141416',
|
|
7
|
+
tertiary: '#1e1e21',
|
|
8
|
+
},
|
|
9
|
+
text: {
|
|
10
|
+
primary: '#fafafa',
|
|
11
|
+
secondary: '#a1a1aa',
|
|
12
|
+
muted: '#71717a',
|
|
13
|
+
},
|
|
14
|
+
accent: '#3b82f6',
|
|
15
|
+
border: '#27272a',
|
|
16
|
+
success: '#22c55e',
|
|
17
|
+
warning: '#eab308',
|
|
18
|
+
error: '#ef4444',
|
|
19
|
+
info: '#06b6d4',
|
|
20
|
+
};
|
|
21
|
+
const lightColors = {
|
|
22
|
+
bg: {
|
|
23
|
+
primary: '#ffffff',
|
|
24
|
+
secondary: '#f4f4f5',
|
|
25
|
+
tertiary: '#e4e4e7',
|
|
26
|
+
},
|
|
27
|
+
text: {
|
|
28
|
+
primary: '#09090b',
|
|
29
|
+
secondary: '#52525b',
|
|
30
|
+
muted: '#a1a1aa',
|
|
31
|
+
},
|
|
32
|
+
accent: '#2563eb',
|
|
33
|
+
border: '#e4e4e7',
|
|
34
|
+
success: '#16a34a',
|
|
35
|
+
warning: '#ca8a04',
|
|
36
|
+
error: '#dc2626',
|
|
37
|
+
info: '#0891b2',
|
|
38
|
+
};
|
|
39
|
+
const ThemeContext = createContext(null);
|
|
40
|
+
export function ThemeProvider({ children, defaultTheme = 'dark' }) {
|
|
41
|
+
const [theme, setThemeState] = useState(defaultTheme);
|
|
42
|
+
const [useSystemTheme, setUseSystemTheme] = useState(false);
|
|
43
|
+
const isDark = theme === 'dark';
|
|
44
|
+
const isLight = theme === 'light';
|
|
45
|
+
const colors = isDark ? darkColors : lightColors;
|
|
46
|
+
const toggleTheme = useCallback(() => {
|
|
47
|
+
setThemeState(prev => (prev === 'dark' ? 'light' : 'dark'));
|
|
48
|
+
}, []);
|
|
49
|
+
const setTheme = useCallback((newTheme) => {
|
|
50
|
+
setThemeState(newTheme);
|
|
51
|
+
}, []);
|
|
52
|
+
// Persist to localStorage if available
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (typeof localStorage !== 'undefined' && !useSystemTheme) {
|
|
55
|
+
localStorage.setItem('tlc-theme', theme);
|
|
56
|
+
}
|
|
57
|
+
}, [theme, useSystemTheme]);
|
|
58
|
+
// Load from localStorage on mount
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (typeof localStorage !== 'undefined') {
|
|
61
|
+
const saved = localStorage.getItem('tlc-theme');
|
|
62
|
+
if (saved) {
|
|
63
|
+
setThemeState(saved);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
const value = {
|
|
68
|
+
theme,
|
|
69
|
+
isDark,
|
|
70
|
+
isLight,
|
|
71
|
+
colors,
|
|
72
|
+
toggleTheme,
|
|
73
|
+
setTheme,
|
|
74
|
+
useSystemTheme,
|
|
75
|
+
setUseSystemTheme,
|
|
76
|
+
};
|
|
77
|
+
return (_jsx(ThemeContext.Provider, { value: value, children: children }));
|
|
78
|
+
}
|
|
79
|
+
export function useTheme() {
|
|
80
|
+
const context = useContext(ThemeContext);
|
|
81
|
+
if (!context) {
|
|
82
|
+
// Return default values if used outside provider
|
|
83
|
+
return {
|
|
84
|
+
theme: 'dark',
|
|
85
|
+
isDark: true,
|
|
86
|
+
isLight: false,
|
|
87
|
+
colors: darkColors,
|
|
88
|
+
toggleTheme: () => { },
|
|
89
|
+
setTheme: () => { },
|
|
90
|
+
useSystemTheme: false,
|
|
91
|
+
setUseSystemTheme: () => { },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return context;
|
|
95
|
+
}
|
|
96
|
+
export default useTheme;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { Text } from 'ink';
|
|
5
|
+
import { ThemeProvider, useTheme } from './useTheme.js';
|
|
6
|
+
// Test component that uses the hook
|
|
7
|
+
function ThemeConsumer() {
|
|
8
|
+
const { theme, isDark, isLight, colors, useSystemTheme } = useTheme();
|
|
9
|
+
return (_jsx(Text, { children: `theme:${theme}|isDark:${String(isDark)}|isLight:${String(isLight)}|accent:${colors.accent}|useSystem:${String(useSystemTheme)}` }));
|
|
10
|
+
}
|
|
11
|
+
function LightThemeConsumer() {
|
|
12
|
+
const { theme, isDark, isLight } = useTheme();
|
|
13
|
+
return (_jsx(Text, { children: `theme:${theme}|isDark:${String(isDark)}|isLight:${String(isLight)}` }));
|
|
14
|
+
}
|
|
15
|
+
describe('useTheme', () => {
|
|
16
|
+
describe('Initial State', () => {
|
|
17
|
+
it('returns theme object', () => {
|
|
18
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
19
|
+
expect(lastFrame()).toContain('theme:');
|
|
20
|
+
});
|
|
21
|
+
it('defaults to dark theme', () => {
|
|
22
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
23
|
+
expect(lastFrame()).toContain('theme:dark');
|
|
24
|
+
});
|
|
25
|
+
it('returns isDark helper', () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
27
|
+
expect(lastFrame()).toContain('isDark:true');
|
|
28
|
+
});
|
|
29
|
+
it('returns isLight helper', () => {
|
|
30
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
31
|
+
expect(lastFrame()).toContain('isLight:false');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('Default Theme Override', () => {
|
|
35
|
+
it('accepts light as default theme', () => {
|
|
36
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { defaultTheme: "light", children: _jsx(LightThemeConsumer, {}) }));
|
|
37
|
+
expect(lastFrame()).toContain('theme:light');
|
|
38
|
+
});
|
|
39
|
+
it('sets isDark false when light theme', () => {
|
|
40
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { defaultTheme: "light", children: _jsx(LightThemeConsumer, {}) }));
|
|
41
|
+
expect(lastFrame()).toContain('isDark:false');
|
|
42
|
+
});
|
|
43
|
+
it('sets isLight true when light theme', () => {
|
|
44
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { defaultTheme: "light", children: _jsx(LightThemeConsumer, {}) }));
|
|
45
|
+
expect(lastFrame()).toContain('isLight:true');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Theme Colors', () => {
|
|
49
|
+
it('provides color tokens', () => {
|
|
50
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
51
|
+
expect(lastFrame()).toContain('accent:');
|
|
52
|
+
});
|
|
53
|
+
it('provides accent color for dark theme', () => {
|
|
54
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
55
|
+
expect(lastFrame()).toContain('accent:#3b82f6');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('System Preference', () => {
|
|
59
|
+
it('provides useSystemTheme option', () => {
|
|
60
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
61
|
+
expect(lastFrame()).toContain('useSystem:');
|
|
62
|
+
});
|
|
63
|
+
it('defaults useSystemTheme to false', () => {
|
|
64
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
|
|
65
|
+
expect(lastFrame()).toContain('useSystem:false');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('Hook Functions', () => {
|
|
69
|
+
it('provides toggleTheme function', () => {
|
|
70
|
+
const TestFunctions = () => {
|
|
71
|
+
const { toggleTheme } = useTheme();
|
|
72
|
+
return _jsx(Text, { children: `hasToggle:${typeof toggleTheme === 'function'}` });
|
|
73
|
+
};
|
|
74
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(TestFunctions, {}) }));
|
|
75
|
+
expect(lastFrame()).toContain('hasToggle:true');
|
|
76
|
+
});
|
|
77
|
+
it('provides setTheme function', () => {
|
|
78
|
+
const TestFunctions = () => {
|
|
79
|
+
const { setTheme } = useTheme();
|
|
80
|
+
return _jsx(Text, { children: `hasSetTheme:${typeof setTheme === 'function'}` });
|
|
81
|
+
};
|
|
82
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(TestFunctions, {}) }));
|
|
83
|
+
expect(lastFrame()).toContain('hasSetTheme:true');
|
|
84
|
+
});
|
|
85
|
+
it('provides setUseSystemTheme function', () => {
|
|
86
|
+
const TestFunctions = () => {
|
|
87
|
+
const { setUseSystemTheme } = useTheme();
|
|
88
|
+
return _jsx(Text, { children: `hasSetSystem:${typeof setUseSystemTheme === 'function'}` });
|
|
89
|
+
};
|
|
90
|
+
const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(TestFunctions, {}) }));
|
|
91
|
+
expect(lastFrame()).toContain('hasSetSystem:true');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
2
|
+
export interface WebSocketMessage {
|
|
3
|
+
type: string;
|
|
4
|
+
channel?: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface UseWebSocketReturn {
|
|
8
|
+
status: ConnectionStatus;
|
|
9
|
+
isConnected: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
send: (message: WebSocketMessage) => void;
|
|
12
|
+
subscribe: (channel: string, handler: (data: unknown) => void) => () => void;
|
|
13
|
+
reconnect: () => void;
|
|
14
|
+
disconnect: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function useWebSocket(url: string): UseWebSocketReturn;
|
|
17
|
+
export default useWebSocket;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
export function useWebSocket(url) {
|
|
3
|
+
const [status, setStatus] = useState('connecting');
|
|
4
|
+
const [error, setError] = useState(null);
|
|
5
|
+
const wsRef = useRef(null);
|
|
6
|
+
const handlersRef = useRef(new Map());
|
|
7
|
+
const reconnectTimeoutRef = useRef();
|
|
8
|
+
const connect = useCallback(() => {
|
|
9
|
+
try {
|
|
10
|
+
setStatus('connecting');
|
|
11
|
+
setError(null);
|
|
12
|
+
const ws = new WebSocket(url);
|
|
13
|
+
wsRef.current = ws;
|
|
14
|
+
ws.onopen = () => {
|
|
15
|
+
setStatus('connected');
|
|
16
|
+
setError(null);
|
|
17
|
+
};
|
|
18
|
+
ws.onclose = () => {
|
|
19
|
+
setStatus('disconnected');
|
|
20
|
+
wsRef.current = null;
|
|
21
|
+
};
|
|
22
|
+
ws.onerror = (event) => {
|
|
23
|
+
setStatus('error');
|
|
24
|
+
setError(new Error('WebSocket error'));
|
|
25
|
+
};
|
|
26
|
+
ws.onmessage = (event) => {
|
|
27
|
+
try {
|
|
28
|
+
const message = JSON.parse(event.data);
|
|
29
|
+
const channel = message.channel || message.type;
|
|
30
|
+
const handlers = handlersRef.current.get(channel);
|
|
31
|
+
if (handlers) {
|
|
32
|
+
handlers.forEach(handler => handler(message.data));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// Ignore parse errors
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
setStatus('error');
|
|
42
|
+
setError(e instanceof Error ? e : new Error('Failed to connect'));
|
|
43
|
+
}
|
|
44
|
+
}, [url]);
|
|
45
|
+
const disconnect = useCallback(() => {
|
|
46
|
+
if (reconnectTimeoutRef.current) {
|
|
47
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
48
|
+
}
|
|
49
|
+
if (wsRef.current) {
|
|
50
|
+
wsRef.current.close();
|
|
51
|
+
wsRef.current = null;
|
|
52
|
+
}
|
|
53
|
+
setStatus('disconnected');
|
|
54
|
+
}, []);
|
|
55
|
+
const reconnect = useCallback(() => {
|
|
56
|
+
disconnect();
|
|
57
|
+
connect();
|
|
58
|
+
}, [connect, disconnect]);
|
|
59
|
+
const send = useCallback((message) => {
|
|
60
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
61
|
+
wsRef.current.send(JSON.stringify(message));
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
const subscribe = useCallback((channel, handler) => {
|
|
65
|
+
if (!handlersRef.current.has(channel)) {
|
|
66
|
+
handlersRef.current.set(channel, new Set());
|
|
67
|
+
}
|
|
68
|
+
handlersRef.current.get(channel).add(handler);
|
|
69
|
+
// Send subscribe message
|
|
70
|
+
send({ type: 'subscribe', channel });
|
|
71
|
+
// Return unsubscribe function
|
|
72
|
+
return () => {
|
|
73
|
+
const handlers = handlersRef.current.get(channel);
|
|
74
|
+
if (handlers) {
|
|
75
|
+
handlers.delete(handler);
|
|
76
|
+
if (handlers.size === 0) {
|
|
77
|
+
handlersRef.current.delete(channel);
|
|
78
|
+
send({ type: 'unsubscribe', channel });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, [send]);
|
|
83
|
+
// Connect on mount
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
connect();
|
|
86
|
+
return () => {
|
|
87
|
+
disconnect();
|
|
88
|
+
};
|
|
89
|
+
}, [connect, disconnect]);
|
|
90
|
+
return {
|
|
91
|
+
status,
|
|
92
|
+
isConnected: status === 'connected',
|
|
93
|
+
error,
|
|
94
|
+
send,
|
|
95
|
+
subscribe,
|
|
96
|
+
reconnect,
|
|
97
|
+
disconnect,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export default useWebSocket;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|