@vizzly-testing/cli 0.19.2 → 0.20.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +1 -1
- package/dist/client/index.js +0 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/finalize.js +41 -15
- package/dist/commands/login.js +7 -6
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +5 -4
- package/dist/commands/run.js +158 -90
- package/dist/commands/status.js +22 -18
- package/dist/commands/tdd.js +105 -78
- package/dist/commands/upload.js +61 -26
- package/dist/commands/whoami.js +4 -4
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/report-generator/core.js +315 -0
- package/dist/report-generator/index.js +8 -0
- package/dist/report-generator/operations.js +196 -0
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +80 -48
- package/dist/server-manager/core.js +183 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +208 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/static-report-generator.js +21 -163
- package/dist/services/test-runner.js +90 -249
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1081 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +4 -2
- package/dist/types/index.d.ts +5 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/config-schema.js +8 -3
- package/package.json +7 -12
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/tdd-service.js +0 -1429
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Service
|
|
3
|
-
* Manages project mappings and project-related operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
7
|
-
import { deleteProjectMapping, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* ProjectService for managing project mappings and operations
|
|
11
|
-
*/
|
|
12
|
-
export class ProjectService {
|
|
13
|
-
constructor(config, options = {}) {
|
|
14
|
-
this.config = config;
|
|
15
|
-
this.apiService = options.apiService;
|
|
16
|
-
this.authService = options.authService;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* List all project mappings
|
|
21
|
-
* @returns {Promise<Array>} Array of project mappings
|
|
22
|
-
*/
|
|
23
|
-
async listMappings() {
|
|
24
|
-
const mappings = await getProjectMappings();
|
|
25
|
-
|
|
26
|
-
// Convert object to array with directory path included
|
|
27
|
-
return Object.entries(mappings).map(([directory, data]) => ({
|
|
28
|
-
directory,
|
|
29
|
-
...data
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Get project mapping for a specific directory
|
|
35
|
-
* @param {string} directory - Directory path
|
|
36
|
-
* @returns {Promise<Object|null>} Project mapping or null
|
|
37
|
-
*/
|
|
38
|
-
async getMapping(directory) {
|
|
39
|
-
return getProjectMapping(directory);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create or update project mapping
|
|
44
|
-
* @param {string} directory - Directory path
|
|
45
|
-
* @param {Object} projectData - Project data
|
|
46
|
-
* @param {string} projectData.projectSlug - Project slug
|
|
47
|
-
* @param {string} projectData.organizationSlug - Organization slug
|
|
48
|
-
* @param {string} projectData.token - Project API token
|
|
49
|
-
* @param {string} [projectData.projectName] - Optional project name
|
|
50
|
-
* @returns {Promise<Object>} Created mapping
|
|
51
|
-
*/
|
|
52
|
-
async createMapping(directory, projectData) {
|
|
53
|
-
if (!directory) {
|
|
54
|
-
throw new VizzlyError('Directory path is required', 'INVALID_DIRECTORY');
|
|
55
|
-
}
|
|
56
|
-
if (!projectData.projectSlug) {
|
|
57
|
-
throw new VizzlyError('Project slug is required', 'INVALID_PROJECT_DATA');
|
|
58
|
-
}
|
|
59
|
-
if (!projectData.organizationSlug) {
|
|
60
|
-
throw new VizzlyError('Organization slug is required', 'INVALID_PROJECT_DATA');
|
|
61
|
-
}
|
|
62
|
-
if (!projectData.token) {
|
|
63
|
-
throw new VizzlyError('Project token is required', 'INVALID_PROJECT_DATA');
|
|
64
|
-
}
|
|
65
|
-
await saveProjectMapping(directory, projectData);
|
|
66
|
-
return {
|
|
67
|
-
directory,
|
|
68
|
-
...projectData
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Remove project mapping
|
|
74
|
-
* @param {string} directory - Directory path
|
|
75
|
-
* @returns {Promise<void>}
|
|
76
|
-
*/
|
|
77
|
-
async removeMapping(directory) {
|
|
78
|
-
if (!directory) {
|
|
79
|
-
throw new VizzlyError('Directory path is required', 'INVALID_DIRECTORY');
|
|
80
|
-
}
|
|
81
|
-
await deleteProjectMapping(directory);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Switch project for current directory
|
|
86
|
-
* @param {string} projectSlug - Project slug
|
|
87
|
-
* @param {string} organizationSlug - Organization slug
|
|
88
|
-
* @param {string} token - Project token
|
|
89
|
-
* @returns {Promise<Object>} Updated mapping
|
|
90
|
-
*/
|
|
91
|
-
async switchProject(projectSlug, organizationSlug, token) {
|
|
92
|
-
const currentDir = process.cwd();
|
|
93
|
-
return this.createMapping(currentDir, {
|
|
94
|
-
projectSlug,
|
|
95
|
-
organizationSlug,
|
|
96
|
-
token
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* List all projects from API
|
|
102
|
-
* Uses OAuth authentication (authService) when available, falls back to API token
|
|
103
|
-
* @returns {Promise<Array>} Array of projects with organization info
|
|
104
|
-
*/
|
|
105
|
-
async listProjects() {
|
|
106
|
-
// Try OAuth-based request first (user login via device flow)
|
|
107
|
-
if (this.authService) {
|
|
108
|
-
try {
|
|
109
|
-
// First get the user's organizations via whoami
|
|
110
|
-
const whoami = await this.authService.authenticatedRequest('/api/auth/cli/whoami', {
|
|
111
|
-
method: 'GET'
|
|
112
|
-
});
|
|
113
|
-
const organizations = whoami.organizations || [];
|
|
114
|
-
if (organizations.length === 0) {
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Fetch projects for each organization
|
|
119
|
-
const allProjects = [];
|
|
120
|
-
for (const org of organizations) {
|
|
121
|
-
try {
|
|
122
|
-
const response = await this.authService.authenticatedRequest('/api/project', {
|
|
123
|
-
method: 'GET',
|
|
124
|
-
headers: {
|
|
125
|
-
'X-Organization': org.slug
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Add organization info to each project
|
|
130
|
-
const projects = (response.projects || []).map(project => ({
|
|
131
|
-
...project,
|
|
132
|
-
organizationSlug: org.slug,
|
|
133
|
-
organizationName: org.name
|
|
134
|
-
}));
|
|
135
|
-
allProjects.push(...projects);
|
|
136
|
-
} catch {
|
|
137
|
-
// Silently skip failed orgs
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return allProjects;
|
|
141
|
-
} catch {
|
|
142
|
-
// Fall back to API token
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Fall back to API token-based request (tokens are org-scoped, so no org header needed)
|
|
147
|
-
if (this.apiService) {
|
|
148
|
-
try {
|
|
149
|
-
const response = await this.apiService.request('/api/project', {
|
|
150
|
-
method: 'GET'
|
|
151
|
-
});
|
|
152
|
-
return response.projects || [];
|
|
153
|
-
} catch {
|
|
154
|
-
return [];
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// No authentication available
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Get project details
|
|
164
|
-
* @param {string} projectSlug - Project slug
|
|
165
|
-
* @param {string} organizationSlug - Organization slug
|
|
166
|
-
* @returns {Promise<Object>} Project details
|
|
167
|
-
*/
|
|
168
|
-
async getProject(projectSlug, organizationSlug) {
|
|
169
|
-
// Try OAuth-based request first
|
|
170
|
-
if (this.authService) {
|
|
171
|
-
try {
|
|
172
|
-
const response = await this.authService.authenticatedRequest(`/api/project/${projectSlug}`, {
|
|
173
|
-
method: 'GET',
|
|
174
|
-
headers: {
|
|
175
|
-
'X-Organization': organizationSlug
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
return response.project || response;
|
|
179
|
-
} catch {
|
|
180
|
-
// Fall back to API token
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Fall back to API token
|
|
185
|
-
if (this.apiService) {
|
|
186
|
-
try {
|
|
187
|
-
const response = await this.apiService.request(`/api/project/${projectSlug}`, {
|
|
188
|
-
method: 'GET',
|
|
189
|
-
headers: {
|
|
190
|
-
'X-Organization': organizationSlug
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
return response.project || response;
|
|
194
|
-
} catch (error) {
|
|
195
|
-
throw new VizzlyError(`Failed to fetch project: ${error.message}`, 'PROJECT_FETCH_FAILED', {
|
|
196
|
-
originalError: error
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
throw new VizzlyError('No authentication available', 'NO_AUTH_SERVICE');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Get recent builds for a project
|
|
205
|
-
* Uses OAuth authentication (authService) when available, falls back to API token
|
|
206
|
-
* @param {string} projectSlug - Project slug
|
|
207
|
-
* @param {string} organizationSlug - Organization slug
|
|
208
|
-
* @param {Object} options - Query options
|
|
209
|
-
* @param {number} [options.limit=10] - Number of builds to fetch
|
|
210
|
-
* @param {string} [options.branch] - Filter by branch
|
|
211
|
-
* @returns {Promise<Array>} Array of builds
|
|
212
|
-
*/
|
|
213
|
-
async getRecentBuilds(projectSlug, organizationSlug, options = {}) {
|
|
214
|
-
const queryParams = new globalThis.URLSearchParams();
|
|
215
|
-
if (options.limit) queryParams.append('limit', String(options.limit));
|
|
216
|
-
if (options.branch) queryParams.append('branch', options.branch);
|
|
217
|
-
const query = queryParams.toString();
|
|
218
|
-
const url = `/api/build/${projectSlug}${query ? `?${query}` : ''}`;
|
|
219
|
-
|
|
220
|
-
// Try OAuth-based request first (user login via device flow)
|
|
221
|
-
if (this.authService) {
|
|
222
|
-
try {
|
|
223
|
-
const response = await this.authService.authenticatedRequest(url, {
|
|
224
|
-
method: 'GET',
|
|
225
|
-
headers: {
|
|
226
|
-
'X-Organization': organizationSlug
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
return response.builds || [];
|
|
230
|
-
} catch {
|
|
231
|
-
// Fall back to API token
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Fall back to API token-based request
|
|
236
|
-
if (this.apiService) {
|
|
237
|
-
try {
|
|
238
|
-
const response = await this.apiService.request(url, {
|
|
239
|
-
method: 'GET',
|
|
240
|
-
headers: {
|
|
241
|
-
'X-Organization': organizationSlug
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
return response.builds || [];
|
|
245
|
-
} catch {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// No authentication available
|
|
251
|
-
return [];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Create a project token
|
|
256
|
-
* @param {string} projectSlug - Project slug
|
|
257
|
-
* @param {string} organizationSlug - Organization slug
|
|
258
|
-
* @param {Object} tokenData - Token data
|
|
259
|
-
* @param {string} tokenData.name - Token name
|
|
260
|
-
* @param {string} [tokenData.description] - Token description
|
|
261
|
-
* @returns {Promise<Object>} Created token
|
|
262
|
-
*/
|
|
263
|
-
async createProjectToken(projectSlug, organizationSlug, tokenData) {
|
|
264
|
-
if (!this.apiService) {
|
|
265
|
-
throw new VizzlyError('API service not available', 'NO_API_SERVICE');
|
|
266
|
-
}
|
|
267
|
-
try {
|
|
268
|
-
const response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, {
|
|
269
|
-
method: 'POST',
|
|
270
|
-
headers: {
|
|
271
|
-
'Content-Type': 'application/json'
|
|
272
|
-
},
|
|
273
|
-
body: JSON.stringify(tokenData)
|
|
274
|
-
});
|
|
275
|
-
return response.token;
|
|
276
|
-
} catch (error) {
|
|
277
|
-
throw new VizzlyError(`Failed to create project token: ${error.message}`, 'TOKEN_CREATE_FAILED', {
|
|
278
|
-
originalError: error
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* List project tokens
|
|
285
|
-
* @param {string} projectSlug - Project slug
|
|
286
|
-
* @param {string} organizationSlug - Organization slug
|
|
287
|
-
* @returns {Promise<Array>} Array of tokens
|
|
288
|
-
*/
|
|
289
|
-
async listProjectTokens(projectSlug, organizationSlug) {
|
|
290
|
-
if (!this.apiService) {
|
|
291
|
-
throw new VizzlyError('API service not available', 'NO_API_SERVICE');
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
const response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, {
|
|
295
|
-
method: 'GET'
|
|
296
|
-
});
|
|
297
|
-
return response.tokens || [];
|
|
298
|
-
} catch (error) {
|
|
299
|
-
throw new VizzlyError(`Failed to fetch project tokens: ${error.message}`, 'TOKENS_FETCH_FAILED', {
|
|
300
|
-
originalError: error
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Revoke a project token
|
|
307
|
-
* @param {string} projectSlug - Project slug
|
|
308
|
-
* @param {string} organizationSlug - Organization slug
|
|
309
|
-
* @param {string} tokenId - Token ID
|
|
310
|
-
* @returns {Promise<void>}
|
|
311
|
-
*/
|
|
312
|
-
async revokeProjectToken(projectSlug, organizationSlug, tokenId) {
|
|
313
|
-
if (!this.apiService) {
|
|
314
|
-
throw new VizzlyError('API service not available', 'NO_API_SERVICE');
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens/${tokenId}`, {
|
|
318
|
-
method: 'DELETE'
|
|
319
|
-
});
|
|
320
|
-
} catch (error) {
|
|
321
|
-
throw new VizzlyError(`Failed to revoke project token: ${error.message}`, 'TOKEN_REVOKE_FAILED', {
|
|
322
|
-
originalError: error
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
* {
|
|
2
|
-
box-sizing: border-box;
|
|
3
|
-
margin: 0;
|
|
4
|
-
padding: 0;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
font-family:
|
|
9
|
-
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
10
|
-
line-height: 1.6;
|
|
11
|
-
color: #e2e8f0;
|
|
12
|
-
background: #0f172a;
|
|
13
|
-
min-height: 100vh;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.container {
|
|
17
|
-
max-width: 1200px;
|
|
18
|
-
margin: 0 auto;
|
|
19
|
-
padding: 20px;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.header {
|
|
23
|
-
background: #1e293b;
|
|
24
|
-
border-radius: 12px;
|
|
25
|
-
padding: 32px;
|
|
26
|
-
margin-bottom: 24px;
|
|
27
|
-
box-shadow:
|
|
28
|
-
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
29
|
-
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
30
|
-
border: 1px solid #334155;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.header h1 {
|
|
34
|
-
font-size: 2rem;
|
|
35
|
-
margin-bottom: 24px;
|
|
36
|
-
color: #f1f5f9;
|
|
37
|
-
font-weight: 600;
|
|
38
|
-
display: flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
gap: 12px;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.summary {
|
|
44
|
-
display: flex;
|
|
45
|
-
gap: 30px;
|
|
46
|
-
margin-bottom: 15px;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.stat {
|
|
50
|
-
display: flex;
|
|
51
|
-
flex-direction: column;
|
|
52
|
-
align-items: center;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.stat-number {
|
|
56
|
-
font-size: 2rem;
|
|
57
|
-
font-weight: bold;
|
|
58
|
-
color: #94a3b8;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.stat.passed .stat-number {
|
|
62
|
-
color: #22c55e;
|
|
63
|
-
}
|
|
64
|
-
.stat.failed .stat-number {
|
|
65
|
-
color: #f59e0b;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.stat-label {
|
|
69
|
-
font-size: 0.875rem;
|
|
70
|
-
color: #94a3b8;
|
|
71
|
-
text-transform: uppercase;
|
|
72
|
-
letter-spacing: 0.025em;
|
|
73
|
-
font-weight: 500;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.build-info {
|
|
77
|
-
color: #64748b;
|
|
78
|
-
font-size: 0.875rem;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.no-failures {
|
|
82
|
-
text-align: center;
|
|
83
|
-
padding: 48px;
|
|
84
|
-
background: #1e293b;
|
|
85
|
-
border-radius: 12px;
|
|
86
|
-
border: 1px solid #334155;
|
|
87
|
-
font-size: 1.125rem;
|
|
88
|
-
color: #22c55e;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.comparison {
|
|
92
|
-
background: #1e293b;
|
|
93
|
-
border-radius: 12px;
|
|
94
|
-
padding: 24px;
|
|
95
|
-
margin-bottom: 24px;
|
|
96
|
-
box-shadow:
|
|
97
|
-
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
98
|
-
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
99
|
-
border: 1px solid #334155;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.comparison-header {
|
|
103
|
-
display: flex;
|
|
104
|
-
justify-content: space-between;
|
|
105
|
-
align-items: center;
|
|
106
|
-
margin-bottom: 20px;
|
|
107
|
-
padding-bottom: 16px;
|
|
108
|
-
border-bottom: 1px solid #334155;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.comparison-header h3 {
|
|
112
|
-
margin: 0;
|
|
113
|
-
font-size: 1.25rem;
|
|
114
|
-
color: #f1f5f9;
|
|
115
|
-
font-weight: 600;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.comparison-meta {
|
|
119
|
-
display: flex;
|
|
120
|
-
gap: 16px;
|
|
121
|
-
font-size: 0.875rem;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.diff-status {
|
|
125
|
-
padding: 6px 12px;
|
|
126
|
-
background: rgba(245, 158, 11, 0.1);
|
|
127
|
-
color: #f59e0b;
|
|
128
|
-
border-radius: 6px;
|
|
129
|
-
font-weight: 500;
|
|
130
|
-
border: 1px solid rgba(245, 158, 11, 0.2);
|
|
131
|
-
font-size: 0.875rem;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.comparison-controls {
|
|
135
|
-
display: flex;
|
|
136
|
-
gap: 10px;
|
|
137
|
-
margin-bottom: 20px;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.view-mode-btn {
|
|
141
|
-
padding: 8px 16px;
|
|
142
|
-
border: 1px solid #475569;
|
|
143
|
-
background: #334155;
|
|
144
|
-
border-radius: 8px;
|
|
145
|
-
cursor: pointer;
|
|
146
|
-
font-size: 0.875rem;
|
|
147
|
-
font-weight: 500;
|
|
148
|
-
transition: all 0.2s;
|
|
149
|
-
color: #cbd5e1;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.view-mode-btn:hover {
|
|
153
|
-
background: #475569;
|
|
154
|
-
border-color: #64748b;
|
|
155
|
-
color: #e2e8f0;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.view-mode-btn.active {
|
|
159
|
-
background: #f59e0b;
|
|
160
|
-
color: #1e293b;
|
|
161
|
-
border-color: #f59e0b;
|
|
162
|
-
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.comparison-viewer {
|
|
166
|
-
position: relative;
|
|
167
|
-
border: 1px solid #334155;
|
|
168
|
-
border-radius: 8px;
|
|
169
|
-
overflow: hidden;
|
|
170
|
-
background: #0f172a;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.mode-container {
|
|
174
|
-
position: relative;
|
|
175
|
-
min-height: 200px;
|
|
176
|
-
text-align: center;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.mode-container img {
|
|
180
|
-
max-width: 100%;
|
|
181
|
-
height: auto;
|
|
182
|
-
display: block;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* Overlay Mode */
|
|
186
|
-
.overlay-container {
|
|
187
|
-
position: relative;
|
|
188
|
-
display: inline-block;
|
|
189
|
-
margin: 0 auto;
|
|
190
|
-
cursor: pointer;
|
|
191
|
-
max-width: 100%;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.overlay-container img {
|
|
195
|
-
max-width: 100%;
|
|
196
|
-
height: auto;
|
|
197
|
-
display: block;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.overlay-container .current-image {
|
|
201
|
-
position: relative;
|
|
202
|
-
z-index: 1;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.overlay-container .baseline-image {
|
|
206
|
-
position: absolute;
|
|
207
|
-
top: 0;
|
|
208
|
-
left: 0;
|
|
209
|
-
opacity: 0.5;
|
|
210
|
-
z-index: 2;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.overlay-container .diff-image {
|
|
214
|
-
position: absolute;
|
|
215
|
-
top: 0;
|
|
216
|
-
left: 0;
|
|
217
|
-
opacity: 0;
|
|
218
|
-
z-index: 3;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/* Side by Side Mode */
|
|
222
|
-
.side-by-side-container {
|
|
223
|
-
display: grid;
|
|
224
|
-
grid-template-columns: 1fr 1fr;
|
|
225
|
-
gap: 24px;
|
|
226
|
-
align-items: start;
|
|
227
|
-
padding: 16px;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
.side-by-side-image {
|
|
231
|
-
text-align: center;
|
|
232
|
-
flex: 1;
|
|
233
|
-
min-width: 200px;
|
|
234
|
-
max-width: 400px;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.side-by-side-image img {
|
|
238
|
-
width: 100%;
|
|
239
|
-
height: auto;
|
|
240
|
-
max-width: none;
|
|
241
|
-
border: 2px solid #475569;
|
|
242
|
-
border-radius: 8px;
|
|
243
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
244
|
-
transition: border-color 0.2s ease;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.side-by-side-image img:hover {
|
|
248
|
-
border-color: #f59e0b;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.side-by-side-image label {
|
|
252
|
-
display: block;
|
|
253
|
-
margin-top: 12px;
|
|
254
|
-
font-size: 0.875rem;
|
|
255
|
-
color: #94a3b8;
|
|
256
|
-
font-weight: 600;
|
|
257
|
-
text-transform: uppercase;
|
|
258
|
-
letter-spacing: 0.025em;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/* Onion Skin Mode */
|
|
262
|
-
.onion-container {
|
|
263
|
-
position: relative;
|
|
264
|
-
display: inline-block;
|
|
265
|
-
margin: 0 auto;
|
|
266
|
-
cursor: ew-resize;
|
|
267
|
-
user-select: none;
|
|
268
|
-
max-width: 100%;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
.onion-baseline {
|
|
272
|
-
max-width: 100%;
|
|
273
|
-
height: auto;
|
|
274
|
-
display: block;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
.onion-current {
|
|
278
|
-
position: absolute;
|
|
279
|
-
top: 0;
|
|
280
|
-
left: 0;
|
|
281
|
-
max-width: 100%;
|
|
282
|
-
height: auto;
|
|
283
|
-
clip-path: inset(0 50% 0 0);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.onion-divider {
|
|
287
|
-
position: absolute;
|
|
288
|
-
top: 0;
|
|
289
|
-
left: 50%;
|
|
290
|
-
width: 2px;
|
|
291
|
-
height: 100%;
|
|
292
|
-
background: #f59e0b;
|
|
293
|
-
transform: translateX(-50%);
|
|
294
|
-
z-index: 10;
|
|
295
|
-
pointer-events: none;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.onion-divider::before {
|
|
299
|
-
content: "";
|
|
300
|
-
position: absolute;
|
|
301
|
-
top: 50%;
|
|
302
|
-
left: 50%;
|
|
303
|
-
transform: translate(-50%, -50%);
|
|
304
|
-
width: 20px;
|
|
305
|
-
height: 20px;
|
|
306
|
-
border-radius: 50%;
|
|
307
|
-
background: #f59e0b;
|
|
308
|
-
border: 2px solid #1e293b;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.onion-divider::after {
|
|
312
|
-
content: "⟷";
|
|
313
|
-
position: absolute;
|
|
314
|
-
top: 50%;
|
|
315
|
-
left: 50%;
|
|
316
|
-
transform: translate(-50%, -50%);
|
|
317
|
-
color: #1e293b;
|
|
318
|
-
font-size: 12px;
|
|
319
|
-
font-weight: bold;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/* Toggle Mode */
|
|
323
|
-
.toggle-container {
|
|
324
|
-
display: inline-block;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
.toggle-container img {
|
|
328
|
-
max-width: 100%;
|
|
329
|
-
width: auto;
|
|
330
|
-
height: auto;
|
|
331
|
-
cursor: pointer;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.error {
|
|
335
|
-
color: #ef4444;
|
|
336
|
-
text-align: center;
|
|
337
|
-
padding: 40px;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/* Action buttons for accept/reject */
|
|
341
|
-
.comparison-actions {
|
|
342
|
-
display: flex;
|
|
343
|
-
gap: 12px;
|
|
344
|
-
margin: 16px 0;
|
|
345
|
-
padding: 16px;
|
|
346
|
-
background: #1e293b;
|
|
347
|
-
border-radius: 8px;
|
|
348
|
-
border: 1px solid #334155;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
.accept-btn,
|
|
352
|
-
.reject-btn {
|
|
353
|
-
padding: 10px 16px;
|
|
354
|
-
border: none;
|
|
355
|
-
border-radius: 6px;
|
|
356
|
-
font-size: 14px;
|
|
357
|
-
font-weight: 500;
|
|
358
|
-
cursor: pointer;
|
|
359
|
-
transition: all 0.2s ease;
|
|
360
|
-
display: inline-flex;
|
|
361
|
-
align-items: center;
|
|
362
|
-
gap: 8px;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
.accept-btn {
|
|
366
|
-
background: #059669;
|
|
367
|
-
color: white;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.accept-btn:hover {
|
|
371
|
-
background: #047857;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
.accept-btn:disabled {
|
|
375
|
-
background: #6b7280;
|
|
376
|
-
cursor: not-allowed;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
.reject-btn {
|
|
380
|
-
background: #dc2626;
|
|
381
|
-
color: white;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
.reject-btn:hover {
|
|
385
|
-
background: #b91c1c;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
.reject-btn:disabled {
|
|
389
|
-
background: #6b7280;
|
|
390
|
-
cursor: not-allowed;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
@media (max-width: 768px) {
|
|
394
|
-
.container {
|
|
395
|
-
padding: 10px;
|
|
396
|
-
}
|
|
397
|
-
.summary {
|
|
398
|
-
flex-wrap: wrap;
|
|
399
|
-
gap: 15px;
|
|
400
|
-
}
|
|
401
|
-
.comparison-controls {
|
|
402
|
-
flex-wrap: wrap;
|
|
403
|
-
}
|
|
404
|
-
.side-by-side-container {
|
|
405
|
-
grid-template-columns: 1fr;
|
|
406
|
-
gap: 15px;
|
|
407
|
-
}
|
|
408
|
-
.comparison-actions {
|
|
409
|
-
flex-direction: column;
|
|
410
|
-
}
|
|
411
|
-
}
|