tlc-claude-code 1.4.2 → 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/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 +1 -1
- package/server/dashboard/index.html +1336 -779
|
@@ -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 {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { Text } from 'ink';
|
|
5
|
+
import { useWebSocket } from './useWebSocket.js';
|
|
6
|
+
// Mock WebSocket
|
|
7
|
+
class MockWebSocket {
|
|
8
|
+
url;
|
|
9
|
+
static CONNECTING = 0;
|
|
10
|
+
static OPEN = 1;
|
|
11
|
+
static CLOSING = 2;
|
|
12
|
+
static CLOSED = 3;
|
|
13
|
+
readyState = MockWebSocket.CONNECTING;
|
|
14
|
+
onopen = null;
|
|
15
|
+
onclose = null;
|
|
16
|
+
onmessage = null;
|
|
17
|
+
onerror = null;
|
|
18
|
+
constructor(url) {
|
|
19
|
+
this.url = url;
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
this.readyState = MockWebSocket.OPEN;
|
|
22
|
+
this.onopen?.(new Event('open'));
|
|
23
|
+
}, 10);
|
|
24
|
+
}
|
|
25
|
+
send = vi.fn();
|
|
26
|
+
close = vi.fn(() => {
|
|
27
|
+
this.readyState = MockWebSocket.CLOSED;
|
|
28
|
+
this.onclose?.(new CloseEvent('close'));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Test component that uses the hook
|
|
32
|
+
function WebSocketConsumer({ url }) {
|
|
33
|
+
const { status, isConnected, error, send, subscribe, reconnect, disconnect } = useWebSocket(url);
|
|
34
|
+
return (_jsxs(Text, { children: ["status:", status, "|connected:", String(isConnected), "|error:", error ? error.message : 'null'] }));
|
|
35
|
+
}
|
|
36
|
+
describe('useWebSocket', () => {
|
|
37
|
+
let originalWebSocket;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
originalWebSocket = global.WebSocket;
|
|
40
|
+
global.WebSocket = MockWebSocket;
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
global.WebSocket = originalWebSocket;
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
describe('Connection', () => {
|
|
47
|
+
it('connects on mount', () => {
|
|
48
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
49
|
+
// Initially connecting
|
|
50
|
+
expect(lastFrame()).toContain('status:');
|
|
51
|
+
});
|
|
52
|
+
it('provides connection status', () => {
|
|
53
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
54
|
+
expect(lastFrame()).toContain('status:');
|
|
55
|
+
});
|
|
56
|
+
it('starts in connecting state', () => {
|
|
57
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
58
|
+
expect(lastFrame()).toContain('status:connecting');
|
|
59
|
+
});
|
|
60
|
+
it('shows isConnected false initially', () => {
|
|
61
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
62
|
+
expect(lastFrame()).toContain('connected:false');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('Send Messages', () => {
|
|
66
|
+
it('provides send function via hook', () => {
|
|
67
|
+
const TestSend = () => {
|
|
68
|
+
const { send } = useWebSocket('ws://localhost:3147');
|
|
69
|
+
return _jsxs(Text, { children: ["hasSend:", String(typeof send === 'function')] });
|
|
70
|
+
};
|
|
71
|
+
const { lastFrame } = render(_jsx(TestSend, {}));
|
|
72
|
+
expect(lastFrame()).toContain('hasSend:true');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('Subscribe', () => {
|
|
76
|
+
it('provides subscribe function', () => {
|
|
77
|
+
const TestSubscribe = () => {
|
|
78
|
+
const { subscribe } = useWebSocket('ws://localhost:3147');
|
|
79
|
+
return _jsxs(Text, { children: ["hasSubscribe:", String(typeof subscribe === 'function')] });
|
|
80
|
+
};
|
|
81
|
+
const { lastFrame } = render(_jsx(TestSubscribe, {}));
|
|
82
|
+
expect(lastFrame()).toContain('hasSubscribe:true');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('Reconnection', () => {
|
|
86
|
+
it('provides reconnect function', () => {
|
|
87
|
+
const TestReconnect = () => {
|
|
88
|
+
const { reconnect } = useWebSocket('ws://localhost:3147');
|
|
89
|
+
return _jsxs(Text, { children: ["hasReconnect:", String(typeof reconnect === 'function')] });
|
|
90
|
+
};
|
|
91
|
+
const { lastFrame } = render(_jsx(TestReconnect, {}));
|
|
92
|
+
expect(lastFrame()).toContain('hasReconnect:true');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('Disconnect', () => {
|
|
96
|
+
it('provides disconnect function', () => {
|
|
97
|
+
const TestDisconnect = () => {
|
|
98
|
+
const { disconnect } = useWebSocket('ws://localhost:3147');
|
|
99
|
+
return _jsxs(Text, { children: ["hasDisconnect:", String(typeof disconnect === 'function')] });
|
|
100
|
+
};
|
|
101
|
+
const { lastFrame } = render(_jsx(TestDisconnect, {}));
|
|
102
|
+
expect(lastFrame()).toContain('hasDisconnect:true');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('Error Handling', () => {
|
|
106
|
+
it('provides error state', () => {
|
|
107
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
108
|
+
expect(lastFrame()).toContain('error:');
|
|
109
|
+
});
|
|
110
|
+
it('starts with no error', () => {
|
|
111
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
112
|
+
expect(lastFrame()).toContain('error:null');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface ProjectPhase {
|
|
2
|
+
current: number;
|
|
3
|
+
total: number;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProjectTests {
|
|
7
|
+
passing: number;
|
|
8
|
+
failing: number;
|
|
9
|
+
total: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Project {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
phase: ProjectPhase;
|
|
16
|
+
tests: ProjectTests;
|
|
17
|
+
coverage: number;
|
|
18
|
+
lastActivity: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProjectState {
|
|
21
|
+
projects: Project[];
|
|
22
|
+
selectedProject: Project | null;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface ProjectActions {
|
|
27
|
+
setProjects: (projects: Project[]) => void;
|
|
28
|
+
addProject: (project: Project) => void;
|
|
29
|
+
removeProject: (id: string) => void;
|
|
30
|
+
updateProject: (id: string, updates: Partial<Project>) => void;
|
|
31
|
+
selectProject: (id: string) => void;
|
|
32
|
+
clearSelection: () => void;
|
|
33
|
+
setLoading: (loading: boolean) => void;
|
|
34
|
+
setError: (error: string | null) => void;
|
|
35
|
+
clearError: () => void;
|
|
36
|
+
getFilteredProjects: (query: string) => Project[];
|
|
37
|
+
}
|
|
38
|
+
export interface ProjectStore {
|
|
39
|
+
getState: () => ProjectState & ProjectActions;
|
|
40
|
+
setState: (partial: Partial<ProjectState>) => void;
|
|
41
|
+
subscribe: (listener: (state: ProjectState & ProjectActions) => void) => () => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function createProjectStore(): ProjectStore;
|
|
44
|
+
export declare const projectStore: ProjectStore;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Project Store - manages project list and selection
|
|
2
|
+
export function createProjectStore() {
|
|
3
|
+
let state = {
|
|
4
|
+
projects: [],
|
|
5
|
+
selectedProject: null,
|
|
6
|
+
loading: false,
|
|
7
|
+
error: null,
|
|
8
|
+
};
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
const notify = () => {
|
|
11
|
+
const fullState = { ...state, ...actions };
|
|
12
|
+
listeners.forEach(listener => listener(fullState));
|
|
13
|
+
};
|
|
14
|
+
const actions = {
|
|
15
|
+
setProjects: (projects) => {
|
|
16
|
+
state = { ...state, projects };
|
|
17
|
+
notify();
|
|
18
|
+
},
|
|
19
|
+
addProject: (project) => {
|
|
20
|
+
state = { ...state, projects: [...state.projects, project] };
|
|
21
|
+
notify();
|
|
22
|
+
},
|
|
23
|
+
removeProject: (id) => {
|
|
24
|
+
state = { ...state, projects: state.projects.filter(p => p.id !== id) };
|
|
25
|
+
notify();
|
|
26
|
+
},
|
|
27
|
+
updateProject: (id, updates) => {
|
|
28
|
+
state = {
|
|
29
|
+
...state,
|
|
30
|
+
projects: state.projects.map(p => p.id === id ? { ...p, ...updates } : p),
|
|
31
|
+
};
|
|
32
|
+
notify();
|
|
33
|
+
},
|
|
34
|
+
selectProject: (id) => {
|
|
35
|
+
const project = state.projects.find(p => p.id === id) || null;
|
|
36
|
+
state = { ...state, selectedProject: project };
|
|
37
|
+
notify();
|
|
38
|
+
},
|
|
39
|
+
clearSelection: () => {
|
|
40
|
+
state = { ...state, selectedProject: null };
|
|
41
|
+
notify();
|
|
42
|
+
},
|
|
43
|
+
setLoading: (loading) => {
|
|
44
|
+
state = { ...state, loading };
|
|
45
|
+
notify();
|
|
46
|
+
},
|
|
47
|
+
setError: (error) => {
|
|
48
|
+
state = { ...state, error };
|
|
49
|
+
notify();
|
|
50
|
+
},
|
|
51
|
+
clearError: () => {
|
|
52
|
+
state = { ...state, error: null };
|
|
53
|
+
notify();
|
|
54
|
+
},
|
|
55
|
+
getFilteredProjects: (query) => {
|
|
56
|
+
if (!query)
|
|
57
|
+
return state.projects;
|
|
58
|
+
const lowerQuery = query.toLowerCase();
|
|
59
|
+
return state.projects.filter(p => p.name.toLowerCase().includes(lowerQuery) ||
|
|
60
|
+
p.description.toLowerCase().includes(lowerQuery));
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
getState: () => ({ ...state, ...actions }),
|
|
65
|
+
setState: (partial) => {
|
|
66
|
+
state = { ...state, ...partial };
|
|
67
|
+
notify();
|
|
68
|
+
},
|
|
69
|
+
subscribe: (listener) => {
|
|
70
|
+
listeners.add(listener);
|
|
71
|
+
return () => listeners.delete(listener);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Default singleton instance
|
|
76
|
+
export const projectStore = createProjectStore();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|