tlc-claude-code 1.2.27 → 1.2.29
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/README.md +9 -4
- package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
- package/templates/docs-sync.yml +91 -0
|
@@ -0,0 +1,181 @@
|
|
|
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 { CommandPalette } from './CommandPalette.js';
|
|
5
|
+
const sampleCommands = [
|
|
6
|
+
{
|
|
7
|
+
id: 'tlc:plan',
|
|
8
|
+
name: 'Plan Phase',
|
|
9
|
+
description: 'Create implementation plan for a phase',
|
|
10
|
+
shortcut: 'p',
|
|
11
|
+
category: 'workflow',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'tlc:build',
|
|
15
|
+
name: 'Build Phase',
|
|
16
|
+
description: 'Implement phase with test-first approach',
|
|
17
|
+
shortcut: 'b',
|
|
18
|
+
category: 'workflow',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'tlc:verify',
|
|
22
|
+
name: 'Verify Phase',
|
|
23
|
+
description: 'Run human acceptance testing',
|
|
24
|
+
shortcut: 'v',
|
|
25
|
+
category: 'workflow',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'tlc:claim',
|
|
29
|
+
name: 'Claim Task',
|
|
30
|
+
description: 'Claim a task for yourself',
|
|
31
|
+
shortcut: 'c',
|
|
32
|
+
category: 'team',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'tlc:who',
|
|
36
|
+
name: 'Team Status',
|
|
37
|
+
description: 'Show team member status',
|
|
38
|
+
shortcut: 'w',
|
|
39
|
+
category: 'team',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'settings',
|
|
43
|
+
name: 'Open Settings',
|
|
44
|
+
description: 'View and edit configuration',
|
|
45
|
+
shortcut: ',',
|
|
46
|
+
category: 'general',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
describe('CommandPalette', () => {
|
|
50
|
+
describe('Search Input', () => {
|
|
51
|
+
it('shows search input', () => {
|
|
52
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
53
|
+
expect(lastFrame()).toMatch(/search|>|type/i);
|
|
54
|
+
});
|
|
55
|
+
it('shows current query', () => {
|
|
56
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "plan", onSelect: () => { } }));
|
|
57
|
+
expect(lastFrame()).toContain('plan');
|
|
58
|
+
});
|
|
59
|
+
it('shows cursor indicator', () => {
|
|
60
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
61
|
+
expect(lastFrame()).toMatch(/▏|│|_|\|/);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('Fuzzy Search', () => {
|
|
65
|
+
it('filters commands by name', () => {
|
|
66
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "plan", onSelect: () => { } }));
|
|
67
|
+
expect(lastFrame()).toContain('Plan Phase');
|
|
68
|
+
expect(lastFrame()).not.toContain('Team Status');
|
|
69
|
+
});
|
|
70
|
+
it('filters commands by description', () => {
|
|
71
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "test", onSelect: () => { } }));
|
|
72
|
+
expect(lastFrame()).toContain('Build Phase');
|
|
73
|
+
});
|
|
74
|
+
it('matches partial words', () => {
|
|
75
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "ver", onSelect: () => { } }));
|
|
76
|
+
expect(lastFrame()).toContain('Verify');
|
|
77
|
+
});
|
|
78
|
+
it('is case insensitive', () => {
|
|
79
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "PLAN", onSelect: () => { } }));
|
|
80
|
+
expect(lastFrame()).toContain('Plan Phase');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('Command Display', () => {
|
|
84
|
+
it('shows command name', () => {
|
|
85
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
86
|
+
expect(lastFrame()).toContain('Plan Phase');
|
|
87
|
+
expect(lastFrame()).toContain('Build Phase');
|
|
88
|
+
});
|
|
89
|
+
it('shows command description', () => {
|
|
90
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
91
|
+
expect(lastFrame()).toContain('Create implementation plan');
|
|
92
|
+
});
|
|
93
|
+
it('shows keyboard shortcut', () => {
|
|
94
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
95
|
+
expect(lastFrame()).toMatch(/\[p\]|p/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Category Grouping', () => {
|
|
99
|
+
it('groups commands by category', () => {
|
|
100
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
101
|
+
expect(lastFrame()).toMatch(/workflow/i);
|
|
102
|
+
expect(lastFrame()).toMatch(/team/i);
|
|
103
|
+
});
|
|
104
|
+
it('shows category headers', () => {
|
|
105
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
106
|
+
expect(lastFrame()).toMatch(/workflow|team|general/i);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('Recent Commands', () => {
|
|
110
|
+
it('shows recent commands section', () => {
|
|
111
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, recentIds: ['tlc:plan', 'tlc:build'], onSelect: () => { } }));
|
|
112
|
+
expect(lastFrame()).toMatch(/recent/i);
|
|
113
|
+
});
|
|
114
|
+
it('lists recent commands first', () => {
|
|
115
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, recentIds: ['tlc:claim'], onSelect: () => { } }));
|
|
116
|
+
const output = lastFrame() || '';
|
|
117
|
+
const claimIndex = output.indexOf('Claim Task');
|
|
118
|
+
const planIndex = output.indexOf('Plan Phase');
|
|
119
|
+
// Claim should appear before Plan due to recent
|
|
120
|
+
expect(claimIndex).toBeLessThan(planIndex);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('Selection', () => {
|
|
124
|
+
it('first command is selected by default', () => {
|
|
125
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
126
|
+
expect(lastFrame()).toContain('▶');
|
|
127
|
+
});
|
|
128
|
+
it('shows selection indicator', () => {
|
|
129
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
130
|
+
expect(lastFrame()).toMatch(/▶|→|>/);
|
|
131
|
+
});
|
|
132
|
+
it('calls onSelect on Enter', () => {
|
|
133
|
+
const onSelect = vi.fn();
|
|
134
|
+
render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: onSelect }));
|
|
135
|
+
// Selection happens on Enter key
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Navigation', () => {
|
|
139
|
+
it('shows navigation hints', () => {
|
|
140
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
141
|
+
expect(lastFrame()).toMatch(/↑|↓|j|k/);
|
|
142
|
+
});
|
|
143
|
+
it('shows execute hint', () => {
|
|
144
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
145
|
+
expect(lastFrame()).toMatch(/enter|execute|run/i);
|
|
146
|
+
});
|
|
147
|
+
it('shows close hint', () => {
|
|
148
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
|
|
149
|
+
expect(lastFrame()).toMatch(/esc|close/i);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('Empty State', () => {
|
|
153
|
+
it('shows message when no commands match', () => {
|
|
154
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "xyznonexistent", onSelect: () => { } }));
|
|
155
|
+
expect(lastFrame()).toMatch(/no.*command|no.*match|not.*found/i);
|
|
156
|
+
});
|
|
157
|
+
it('shows message when no commands', () => {
|
|
158
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: [], onSelect: () => { } }));
|
|
159
|
+
expect(lastFrame()).toMatch(/no.*command|empty/i);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('Callbacks', () => {
|
|
163
|
+
it('calls onQueryChange when typing', () => {
|
|
164
|
+
const onQueryChange = vi.fn();
|
|
165
|
+
render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { }, onQueryChange: onQueryChange }));
|
|
166
|
+
// Query change happens on input
|
|
167
|
+
});
|
|
168
|
+
it('calls onClose on Escape', () => {
|
|
169
|
+
const onClose = vi.fn();
|
|
170
|
+
render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { }, onClose: onClose }));
|
|
171
|
+
// Close happens on Esc key
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('Result Count', () => {
|
|
175
|
+
it('shows number of matching commands', () => {
|
|
176
|
+
const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "phase", onSelect: () => { } }));
|
|
177
|
+
// Should show count of matching commands
|
|
178
|
+
expect(lastFrame()).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ConnectionState = 'connected' | 'connecting' | 'disconnected';
|
|
2
|
+
export interface ConnectionStatusProps {
|
|
3
|
+
state: ConnectionState;
|
|
4
|
+
serverUrl?: string;
|
|
5
|
+
lastConnected?: string;
|
|
6
|
+
latencyMs?: number;
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
autoReconnect?: boolean;
|
|
9
|
+
reconnectIn?: number;
|
|
10
|
+
attemptCount?: number;
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
isActive?: boolean;
|
|
13
|
+
onReconnect?: () => void;
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function ConnectionStatus({ state, serverUrl, lastConnected, latencyMs, errorMessage, autoReconnect, reconnectIn, attemptCount, compact, isActive, onReconnect, onCancel, }: ConnectionStatusProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
const stateConfig = {
|
|
4
|
+
connected: { icon: '●', color: 'green', label: 'connected' },
|
|
5
|
+
connecting: { icon: '◐', color: 'yellow', label: 'connecting' },
|
|
6
|
+
disconnected: { icon: '○', color: 'red', label: 'disconnected' },
|
|
7
|
+
};
|
|
8
|
+
export function ConnectionStatus({ state, serverUrl, lastConnected, latencyMs, errorMessage, autoReconnect = false, reconnectIn, attemptCount, compact = false, isActive = true, onReconnect, onCancel, }) {
|
|
9
|
+
const config = stateConfig[state];
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (!isActive)
|
|
12
|
+
return;
|
|
13
|
+
// Manual reconnect
|
|
14
|
+
if (input === 'r' && state === 'disconnected' && onReconnect) {
|
|
15
|
+
onReconnect();
|
|
16
|
+
}
|
|
17
|
+
// Cancel reconnect
|
|
18
|
+
if (key.escape && autoReconnect && state === 'disconnected' && onCancel) {
|
|
19
|
+
onCancel();
|
|
20
|
+
}
|
|
21
|
+
}, { isActive });
|
|
22
|
+
// Compact mode
|
|
23
|
+
if (compact) {
|
|
24
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: config.color, children: config.icon }), _jsxs(Text, { color: config.color, children: [" ", config.label] }), state === 'connected' && latencyMs !== undefined && (_jsxs(Text, { dimColor: true, children: [" (", latencyMs, "ms)"] }))] }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: config.color, bold: true, children: [config.icon, " ", config.label] }), state === 'connected' && latencyMs !== undefined && (_jsxs(Text, { dimColor: true, children: [" (", latencyMs, "ms)"] })), state === 'connecting' && attemptCount !== undefined && (_jsxs(Text, { dimColor: true, children: [" (attempt ", attemptCount, ")"] }))] }), serverUrl && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Server: " }), _jsx(Text, { color: "cyan", children: serverUrl })] })), state !== 'connected' && lastConnected && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Last connected: " }), _jsx(Text, { children: lastConnected })] })), state === 'connected' && lastConnected && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Connected: " }), _jsx(Text, { children: lastConnected })] })), state === 'disconnected' && errorMessage && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", errorMessage] }) })), state === 'disconnected' && autoReconnect && reconnectIn !== undefined && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["Reconnecting in ", reconnectIn, "s..."] }) })), state === 'disconnected' && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["r reconnect", autoReconnect && ' • Esc cancel'] }) }))] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { ConnectionStatus } from './ConnectionStatus.js';
|
|
5
|
+
describe('ConnectionStatus', () => {
|
|
6
|
+
describe('Connected State', () => {
|
|
7
|
+
it('shows connected indicator', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected" }));
|
|
9
|
+
expect(lastFrame()).toMatch(/●|connected|online/i);
|
|
10
|
+
});
|
|
11
|
+
it('uses green color for connected', () => {
|
|
12
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected" }));
|
|
13
|
+
expect(lastFrame()).toContain('connected');
|
|
14
|
+
});
|
|
15
|
+
it('shows last connected time', () => {
|
|
16
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", lastConnected: "2 min ago" }));
|
|
17
|
+
expect(lastFrame()).toContain('2 min ago');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('Connecting State', () => {
|
|
21
|
+
it('shows connecting indicator', () => {
|
|
22
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connecting" }));
|
|
23
|
+
expect(lastFrame()).toMatch(/◐|connecting|…/i);
|
|
24
|
+
});
|
|
25
|
+
it('uses yellow color for connecting', () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connecting" }));
|
|
27
|
+
expect(lastFrame()).toContain('connecting');
|
|
28
|
+
});
|
|
29
|
+
it('shows attempt count', () => {
|
|
30
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connecting", attemptCount: 3 }));
|
|
31
|
+
expect(lastFrame()).toMatch(/3|attempt/i);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('Disconnected State', () => {
|
|
35
|
+
it('shows disconnected indicator', () => {
|
|
36
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected" }));
|
|
37
|
+
expect(lastFrame()).toMatch(/○|disconnected|offline/i);
|
|
38
|
+
});
|
|
39
|
+
it('uses red color for disconnected', () => {
|
|
40
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected" }));
|
|
41
|
+
expect(lastFrame()).toContain('disconnected');
|
|
42
|
+
});
|
|
43
|
+
it('shows last connected time', () => {
|
|
44
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", lastConnected: "5 min ago" }));
|
|
45
|
+
expect(lastFrame()).toContain('5 min ago');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Auto-Reconnect', () => {
|
|
49
|
+
it('shows countdown when auto-reconnect enabled', () => {
|
|
50
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: true, reconnectIn: 10 }));
|
|
51
|
+
expect(lastFrame()).toMatch(/10|sec|reconnect/i);
|
|
52
|
+
});
|
|
53
|
+
it('hides countdown when auto-reconnect disabled', () => {
|
|
54
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: false }));
|
|
55
|
+
const output = lastFrame() || '';
|
|
56
|
+
expect(output).not.toMatch(/\d+s/);
|
|
57
|
+
});
|
|
58
|
+
it('shows reconnecting message during countdown', () => {
|
|
59
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: true, reconnectIn: 5 }));
|
|
60
|
+
expect(lastFrame()).toMatch(/reconnect.*5|5.*reconnect/i);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('Manual Reconnect', () => {
|
|
64
|
+
it('shows manual reconnect hint', () => {
|
|
65
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected" }));
|
|
66
|
+
expect(lastFrame()).toMatch(/r|reconnect|retry/i);
|
|
67
|
+
});
|
|
68
|
+
it('calls onReconnect when triggered', () => {
|
|
69
|
+
const onReconnect = vi.fn();
|
|
70
|
+
render(_jsx(ConnectionStatus, { state: "disconnected", onReconnect: onReconnect }));
|
|
71
|
+
// Reconnect happens on 'r' key
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Error Message', () => {
|
|
75
|
+
it('shows error message when provided', () => {
|
|
76
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", errorMessage: "Connection refused" }));
|
|
77
|
+
expect(lastFrame()).toContain('Connection refused');
|
|
78
|
+
});
|
|
79
|
+
it('hides error when connected', () => {
|
|
80
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", errorMessage: "Old error" }));
|
|
81
|
+
const output = lastFrame() || '';
|
|
82
|
+
expect(output).not.toContain('Old error');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('Server URL', () => {
|
|
86
|
+
it('shows server URL', () => {
|
|
87
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", serverUrl: "wss://api.example.com" }));
|
|
88
|
+
expect(lastFrame()).toContain('api.example.com');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('Compact Mode', () => {
|
|
92
|
+
it('shows compact indicator', () => {
|
|
93
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", compact: true }));
|
|
94
|
+
expect(lastFrame()).toMatch(/●|connected/i);
|
|
95
|
+
});
|
|
96
|
+
it('hides details in compact mode', () => {
|
|
97
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", compact: true, lastConnected: "2 min ago" }));
|
|
98
|
+
const output = lastFrame() || '';
|
|
99
|
+
// Should be shorter than expanded mode
|
|
100
|
+
expect(output.length).toBeLessThan(100);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('Latency', () => {
|
|
104
|
+
it('shows latency when connected', () => {
|
|
105
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", latencyMs: 45 }));
|
|
106
|
+
expect(lastFrame()).toMatch(/45|ms|latency/i);
|
|
107
|
+
});
|
|
108
|
+
it('hides latency when disconnected', () => {
|
|
109
|
+
const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", latencyMs: 45 }));
|
|
110
|
+
const output = lastFrame() || '';
|
|
111
|
+
expect(output).not.toMatch(/45ms/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('Callbacks', () => {
|
|
115
|
+
it('calls onCancel to cancel reconnect', () => {
|
|
116
|
+
const onCancel = vi.fn();
|
|
117
|
+
render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: true, reconnectIn: 10, onCancel: onCancel }));
|
|
118
|
+
// Cancel happens on Esc key
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type DeviceType = 'phone' | 'tablet' | 'desktop' | 'custom';
|
|
2
|
+
export interface DeviceDimensions {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getDeviceDimensions(device: DeviceType, customWidth?: number, customHeight?: number): DeviceDimensions;
|
|
8
|
+
export declare function generateViewportUrl(baseUrl: string, device: DeviceType, customWidth?: number, customHeight?: number): string;
|
|
9
|
+
export interface DeviceFrameProps {
|
|
10
|
+
selectedDevice: DeviceType;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
customWidth?: number;
|
|
13
|
+
customHeight?: number;
|
|
14
|
+
showCustom?: boolean;
|
|
15
|
+
isActive?: boolean;
|
|
16
|
+
onSelect: (device: DeviceType) => void;
|
|
17
|
+
onCustomDimensions?: (width: number, height: number) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function DeviceFrame({ selectedDevice, baseUrl, customWidth, customHeight, showCustom, isActive, onSelect, onCustomDimensions, }: DeviceFrameProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
const devicePresets = {
|
|
4
|
+
phone: { width: 390, height: 844, label: 'Phone (iPhone 14)' },
|
|
5
|
+
tablet: { width: 820, height: 1180, label: 'Tablet (iPad Air)' },
|
|
6
|
+
desktop: { width: 1440, height: 900, label: 'Desktop (MacBook)' },
|
|
7
|
+
};
|
|
8
|
+
export function getDeviceDimensions(device, customWidth, customHeight) {
|
|
9
|
+
if (device === 'custom') {
|
|
10
|
+
return {
|
|
11
|
+
width: customWidth || 800,
|
|
12
|
+
height: customHeight || 600,
|
|
13
|
+
label: 'Custom',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return devicePresets[device];
|
|
17
|
+
}
|
|
18
|
+
export function generateViewportUrl(baseUrl, device, customWidth, customHeight) {
|
|
19
|
+
const dims = getDeviceDimensions(device, customWidth, customHeight);
|
|
20
|
+
const url = new URL(baseUrl);
|
|
21
|
+
url.searchParams.set('viewport', `${dims.width}x${dims.height}`);
|
|
22
|
+
return url.toString();
|
|
23
|
+
}
|
|
24
|
+
const devices = [
|
|
25
|
+
{ key: 'phone', num: '1' },
|
|
26
|
+
{ key: 'tablet', num: '2' },
|
|
27
|
+
{ key: 'desktop', num: '3' },
|
|
28
|
+
];
|
|
29
|
+
export function DeviceFrame({ selectedDevice, baseUrl, customWidth = 800, customHeight = 600, showCustom = false, isActive = true, onSelect, onCustomDimensions, }) {
|
|
30
|
+
const allDevices = showCustom
|
|
31
|
+
? [...devices, { key: 'custom', num: '4' }]
|
|
32
|
+
: devices;
|
|
33
|
+
useInput((input) => {
|
|
34
|
+
if (!isActive)
|
|
35
|
+
return;
|
|
36
|
+
if (input === '1')
|
|
37
|
+
onSelect('phone');
|
|
38
|
+
else if (input === '2')
|
|
39
|
+
onSelect('tablet');
|
|
40
|
+
else if (input === '3')
|
|
41
|
+
onSelect('desktop');
|
|
42
|
+
else if (input === '4' && showCustom)
|
|
43
|
+
onSelect('custom');
|
|
44
|
+
}, { isActive });
|
|
45
|
+
const currentDims = getDeviceDimensions(selectedDevice, customWidth, customHeight);
|
|
46
|
+
const viewportUrl = baseUrl ? generateViewportUrl(baseUrl, selectedDevice, customWidth, customHeight) : null;
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Device Size" }) }), allDevices.map((device) => {
|
|
48
|
+
const isSelected = device.key === selectedDevice;
|
|
49
|
+
const dims = getDeviceDimensions(device.key, customWidth, customHeight);
|
|
50
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), _jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: ["[", device.num, "]"] }), _jsxs(Text, { bold: isSelected, color: isSelected ? 'cyan' : 'white', children: [' ', device.key.charAt(0).toUpperCase() + device.key.slice(1)] }), _jsxs(Text, { dimColor: true, children: [' ', dims.width, " \u00D7 ", dims.height] })] }, device.key));
|
|
51
|
+
}), _jsxs(Box, { marginTop: 1, borderStyle: "single", paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Selected: " }), _jsx(Text, { color: "cyan", children: currentDims.label })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Viewport: " }), _jsxs(Text, { children: [currentDims.width, " \u00D7 ", currentDims.height] })] }), viewportUrl && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "URL: " }), _jsx(Text, { color: "blue", children: viewportUrl })] }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["1 phone \u2022 2 tablet \u2022 3 desktop", showCustom && ' • 4 custom'] }) })] }));
|
|
52
|
+
}
|
|
@@ -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 { DeviceFrame, getDeviceDimensions, generateViewportUrl } from './DeviceFrame.js';
|
|
5
|
+
describe('DeviceFrame', () => {
|
|
6
|
+
describe('Device Presets', () => {
|
|
7
|
+
it('shows phone option', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
9
|
+
expect(lastFrame()).toMatch(/phone|mobile/i);
|
|
10
|
+
});
|
|
11
|
+
it('shows tablet option', () => {
|
|
12
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
13
|
+
expect(lastFrame()).toMatch(/tablet|ipad/i);
|
|
14
|
+
});
|
|
15
|
+
it('shows desktop option', () => {
|
|
16
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
17
|
+
expect(lastFrame()).toMatch(/desktop|laptop/i);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('Dimensions Display', () => {
|
|
21
|
+
it('shows phone dimensions', () => {
|
|
22
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
23
|
+
expect(lastFrame()).toMatch(/375|390|414/); // Common phone widths
|
|
24
|
+
});
|
|
25
|
+
it('shows tablet dimensions', () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "tablet", onSelect: () => { } }));
|
|
27
|
+
expect(lastFrame()).toMatch(/768|820|1024/); // Common tablet widths
|
|
28
|
+
});
|
|
29
|
+
it('shows desktop dimensions', () => {
|
|
30
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "desktop", onSelect: () => { } }));
|
|
31
|
+
expect(lastFrame()).toMatch(/1280|1440|1920/); // Common desktop widths
|
|
32
|
+
});
|
|
33
|
+
it('shows width x height format', () => {
|
|
34
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
35
|
+
expect(lastFrame()).toMatch(/\d+\s*[x×]\s*\d+/i);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('Selection', () => {
|
|
39
|
+
it('highlights selected device', () => {
|
|
40
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "tablet", onSelect: () => { } }));
|
|
41
|
+
// Tablet should be highlighted
|
|
42
|
+
expect(lastFrame()).toContain('tablet');
|
|
43
|
+
});
|
|
44
|
+
it('shows selection indicator', () => {
|
|
45
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
46
|
+
expect(lastFrame()).toMatch(/▶|●|\[x\]|selected/i);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('Keyboard Selection', () => {
|
|
50
|
+
it('shows number hints', () => {
|
|
51
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
52
|
+
expect(lastFrame()).toContain('1');
|
|
53
|
+
expect(lastFrame()).toContain('2');
|
|
54
|
+
expect(lastFrame()).toContain('3');
|
|
55
|
+
});
|
|
56
|
+
it('calls onSelect with device type', () => {
|
|
57
|
+
const onSelect = vi.fn();
|
|
58
|
+
render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: onSelect }));
|
|
59
|
+
// Selection happens on number key press
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('URL Generation', () => {
|
|
63
|
+
it('shows URL with viewport params', () => {
|
|
64
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", baseUrl: "http://localhost:3000", onSelect: () => { } }));
|
|
65
|
+
// Should show URL with dimensions
|
|
66
|
+
expect(lastFrame()).toContain('localhost');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('Custom Dimensions', () => {
|
|
70
|
+
it('shows custom option', () => {
|
|
71
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { }, showCustom: true }));
|
|
72
|
+
expect(lastFrame()).toMatch(/custom|\d+.*×.*\d+/i);
|
|
73
|
+
});
|
|
74
|
+
it('shows custom dimensions when selected', () => {
|
|
75
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "custom", customWidth: 800, customHeight: 600, onSelect: () => { }, showCustom: true }));
|
|
76
|
+
expect(lastFrame()).toContain('800');
|
|
77
|
+
expect(lastFrame()).toContain('600');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('Navigation Hints', () => {
|
|
81
|
+
it('shows keyboard navigation hints', () => {
|
|
82
|
+
const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
|
|
83
|
+
expect(lastFrame()).toMatch(/1.*2.*3|phone.*tablet.*desktop/i);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('getDeviceDimensions', () => {
|
|
88
|
+
it('returns phone dimensions', () => {
|
|
89
|
+
const dims = getDeviceDimensions('phone');
|
|
90
|
+
expect(dims.width).toBe(390);
|
|
91
|
+
expect(dims.height).toBe(844);
|
|
92
|
+
});
|
|
93
|
+
it('returns tablet dimensions', () => {
|
|
94
|
+
const dims = getDeviceDimensions('tablet');
|
|
95
|
+
expect(dims.width).toBe(820);
|
|
96
|
+
expect(dims.height).toBe(1180);
|
|
97
|
+
});
|
|
98
|
+
it('returns desktop dimensions', () => {
|
|
99
|
+
const dims = getDeviceDimensions('desktop');
|
|
100
|
+
expect(dims.width).toBe(1440);
|
|
101
|
+
expect(dims.height).toBe(900);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('generateViewportUrl', () => {
|
|
105
|
+
it('adds viewport params to URL', () => {
|
|
106
|
+
const url = generateViewportUrl('http://localhost:3000', 'phone');
|
|
107
|
+
expect(url).toContain('viewport=');
|
|
108
|
+
});
|
|
109
|
+
it('preserves existing URL params', () => {
|
|
110
|
+
const url = generateViewportUrl('http://localhost:3000?foo=bar', 'tablet');
|
|
111
|
+
expect(url).toContain('foo=bar');
|
|
112
|
+
expect(url).toContain('viewport=');
|
|
113
|
+
});
|
|
114
|
+
it('includes width in viewport', () => {
|
|
115
|
+
const url = generateViewportUrl('http://localhost:3000', 'desktop');
|
|
116
|
+
expect(url).toContain('1440');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Environment = 'local' | 'vps' | 'staging' | 'production';
|
|
2
|
+
export interface EnvironmentBadgeProps {
|
|
3
|
+
environment: Environment;
|
|
4
|
+
branch?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
commit?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
connected?: boolean;
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function EnvironmentBadge({ environment, branch, version, commit, url, connected, compact, }: EnvironmentBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const envConfig = {
|
|
4
|
+
local: { label: 'local', color: 'green', icon: '◆' },
|
|
5
|
+
vps: { label: 'vps', color: 'cyan', icon: '◈' },
|
|
6
|
+
staging: { label: 'staging', color: 'yellow', icon: '◇' },
|
|
7
|
+
production: { label: 'PROD', color: 'red', icon: '⚠' },
|
|
8
|
+
};
|
|
9
|
+
export function EnvironmentBadge({ environment, branch, version, commit, url, connected, compact = false, }) {
|
|
10
|
+
const config = envConfig[environment];
|
|
11
|
+
const isProduction = environment === 'production';
|
|
12
|
+
if (compact) {
|
|
13
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: config.color, bold: true, children: ["[", config.label, "]"] }), connected !== undefined && (_jsx(Text, { color: connected ? 'green' : 'red', children: connected ? ' ●' : ' ○' }))] }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: config.color, bold: true, children: [config.icon, " ", config.label] }), isProduction && (_jsx(Text, { color: "red", bold: true, children: " \u26A0 CAUTION" })), connected !== undefined && (_jsx(Text, { color: connected ? 'green' : 'red', children: connected ? ' ● connected' : ' ○ disconnected' }))] }), _jsxs(Box, { marginTop: 1, children: [branch && (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { color: "cyan", children: branch })] })), version && (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { dimColor: true, children: "v" }), _jsx(Text, { children: version })] })), commit && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "@" }), _jsx(Text, { color: "yellow", children: commit.slice(0, 7) })] }))] }), url && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "url: " }), _jsx(Text, { color: "blue", children: url })] }))] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|