tuffgal 0.1.0-alpha.1 → 0.1.0-alpha.2

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.
@@ -0,0 +1,510 @@
1
+ :root {
2
+ color-scheme: dark light;
3
+ --bg: #0a0a0a;
4
+ --bg-elevated: #111111;
5
+ --text: #e5e5e5;
6
+ --text-muted: #a3a3a3;
7
+ --text-subtle: #737373;
8
+ --border: #262626;
9
+ --border-bright: #404040;
10
+ --accent: #38bdf8;
11
+ --pass: #22c55e;
12
+ --pass-fg: #052e16;
13
+ --changed: #facc15;
14
+ --changed-fg: #1f2937;
15
+ --failed: #f87171;
16
+ --failed-fg: #450a0a;
17
+ --skipped: #a1a1aa;
18
+ --skipped-fg: #18181b;
19
+ --new: #38bdf8;
20
+ --new-fg: #082f49;
21
+ --font-mono:
22
+ ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace;
23
+ --font-sans:
24
+ ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
25
+ }
26
+
27
+ @media (prefers-color-scheme: light) {
28
+ :root {
29
+ --bg: #fafafa;
30
+ --bg-elevated: #ffffff;
31
+ --text: #18181b;
32
+ --text-muted: #525252;
33
+ --text-subtle: #737373;
34
+ --border: #d4d4d4;
35
+ --border-bright: #a3a3a3;
36
+ --accent: #0369a1;
37
+ --pass: #15803d;
38
+ --pass-fg: #ffffff;
39
+ --changed: #a16207;
40
+ --changed-fg: #ffffff;
41
+ --failed: #b91c1c;
42
+ --failed-fg: #ffffff;
43
+ --skipped: #52525b;
44
+ --skipped-fg: #ffffff;
45
+ --new: #0369a1;
46
+ --new-fg: #ffffff;
47
+ }
48
+ }
49
+
50
+ * {
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ body {
55
+ margin: 0;
56
+ padding: 0 1.5rem 4rem;
57
+ background: var(--bg);
58
+ color: var(--text);
59
+ font-family: var(--font-mono);
60
+ font-size: 14px;
61
+ line-height: 1.55;
62
+ }
63
+
64
+ .sr-only {
65
+ position: absolute;
66
+ width: 1px;
67
+ height: 1px;
68
+ margin: -1px;
69
+ padding: 0;
70
+ border: 0;
71
+ clip: rect(0 0 0 0);
72
+ overflow: hidden;
73
+ white-space: nowrap;
74
+ }
75
+
76
+ .skip-link {
77
+ position: absolute;
78
+ left: 0;
79
+ top: 0;
80
+ padding: 0.5rem 1rem;
81
+ background: var(--bg-elevated);
82
+ color: var(--text);
83
+ text-decoration: none;
84
+ border: 1px solid var(--border-bright);
85
+ transform: translateY(-200%);
86
+ transition: transform 0.15s ease;
87
+ }
88
+
89
+ .skip-link:focus-visible {
90
+ transform: translateY(0);
91
+ outline: 2px solid var(--accent);
92
+ outline-offset: 2px;
93
+ }
94
+
95
+ .report-header {
96
+ max-width: 80rem;
97
+ margin: 0 auto;
98
+ padding: 2rem 0 1.25rem;
99
+ border-bottom: 1px solid var(--border);
100
+ }
101
+
102
+ .report-header h1 {
103
+ margin: 0 0 0.25rem;
104
+ font-size: 1.25rem;
105
+ font-weight: 600;
106
+ }
107
+
108
+ .report-meta {
109
+ margin: 0;
110
+ color: var(--text-muted);
111
+ font-size: 0.875rem;
112
+ }
113
+
114
+ main {
115
+ max-width: 80rem;
116
+ margin: 0 auto;
117
+ }
118
+
119
+ main:focus-visible {
120
+ outline: 2px solid var(--accent);
121
+ outline-offset: 4px;
122
+ }
123
+
124
+ h2 {
125
+ margin: 2rem 0 0.75rem;
126
+ font-size: 0.875rem;
127
+ font-weight: 600;
128
+ letter-spacing: 0.05em;
129
+ text-transform: uppercase;
130
+ color: var(--text-muted);
131
+ }
132
+
133
+ /* Summary */
134
+
135
+ .summary {
136
+ margin-top: 0.5rem;
137
+ }
138
+
139
+ .summary-list {
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ gap: 1.25rem;
143
+ margin: 0;
144
+ padding: 0.75rem 1rem;
145
+ list-style: none;
146
+ background: var(--bg-elevated);
147
+ border: 1px solid var(--border);
148
+ }
149
+
150
+ .summary-item {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 0.5rem;
154
+ color: var(--text);
155
+ }
156
+
157
+ .summary-item .count {
158
+ font-weight: 600;
159
+ }
160
+
161
+ .summary-item .label {
162
+ color: var(--text-muted);
163
+ }
164
+
165
+ .indicator {
166
+ display: inline-block;
167
+ width: 0.625rem;
168
+ height: 0.625rem;
169
+ background: var(--text-muted);
170
+ border: 1px solid currentColor;
171
+ }
172
+
173
+ .summary-item[data-status='pass'] .indicator {
174
+ background: var(--pass);
175
+ color: var(--pass);
176
+ }
177
+ .summary-item[data-status='changed'] .indicator {
178
+ background: var(--changed);
179
+ color: var(--changed);
180
+ }
181
+ .summary-item[data-status='failed'] .indicator {
182
+ background: var(--failed);
183
+ color: var(--failed);
184
+ }
185
+
186
+ .summary-item.coverage {
187
+ gap: 0.4rem;
188
+ }
189
+
190
+ .summary-item.coverage .coverage-detail {
191
+ color: var(--text-subtle);
192
+ font-size: 0.8125rem;
193
+ }
194
+
195
+ .summary-item.coverage .coverage-detail::before {
196
+ content: '·';
197
+ margin-right: 0.35rem;
198
+ color: var(--text-subtle);
199
+ }
200
+
201
+ /* Stories tree */
202
+
203
+ .stories {
204
+ margin: 0;
205
+ padding: 0;
206
+ list-style: none;
207
+ background: var(--bg-elevated);
208
+ border: 1px solid var(--border);
209
+ }
210
+
211
+ .story {
212
+ padding: 0.875rem 1rem;
213
+ border-top: 1px solid var(--border);
214
+ }
215
+
216
+ .story:first-child {
217
+ border-top: 0;
218
+ }
219
+
220
+ .story[data-status='failed'] {
221
+ box-shadow: inset 3px 0 0 var(--failed);
222
+ }
223
+ .story[data-status='changed'] {
224
+ box-shadow: inset 3px 0 0 var(--changed);
225
+ }
226
+
227
+ .story-row {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 0.75rem;
231
+ flex-wrap: wrap;
232
+ }
233
+
234
+ .story-file {
235
+ font-weight: 600;
236
+ }
237
+
238
+ .story-duration {
239
+ margin-left: auto;
240
+ color: var(--text-subtle);
241
+ }
242
+
243
+ .story-prose {
244
+ margin: 0.25rem 0 0.5rem;
245
+ color: var(--text-muted);
246
+ font-family: var(--font-sans);
247
+ font-size: 0.875rem;
248
+ }
249
+
250
+ .actions {
251
+ margin: 0.5rem 0 0;
252
+ padding: 0;
253
+ list-style: none;
254
+ }
255
+
256
+ .action {
257
+ padding: 0.15rem 0;
258
+ }
259
+
260
+ .action[data-status='failed'] {
261
+ color: var(--failed);
262
+ }
263
+ .action[data-status='changed'] {
264
+ color: var(--changed);
265
+ }
266
+
267
+ .action-row {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 0.6rem;
271
+ flex-wrap: wrap;
272
+ }
273
+
274
+ .branch {
275
+ color: var(--text-subtle);
276
+ user-select: none;
277
+ }
278
+
279
+ .action-name {
280
+ color: var(--text);
281
+ }
282
+
283
+ .action-duration {
284
+ margin-left: auto;
285
+ color: var(--text-subtle);
286
+ }
287
+
288
+ /* Status badges: square bracket letter + label, fully sans-serif label */
289
+
290
+ .status {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ gap: 0.4rem;
294
+ padding: 0 0.5rem;
295
+ border: 1px solid currentColor;
296
+ min-width: 5.5rem;
297
+ }
298
+
299
+ .status-letter {
300
+ font-weight: 600;
301
+ letter-spacing: -0.02em;
302
+ }
303
+
304
+ .status-label {
305
+ font-family: var(--font-sans);
306
+ font-size: 0.75rem;
307
+ }
308
+
309
+ .status[data-status='pass'] {
310
+ color: var(--pass);
311
+ }
312
+ .status[data-status='changed'] {
313
+ color: var(--changed);
314
+ }
315
+ .status[data-status='failed'] {
316
+ color: var(--failed);
317
+ }
318
+ .status[data-status='skipped'] {
319
+ color: var(--skipped);
320
+ }
321
+ .status[data-status='new'] {
322
+ color: var(--new);
323
+ }
324
+
325
+ .action-parameters {
326
+ display: grid;
327
+ grid-template-columns: max-content 1fr;
328
+ gap: 0.15rem 0.75rem;
329
+ margin: 0.25rem 0 0 2.2rem;
330
+ font-size: 0.8125rem;
331
+ color: var(--text-muted);
332
+ }
333
+
334
+ .action-parameters dt {
335
+ color: var(--text-subtle);
336
+ }
337
+
338
+ .action-parameters dd {
339
+ margin: 0;
340
+ }
341
+
342
+ .action-error {
343
+ margin: 0.5rem 0 0 2.2rem;
344
+ padding: 0.5rem 0.75rem;
345
+ background: var(--bg);
346
+ border-left: 3px solid var(--failed);
347
+ color: var(--text);
348
+ font-family: var(--font-mono);
349
+ font-size: 0.8125rem;
350
+ white-space: pre-wrap;
351
+ overflow-x: auto;
352
+ }
353
+
354
+ /* Screenshot details / radiogroup */
355
+
356
+ .shots {
357
+ margin: 0 0 0 2.2rem;
358
+ margin-top: 0.25rem;
359
+ }
360
+
361
+ .shots > summary {
362
+ display: inline-block;
363
+ color: var(--accent);
364
+ cursor: pointer;
365
+ user-select: none;
366
+ list-style: none;
367
+ font-size: 0.8125rem;
368
+ }
369
+
370
+ .shots > summary::-webkit-details-marker {
371
+ display: none;
372
+ }
373
+
374
+ .shots > summary:focus-visible {
375
+ outline: 2px solid var(--accent);
376
+ outline-offset: 2px;
377
+ }
378
+
379
+ .shot-radio {
380
+ display: flex;
381
+ gap: 0.25rem;
382
+ margin: 0.5rem 0;
383
+ padding: 0;
384
+ border: 0;
385
+ }
386
+
387
+ .shot-radio-label {
388
+ display: inline-flex;
389
+ align-items: center;
390
+ gap: 0.35rem;
391
+ padding: 0.2rem 0.6rem;
392
+ border: 1px solid var(--border-bright);
393
+ cursor: pointer;
394
+ font-size: 0.8125rem;
395
+ color: var(--text-muted);
396
+ }
397
+
398
+ .shot-radio-label:has(input:checked) {
399
+ background: var(--bg-elevated);
400
+ color: var(--text);
401
+ border-color: var(--accent);
402
+ }
403
+
404
+ .shot-radio-label:has(input:focus-visible) {
405
+ outline: 2px solid var(--accent);
406
+ outline-offset: 1px;
407
+ }
408
+
409
+ .shot-radio-label:has(input:disabled) {
410
+ opacity: 0.45;
411
+ cursor: not-allowed;
412
+ }
413
+
414
+ .shot-radio-label input {
415
+ position: absolute;
416
+ opacity: 0;
417
+ pointer-events: none;
418
+ }
419
+
420
+ .shot-panel {
421
+ margin-top: 0.5rem;
422
+ padding: 0.5rem;
423
+ border: 1px solid var(--border);
424
+ background: var(--bg);
425
+ }
426
+
427
+ .shot-panel img {
428
+ display: block;
429
+ width: 100%;
430
+ height: auto;
431
+ border: 1px solid var(--border);
432
+ }
433
+
434
+ .shot-missing {
435
+ margin: 0;
436
+ padding: 1rem;
437
+ text-align: center;
438
+ color: var(--text-subtle);
439
+ font-family: var(--font-sans);
440
+ }
441
+
442
+ .diff-stats {
443
+ margin: 0.5rem 0 0;
444
+ color: var(--text-muted);
445
+ font-size: 0.8125rem;
446
+ }
447
+
448
+ /* Failures section */
449
+
450
+ .failures {
451
+ margin: 0;
452
+ padding: 0;
453
+ list-style: none;
454
+ }
455
+
456
+ .failure {
457
+ margin: 0.5rem 0;
458
+ padding: 0.875rem 1rem;
459
+ background: var(--bg-elevated);
460
+ border: 1px solid var(--border);
461
+ border-left: 3px solid var(--failed);
462
+ }
463
+
464
+ .failure-head {
465
+ display: flex;
466
+ flex-wrap: wrap;
467
+ gap: 0.5rem;
468
+ margin-bottom: 0.5rem;
469
+ font-size: 0.875rem;
470
+ color: var(--text-muted);
471
+ }
472
+
473
+ .failure-message {
474
+ margin: 0;
475
+ padding: 0.5rem 0.75rem;
476
+ background: var(--bg);
477
+ border-left: 2px solid var(--border-bright);
478
+ color: var(--text);
479
+ font-size: 0.8125rem;
480
+ white-space: pre-wrap;
481
+ overflow-x: auto;
482
+ }
483
+
484
+ .failure-trace {
485
+ margin: 0.5rem 0 0;
486
+ font-family: var(--font-sans);
487
+ font-size: 0.8125rem;
488
+ color: var(--text-muted);
489
+ }
490
+
491
+ .failure-trace a {
492
+ color: var(--accent);
493
+ }
494
+
495
+ .prose-block {
496
+ font-family: var(--font-sans);
497
+ color: var(--text-muted);
498
+ font-size: 0.875rem;
499
+ }
500
+
501
+ .empty {
502
+ color: var(--text-subtle);
503
+ }
504
+
505
+ /* Reduce motion */
506
+ @media (prefers-reduced-motion: reduce) {
507
+ .skip-link {
508
+ transition: none;
509
+ }
510
+ }
@@ -0,0 +1,45 @@
1
+ // Reveals the matching <div.shot-panel> for the radio the user picks. The
2
+ // radio group already handles keyboard navigation natively (arrow keys move
3
+ // the selection across the visible labels); this script just translates the
4
+ // `change` event into a panel-visibility toggle so we stay framework-free.
5
+ (function () {
6
+ function setupShots(container) {
7
+ var radios = Array.prototype.slice.call(
8
+ container.querySelectorAll('input[type="radio"]'),
9
+ );
10
+ var panels = Array.prototype.slice.call(
11
+ container.parentElement.querySelectorAll('.shot-panel'),
12
+ );
13
+
14
+ function activate(name) {
15
+ panels.forEach(function (panel) {
16
+ panel.hidden = panel.getAttribute('data-tab') !== name;
17
+ });
18
+ }
19
+
20
+ var defaultTab = container.getAttribute('data-default-tab');
21
+ var initialRadio = radios.find(function (radio) {
22
+ return radio.value === defaultTab && !radio.disabled;
23
+ }) ||
24
+ radios.find(function (radio) {
25
+ return !radio.disabled;
26
+ });
27
+ if (initialRadio) {
28
+ initialRadio.checked = true;
29
+ activate(initialRadio.value);
30
+ }
31
+
32
+ radios.forEach(function (radio) {
33
+ radio.addEventListener('change', function () {
34
+ if (radio.disabled) return;
35
+ activate(radio.value);
36
+ });
37
+ });
38
+ }
39
+
40
+ document
41
+ .querySelectorAll('.shot-radio')
42
+ .forEach(function (container) {
43
+ setupShots(container);
44
+ });
45
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuffgal",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "JSON-driven visual regression for web apps.",
5
5
  "license": "MIT",
6
6
  "author": "Nick Schneble",
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/nschneble/tuffgal.git"
11
11
  },
12
12
  "scripts": {
13
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
13
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/reporter/assets && cp src/reporter/assets/report.css src/reporter/assets/report.js dist/reporter/assets/",
14
14
  "format": "prettier --write \"**/*.{ts,tsx,json,md}\"",
15
15
  "install:browsers": "playwright install chromium",
16
16
  "lint": "eslint \"src/**/*.ts\" --max-warnings=0",