@vizzly-testing/cli 0.20.0 → 0.20.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -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
- }