@matware/e2e-runner 1.5.0 → 1.5.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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +1 -1
- package/README.md +451 -274
- package/agents/test-improver.md +2 -1
- package/bin/cli.js +13 -2
- package/package.json +2 -2
- package/skills/e2e-testing/SKILL.md +2 -1
- package/skills/e2e-testing/references/action-types.md +17 -18
- package/skills/e2e-testing/references/troubleshooting.md +2 -26
- package/src/actions.js +12 -2
- package/src/dashboard.js +50 -5
- package/src/db.js +15 -0
- package/src/mcp-tools.js +238 -75
- package/src/narrate.js +19 -0
- package/src/runner.js +72 -14
- package/src/visual-diff.js +8 -7
- package/templates/dashboard/js/utils.js +23 -2
- package/templates/dashboard/js/view-runs.js +94 -9
- package/templates/dashboard/styles/components.css +17 -0
- package/templates/dashboard/styles/view-runs.css +51 -4
- package/templates/dashboard/template.html +2 -2
- package/templates/dashboard.html +187 -17
package/src/visual-diff.js
CHANGED
|
@@ -32,11 +32,12 @@ function readChunks(buf) {
|
|
|
32
32
|
return chunks;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function decodePNG(
|
|
36
|
-
|
|
35
|
+
function decodePNG(input) {
|
|
36
|
+
// Accepts a file path or an in-memory PNG Buffer (capture-time checks).
|
|
37
|
+
const buf = Buffer.isBuffer(input) ? input : fs.readFileSync(input);
|
|
37
38
|
|
|
38
|
-
if (buf.compare(PNG_SIGNATURE, 0, 8, 0, 8) !== 0) {
|
|
39
|
-
throw new Error(`Not a valid PNG file: ${
|
|
39
|
+
if (buf.length < 8 || buf.compare(PNG_SIGNATURE, 0, 8, 0, 8) !== 0) {
|
|
40
|
+
throw new Error(`Not a valid PNG file: ${Buffer.isBuffer(input) ? '<buffer>' : input}`);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const chunks = readChunks(buf);
|
|
@@ -458,19 +459,19 @@ function buildMaskLookup(regions, imgWidth, imgHeight) {
|
|
|
458
459
|
* frame. Non-PNG or undecodable files are reported as not-blank so they are
|
|
459
460
|
* never deleted by mistake.
|
|
460
461
|
*
|
|
461
|
-
* @param {string}
|
|
462
|
+
* @param {string|Buffer} input — PNG file path, or an in-memory PNG buffer
|
|
462
463
|
* @param {{tolerance?:number, maxOutlierFraction?:number, maxSamples?:number}} [opts]
|
|
463
464
|
* @returns {{blank:boolean, color?:{r:number,g:number,b:number}, brightness?:number,
|
|
464
465
|
* width?:number, height?:number, outlierFraction?:number, error?:string}}
|
|
465
466
|
*/
|
|
466
|
-
export function isBlankImage(
|
|
467
|
+
export function isBlankImage(input, opts = {}) {
|
|
467
468
|
const tolerance = opts.tolerance ?? 10;
|
|
468
469
|
const maxOutlierFraction = opts.maxOutlierFraction ?? 0.005; // ≤0.5% off-color pixels
|
|
469
470
|
const maxSamples = opts.maxSamples ?? 120000;
|
|
470
471
|
|
|
471
472
|
let img;
|
|
472
473
|
try {
|
|
473
|
-
img = decodePNG(
|
|
474
|
+
img = decodePNG(input);
|
|
474
475
|
} catch (error) {
|
|
475
476
|
return { blank: false, error: error.message };
|
|
476
477
|
}
|
|
@@ -23,6 +23,27 @@ function prettyJson(str){
|
|
|
23
23
|
try{return JSON.stringify(JSON.parse(str),null,2)}catch(e){return str}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/* Lightweight JSON syntax highlighter. Escapes HTML, then wraps tokens in
|
|
27
|
+
colored spans. Input should already be pretty-printed (prettyJson). */
|
|
28
|
+
function highlightJson(text){
|
|
29
|
+
var esc=String(text).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
30
|
+
return esc.replace(/"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?\b/g,function(m,off,s){
|
|
31
|
+
var cls;
|
|
32
|
+
if(m[0]==='"'){cls=/^\s*:/.test(s.slice(off+m.length))?'jn-key':'jn-str'}
|
|
33
|
+
else if(m==='true'||m==='false'){cls='jn-bool'}
|
|
34
|
+
else if(m==='null'){cls='jn-null'}
|
|
35
|
+
else{cls='jn-num'}
|
|
36
|
+
return '<span class="'+cls+'">'+m+'</span>';
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Builds a <pre> with syntax-highlighted JSON content. */
|
|
41
|
+
function jsonPre(text){
|
|
42
|
+
var p=document.createElement('pre');
|
|
43
|
+
p.innerHTML=highlightJson(text);
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
function fmtHeaders(h){
|
|
27
48
|
if(!h||typeof h!=='object')return '';
|
|
28
49
|
return Object.keys(h).map(function(k){return k+': '+h[k]}).join('\n');
|
|
@@ -109,7 +130,7 @@ function buildNetRow(n){
|
|
|
109
130
|
}
|
|
110
131
|
if(n.requestBody){
|
|
111
132
|
var rbText=prettyJson(n.requestBody);
|
|
112
|
-
sections.push(buildNdSection('Request Body',
|
|
133
|
+
sections.push(buildNdSection('Request Body',jsonPre(rbText),null,rbText));
|
|
113
134
|
}
|
|
114
135
|
if(n.responseHeaders){
|
|
115
136
|
var rhCount=Object.keys(n.responseHeaders).length;
|
|
@@ -117,7 +138,7 @@ function buildNetRow(n){
|
|
|
117
138
|
}
|
|
118
139
|
if(n.responseBody){
|
|
119
140
|
var respText=prettyJson(n.responseBody);
|
|
120
|
-
sections.push(buildNdSection('Response Body',
|
|
141
|
+
sections.push(buildNdSection('Response Body',jsonPre(respText),null,respText));
|
|
121
142
|
}
|
|
122
143
|
detail=el('div',{className:'rd-net-detail'},sections);
|
|
123
144
|
row.addEventListener('click',function(e){e.stopPropagation();row.classList.toggle('open')});
|
|
@@ -309,7 +309,8 @@ function loadDetailInline(id,detailTr){
|
|
|
309
309
|
var stateCls=a.success?'pass':'fail';
|
|
310
310
|
var icon=a.success?'\u2714':'\u2718';
|
|
311
311
|
|
|
312
|
-
// Thumbnail: prefer autoScreenshot, fall back to action's own screenshot
|
|
312
|
+
// Thumbnail: prefer autoScreenshot, fall back to action's own screenshot.
|
|
313
|
+
// Data-capture steps (raw JSON saved instead of a screenshot) get a {} thumb.
|
|
313
314
|
var thumbPath=a.autoScreenshot||a.screenshot||null;
|
|
314
315
|
var thumb;
|
|
315
316
|
if(thumbPath){
|
|
@@ -317,6 +318,10 @@ function loadDetailInline(id,detailTr){
|
|
|
317
318
|
var img=document.createElement('img');
|
|
318
319
|
img.src=src;img.alt=label;img.loading='lazy';
|
|
319
320
|
thumb=el('div',{className:'sl-thumb',onclick:function(e){e.stopPropagation();openModal(src)}},[img]);
|
|
321
|
+
}else if(a.dataCapture){
|
|
322
|
+
(function(dc){
|
|
323
|
+
thumb=el('div',{className:'sl-thumb sl-thumb-json',title:'Raw JSON response \u2014 click to view',onclick:function(e){e.stopPropagation();openJsonModal(dc)}},[el('span',null,'{\u2009}')]);
|
|
324
|
+
})(a.dataCapture);
|
|
320
325
|
}else{
|
|
321
326
|
thumb=el('div',{className:'sl-thumb sl-thumb-empty',title:'No screenshot for this step'},[el('span',null,'\u25A1')]);
|
|
322
327
|
}
|
|
@@ -326,6 +331,12 @@ function loadDetailInline(id,detailTr){
|
|
|
326
331
|
if(a.selector)chips.appendChild(el('span',{className:'sl-chip sl-chip-sel'},[el('span',{className:'sl-chip-k'},'sel'),el('span',{className:'sl-chip-v'},a.selector)]));
|
|
327
332
|
if(a.text)chips.appendChild(el('span',{className:'sl-chip'},[el('span',{className:'sl-chip-k'},'text'),el('span',{className:'sl-chip-v'},String(a.text))]));
|
|
328
333
|
if(a.value!=null&&a.value!=='')chips.appendChild(el('span',{className:'sl-chip'},[el('span',{className:'sl-chip-k'},'val'),el('span',{className:'sl-chip-v'},String(a.value))]));
|
|
334
|
+
if(a.dataCapture)(function(dc){
|
|
335
|
+
chips.appendChild(el('span',{className:'sl-chip sl-chip-json',title:'Raw JSON response saved for this step \u2014 click to view',onclick:function(e){e.stopPropagation();openJsonModal(dc)}},[
|
|
336
|
+
el('span',{className:'sl-chip-k'},'JSON'),
|
|
337
|
+
el('span',{className:'sl-chip-v'},dc.split('/').pop())
|
|
338
|
+
]));
|
|
339
|
+
})(a.dataCapture);
|
|
329
340
|
|
|
330
341
|
var retryBadge=(a.actionRetries&&a.actionRetries>0)?el('span',{className:'sl-retry'},'\u21BB '+a.actionRetries):null;
|
|
331
342
|
|
|
@@ -456,6 +467,15 @@ function loadDetailInline(id,detailTr){
|
|
|
456
467
|
}
|
|
457
468
|
|
|
458
469
|
/* ── Screenshots ── */
|
|
470
|
+
/* Session memory: which test groups the user expanded (survives refreshes, not reloads) */
|
|
471
|
+
var SS_EXPANDED={};
|
|
472
|
+
function buildGalleryItem(f){
|
|
473
|
+
var src='/api/image?path='+encodeURIComponent(f.path);
|
|
474
|
+
var img=document.createElement('img');img.src=src;img.alt=f.name;img.loading='lazy';
|
|
475
|
+
var capEl=el('div',{className:'cap'},[el('span',{className:'cap-name'},f.name)]);
|
|
476
|
+
(function(c,fp){ssHash(fp).then(function(h){c.appendChild(createHashBadge(h))})})(capEl,f.path);
|
|
477
|
+
return el('div',{className:'gallery-item','data-path':f.path,onclick:function(){openModal(src)}},[img,capEl]);
|
|
478
|
+
}
|
|
459
479
|
function refreshScreenshots(){
|
|
460
480
|
var gal=$('#screenshotGallery'),empty=$('#screenshotsEmpty');
|
|
461
481
|
gal.textContent='';
|
|
@@ -464,12 +484,50 @@ function refreshScreenshots(){
|
|
|
464
484
|
if(!Array.isArray(files)||!files.length){empty.style.display='block';empty.querySelector('p').textContent='No screenshots for this project.';$('#badgeScreenshots').textContent='0';return}
|
|
465
485
|
empty.style.display='none';
|
|
466
486
|
$('#badgeScreenshots').textContent=files.length;
|
|
487
|
+
/* Group by test name; files without one go to a trailing "Other" bucket */
|
|
488
|
+
var groups={},order=[];
|
|
467
489
|
files.forEach(function(f){
|
|
468
|
-
var
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
490
|
+
var key=f.testName||'__other__';
|
|
491
|
+
if(!groups[key]){groups[key]=[];order.push(key)}
|
|
492
|
+
groups[key].push(f);
|
|
493
|
+
});
|
|
494
|
+
function fileTs(f){var m=f.name.match(/(\d{10,})\.[a-z]+$/i);return m?parseInt(m[1],10):0}
|
|
495
|
+
order.sort(function(a,b){
|
|
496
|
+
if(a==='__other__')return 1;
|
|
497
|
+
if(b==='__other__')return -1;
|
|
498
|
+
var ma=0,mb=0;
|
|
499
|
+
groups[a].forEach(function(f){var t=fileTs(f);if(t>ma)ma=t});
|
|
500
|
+
groups[b].forEach(function(f){var t=fileTs(f);if(t>mb)mb=t});
|
|
501
|
+
return mb-ma;
|
|
502
|
+
});
|
|
503
|
+
order.forEach(function(key){
|
|
504
|
+
var items=groups[key],label=key==='__other__'?'Other':key;
|
|
505
|
+
var grid=el('div',{className:'gallery'});
|
|
506
|
+
var materialized=false;
|
|
507
|
+
function materialize(){
|
|
508
|
+
if(materialized)return;materialized=true;
|
|
509
|
+
items.forEach(function(f){grid.appendChild(buildGalleryItem(f))});
|
|
510
|
+
}
|
|
511
|
+
var expanded=!!SS_EXPANDED[key];
|
|
512
|
+
var body=el('div',{className:'gallery-group-body'},[grid]);
|
|
513
|
+
var group=el('div',{className:'gallery-group'+(expanded?' open':'')},[]);
|
|
514
|
+
var header=el('div',{className:'gallery-group-header',onclick:function(){
|
|
515
|
+
expanded=!expanded;
|
|
516
|
+
if(expanded){SS_EXPANDED[key]=true;materialize()}else{delete SS_EXPANDED[key]}
|
|
517
|
+
group.classList.toggle('open',expanded);
|
|
518
|
+
if(expanded)body.removeAttribute('hidden');else body.setAttribute('hidden','');
|
|
519
|
+
}},[
|
|
520
|
+
el('span',{className:'gg-arrow'},'▶'),
|
|
521
|
+
el('span',{className:'gg-name'},label),
|
|
522
|
+
el('span',{className:'gg-count'},items.length+' shot'+(items.length===1?'':'s')),
|
|
523
|
+
el('span',{className:'gg-blank-badge',hidden:''},''),
|
|
524
|
+
]);
|
|
525
|
+
group.appendChild(header);
|
|
526
|
+
group.appendChild(body);
|
|
527
|
+
if(expanded){materialize()}else{body.setAttribute('hidden','')}
|
|
528
|
+
/* Expose to the blank-scan so it can materialize collapsed groups on demand */
|
|
529
|
+
group._ssItems=items;group._ssMaterialize=materialize;
|
|
530
|
+
gal.appendChild(group);
|
|
473
531
|
});
|
|
474
532
|
resetBlankBar();
|
|
475
533
|
}).catch(function(){});
|
|
@@ -479,6 +537,7 @@ function refreshScreenshots(){
|
|
|
479
537
|
function resetBlankBar(){
|
|
480
538
|
var bar=$('#ssBlankBar');if(bar)bar.hidden=true;
|
|
481
539
|
$$('#screenshotGallery .gallery-item.blank-flagged').forEach(function(it){it.classList.remove('blank-flagged')});
|
|
540
|
+
$$('#screenshotGallery .gg-blank-badge').forEach(function(b){b.textContent='';b.setAttribute('hidden','')});
|
|
482
541
|
S.blankPaths=null;
|
|
483
542
|
}
|
|
484
543
|
function scanBlankScreenshots(){
|
|
@@ -494,10 +553,18 @@ function scanBlankScreenshots(){
|
|
|
494
553
|
return;
|
|
495
554
|
}
|
|
496
555
|
S.blankPaths=blanks.map(function(b){return b.path});
|
|
497
|
-
|
|
556
|
+
/* Materialize collapsed groups that contain blanks, flag items, badge the headers */
|
|
557
|
+
var blankSet={};S.blankPaths.forEach(function(p){blankSet[p]=true});
|
|
558
|
+
$$('#screenshotGallery .gallery-group').forEach(function(g){
|
|
559
|
+
var inGroup=(g._ssItems||[]).filter(function(it){return blankSet[it.path]}).length;
|
|
560
|
+
if(!inGroup)return;
|
|
561
|
+
if(g._ssMaterialize)g._ssMaterialize();
|
|
562
|
+
var badge=g.querySelector('.gg-blank-badge');
|
|
563
|
+
if(badge){badge.textContent=inGroup+' blank'+(inGroup===1?'':'s');badge.removeAttribute('hidden')}
|
|
564
|
+
});
|
|
498
565
|
S.blankPaths.forEach(function(p){
|
|
499
566
|
var item=$('#screenshotGallery .gallery-item[data-path="'+(window.CSS&&CSS.escape?CSS.escape(p):p)+'"]');
|
|
500
|
-
if(item)
|
|
567
|
+
if(item)item.classList.add('blank-flagged');
|
|
501
568
|
});
|
|
502
569
|
$('#ssBlankMsg').textContent=blanks.length+' blank image'+(blanks.length===1?'':'s')+' of '+data.scanned+' scanned';
|
|
503
570
|
$('#ssBlankBar').hidden=false;
|
|
@@ -792,8 +859,26 @@ $('#btnExportLearnings').addEventListener('click',function(){
|
|
|
792
859
|
});
|
|
793
860
|
|
|
794
861
|
/* ── Modal ── */
|
|
795
|
-
function openModal(src){
|
|
862
|
+
function openModal(src){
|
|
863
|
+
$('#modalImg').src=src;$('#modalImg').removeAttribute('hidden');
|
|
864
|
+
$('#modalJson').setAttribute('hidden','');
|
|
865
|
+
$('#modal').classList.add('open');
|
|
866
|
+
}
|
|
867
|
+
function openJsonModal(filePath){
|
|
868
|
+
fetch('/api/image?path='+encodeURIComponent(filePath)).then(function(r){
|
|
869
|
+
if(!r.ok)throw new Error('not found');
|
|
870
|
+
return r.text();
|
|
871
|
+
}).then(function(text){
|
|
872
|
+
$('#modalImg').setAttribute('hidden','');
|
|
873
|
+
var pre=$('#modalJson');
|
|
874
|
+
pre.innerHTML=highlightJson(prettyJson(text));
|
|
875
|
+
pre.removeAttribute('hidden');
|
|
876
|
+
$('#modal').classList.add('open');
|
|
877
|
+
}).catch(function(){showToast('Could not load JSON capture','error')});
|
|
878
|
+
}
|
|
796
879
|
$('#modal').addEventListener('click',function(){$('#modal').classList.remove('open')});
|
|
880
|
+
/* Allow selecting/copying JSON text without closing the modal */
|
|
881
|
+
$('#modalJson').addEventListener('click',function(e){e.stopPropagation()});
|
|
797
882
|
|
|
798
883
|
/* ══════════════════════════════════════════════════════════════════
|
|
799
884
|
Screenshot Replay Player — plays a run's per-step screenshots
|
|
@@ -248,6 +248,23 @@ tbody tr.selected td{
|
|
|
248
248
|
cursor:default;
|
|
249
249
|
box-shadow:0 0 0 1px rgba(158,242,106,.12),0 40px 80px rgba(0,0,0,.6);
|
|
250
250
|
}
|
|
251
|
+
.modal pre{
|
|
252
|
+
max-width:min(900px,100%);max-height:90vh;overflow:auto;
|
|
253
|
+
margin:0;padding:20px 24px;
|
|
254
|
+
background:var(--bg-2);
|
|
255
|
+
border:1px solid var(--border);border-radius:var(--r);
|
|
256
|
+
font-family:var(--mono);font-size:12px;line-height:1.6;
|
|
257
|
+
color:var(--text);cursor:text;user-select:text;
|
|
258
|
+
box-shadow:0 0 0 1px rgba(158,242,106,.12),0 40px 80px rgba(0,0,0,.6);
|
|
259
|
+
}
|
|
260
|
+
.modal pre[hidden],.modal img[hidden]{display:none}
|
|
261
|
+
|
|
262
|
+
/* ── JSON syntax highlighting (modal + network body panels) ── */
|
|
263
|
+
.jn-key{color:var(--beacon)}
|
|
264
|
+
.jn-str{color:var(--amber)}
|
|
265
|
+
.jn-num{color:var(--phosphor)}
|
|
266
|
+
.jn-bool{color:var(--red);font-weight:600}
|
|
267
|
+
.jn-null{color:var(--red);opacity:.65;font-style:italic}
|
|
251
268
|
|
|
252
269
|
/* ── Toasts ── */
|
|
253
270
|
.toast-container{
|
|
@@ -439,10 +439,49 @@ tr.expanded td:first-child::before{
|
|
|
439
439
|
.hb-link span{font-size:10px;color:var(--beacon);font-weight:700;letter-spacing:.18em;text-transform:uppercase}
|
|
440
440
|
|
|
441
441
|
/* ═══════════════════ Screenshots ═══════════════════ */
|
|
442
|
+
.gallery-group{
|
|
443
|
+
border:1px solid var(--border);
|
|
444
|
+
border-radius:var(--r);
|
|
445
|
+
background:var(--surface);
|
|
446
|
+
margin-bottom:10px;
|
|
447
|
+
overflow:hidden;
|
|
448
|
+
}
|
|
449
|
+
.gallery-group-header{
|
|
450
|
+
display:flex;align-items:center;gap:10px;
|
|
451
|
+
padding:10px 14px;cursor:pointer;user-select:none;
|
|
452
|
+
background:var(--bg-2);
|
|
453
|
+
transition:background .15s;
|
|
454
|
+
}
|
|
455
|
+
.gallery-group-header:hover{background:var(--surface)}
|
|
456
|
+
.gallery-group .gg-arrow{
|
|
457
|
+
font-size:9px;color:var(--text2);
|
|
458
|
+
transition:transform .15s;flex-shrink:0;
|
|
459
|
+
}
|
|
460
|
+
.gallery-group.open .gg-arrow{transform:rotate(90deg)}
|
|
461
|
+
.gallery-group .gg-name{
|
|
462
|
+
font-family:var(--mono);font-size:12px;font-weight:600;
|
|
463
|
+
color:var(--text);
|
|
464
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
|
|
465
|
+
}
|
|
466
|
+
.gallery-group .gg-count{
|
|
467
|
+
font-family:var(--mono);font-size:10px;color:var(--text2);
|
|
468
|
+
padding:2px 8px;border-radius:999px;
|
|
469
|
+
background:var(--bg);border:1px solid var(--border);
|
|
470
|
+
flex-shrink:0;margin-left:auto;
|
|
471
|
+
}
|
|
472
|
+
.gallery-group .gg-blank-badge{
|
|
473
|
+
font-family:var(--mono);font-size:9px;font-weight:700;
|
|
474
|
+
padding:2px 8px;border-radius:999px;
|
|
475
|
+
background:var(--amber-dim);color:var(--amber);border:1px solid var(--amber);
|
|
476
|
+
flex-shrink:0;
|
|
477
|
+
}
|
|
478
|
+
.gallery-group .gg-blank-badge[hidden]{display:none}
|
|
479
|
+
.gallery-group-body{padding:12px}
|
|
480
|
+
.gallery-group-body[hidden]{display:none}
|
|
442
481
|
.gallery{
|
|
443
482
|
display:grid;
|
|
444
|
-
grid-template-columns:repeat(auto-fill,minmax(
|
|
445
|
-
gap:
|
|
483
|
+
grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
|
|
484
|
+
gap:10px;
|
|
446
485
|
}
|
|
447
486
|
.gallery-item{
|
|
448
487
|
background:var(--surface);
|
|
@@ -456,11 +495,11 @@ tr.expanded td:first-child::before{
|
|
|
456
495
|
content:'';position:absolute;top:0;left:0;width:18px;height:1px;
|
|
457
496
|
background:var(--phosphor);opacity:.4;z-index:1;
|
|
458
497
|
}
|
|
459
|
-
|
|
498
|
+
.gallery-item:hover{
|
|
460
499
|
border-color:var(--ui-accent);transform:translateY(-2px);
|
|
461
500
|
box-shadow:0 12px 28px rgba(0,0,0,.4);
|
|
462
501
|
}
|
|
463
|
-
.gallery-item img{width:100%;height:
|
|
502
|
+
.gallery-item img{width:100%;aspect-ratio:1/1;height:auto;object-fit:cover;object-position:top center;display:block}
|
|
464
503
|
.gallery-item .cap{
|
|
465
504
|
padding:8px 12px;font-size:10px;
|
|
466
505
|
color:var(--text2);display:flex;align-items:center;gap:6px;
|
|
@@ -735,6 +774,11 @@ tr.expanded td:first-child::before{
|
|
|
735
774
|
background:repeating-linear-gradient(45deg,var(--surface2),var(--surface2) 6px,var(--bg-2) 6px,var(--bg-2) 12px);
|
|
736
775
|
}
|
|
737
776
|
.sl-thumb-empty:hover{transform:none;box-shadow:none}
|
|
777
|
+
.sl-thumb-json{
|
|
778
|
+
display:flex;align-items:center;justify-content:center;
|
|
779
|
+
color:var(--beacon);font-family:var(--mono);font-size:18px;font-weight:700;
|
|
780
|
+
background:var(--bg-2);
|
|
781
|
+
}
|
|
738
782
|
.sl-thumb-err{border-color:var(--crimson)}
|
|
739
783
|
|
|
740
784
|
.sl-info{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
@@ -797,6 +841,9 @@ tr.expanded td:first-child::before{
|
|
|
797
841
|
}
|
|
798
842
|
.sl-chip-v{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:280px}
|
|
799
843
|
.sl-chip-sel .sl-chip-v{color:var(--beacon)}
|
|
844
|
+
.sl-chip-json{cursor:pointer;border-color:var(--beacon)}
|
|
845
|
+
.sl-chip-json .sl-chip-k{color:var(--beacon)}
|
|
846
|
+
.sl-chip-json:hover{background:var(--beacon-dim)}
|
|
800
847
|
|
|
801
848
|
.sl-bar{
|
|
802
849
|
width:100%;height:3px;
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
<button class="btn sm" id="ssBlankCancelBtn">Cancel</button>
|
|
267
267
|
</div>
|
|
268
268
|
</div>
|
|
269
|
-
<div class="gallery" id="screenshotGallery"></div>
|
|
269
|
+
<div class="gallery-groups" id="screenshotGallery"></div>
|
|
270
270
|
<div class="empty" id="screenshotsEmpty" style="display:none">
|
|
271
271
|
<div class="empty-icon">▣</div>
|
|
272
272
|
<p>Select a project to view screenshots.</p>
|
|
@@ -512,7 +512,7 @@
|
|
|
512
512
|
</div>
|
|
513
513
|
</div>
|
|
514
514
|
</div>
|
|
515
|
-
<div class="modal" id="modal"><img id="modalImg" src="" alt=""></div>
|
|
515
|
+
<div class="modal" id="modal"><img id="modalImg" src="" alt=""><pre id="modalJson" hidden></pre></div>
|
|
516
516
|
|
|
517
517
|
<!-- ── Quick Search Palette ── -->
|
|
518
518
|
<div class="qs-modal" id="qsModal" aria-hidden="true">
|