kaloko 0.4.8 → 0.4.9
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 +23 -1
- package/package.json +1 -1
- package/schemas/config.v1.json +26 -0
- package/schemas/scenario.v1.json +32 -0
- package/schemas/spec.v1.json +60 -0
- package/skills/kaloko/SKILL.md +12 -2
- package/skills/kaloko/reference/acceptance.md +34 -2
- package/skills/kaloko/reference/design.md +35 -2
- package/skills/kaloko/reference/evaluation.md +46 -3
- package/skills/kaloko/reference/execution.md +18 -0
- package/skills/kaloko/reference/mcp.md +3 -1
- package/src/cli.mjs +44 -4
- package/src/lib/build.mjs +30 -5
- package/src/lib/capture.mjs +43 -1
- package/src/lib/design-cli.mjs +7 -3
- package/src/lib/devspec.mjs +821 -0
- package/src/lib/doctor.mjs +7 -0
- package/src/lib/drift.mjs +16 -8
- package/src/lib/evaluate.mjs +94 -12
- package/src/lib/keyboard.mjs +261 -0
- package/src/lib/manifest-core.mjs +16 -5
- package/src/lib/pack-help.mjs +7 -0
- package/src/lib/packs.mjs +9 -2
- package/src/lib/png.mjs +61 -0
- package/src/lib/probe.mjs +57 -0
- package/src/lib/recheck.mjs +3 -1
- package/src/lib/spec-cli.mjs +90 -0
- package/src/lib/stability-core.mjs +114 -0
- package/src/lib/stability.mjs +249 -0
- package/src/lib/vision-core.mjs +132 -0
- package/src/lib/vision.mjs +127 -0
- package/templates/kaloko.config.yml +6 -0
- package/viewer/index.html +500 -44
package/viewer/index.html
CHANGED
|
@@ -249,6 +249,8 @@ body.embed .helpwrap{display:none}
|
|
|
249
249
|
.bar-break{display:none}
|
|
250
250
|
@media (max-width:820px){.bar{flex-wrap:wrap;gap:6px 8px;padding:8px 10px}.bar > *{order:1}.bar-break{display:block;order:2;flex-basis:100%;height:0}.bar > .picker,#rpBtn,#ptBtn,#modeSeg,#locSeg,#dbar{order:3}.bar > .picker{flex:1 1 0;min-width:0;max-width:none}.task b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.task span{display:none!important}.bar .spacer{flex:1 1 auto;order:1}.picker .kchip{display:none}}
|
|
251
251
|
.legend-in{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px;padding-top:10px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted)}.legend-in span{display:inline-flex;gap:5px;align-items:center}
|
|
252
|
+
/* phones: a run with many locales scrolls its locale switch sideways instead of widening the page */
|
|
253
|
+
@media (max-width:820px){.bar .seg{max-width:100%;min-width:0;overflow-x:auto;scrollbar-width:none}.bar .seg::-webkit-scrollbar{display:none}.bar .seg button{flex:none}}
|
|
252
254
|
@media (max-width:1100px){.bar .ctl .seg-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}#viewBtn span[data-t],#rpBtn span,#ptBtn span{display:none}.picker kbd{display:none}.bar > .picker{max-width:clamp(140px,40vw,420px)}}
|
|
253
255
|
/* canvas */
|
|
254
256
|
.viewport{position:relative;overflow:hidden;cursor:grab;touch-action:none;
|
|
@@ -293,10 +295,12 @@ body.embed .helpwrap{display:none}
|
|
|
293
295
|
.node--stack .kind{background:var(--ink);color:var(--panel)}
|
|
294
296
|
.stack-fan{position:relative;display:block;width:100%;height:162px;padding:0;border:0;background:none;cursor:pointer}
|
|
295
297
|
.stack-fan img,.stack-fan .ph{position:absolute;width:58%;height:128px;object-fit:cover;object-position:top;border:1px solid var(--line);border-radius:4px;background:var(--panel-2);box-shadow:0 2px 6px rgba(0,0,0,.12)}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
/* fan positions apply to thumbnails only: with one or two samples the count badge used to be the 2nd or 3rd child and
|
|
299
|
+
got left/top on top of its right/bottom, stretching it over the whole card */
|
|
300
|
+
.stack-fan>:is(img,.ph):nth-child(1){left:5%;top:22px;transform:rotate(-5deg)}.stack-fan>:is(img,.ph):nth-child(2){left:21%;top:12px;transform:rotate(-1deg)}.stack-fan>:is(img,.ph):nth-child(3){left:37%;top:20px;transform:rotate(4deg)}
|
|
301
|
+
.stack-fan .count{position:absolute;right:10px;bottom:6px;left:auto;top:auto;transform:none;z-index:1;line-height:1.4;background:var(--ink);color:var(--panel);font-weight:700;font-size:12px;border-radius:999px;padding:2px 9px}
|
|
298
302
|
.stack-bar{display:flex;height:6px;border-radius:3px;overflow:hidden;margin:6px 10px 2px;background:var(--panel-2)}.stack-bar i{display:block}
|
|
299
|
-
.ri{grid-column:2;min-width:0;min-height:0;display:grid;background:var(--panel)}.ri[hidden]{display:none}
|
|
303
|
+
.ri{grid-column:2;min-width:0;min-height:0;display:grid;grid-template-columns:minmax(0,1fr);background:var(--panel)}.ri[hidden]{display:none}
|
|
300
304
|
.ri .si__box{border-radius:0;box-shadow:none}
|
|
301
305
|
.ri-crumbs{display:flex;gap:6px;align-items:center;min-width:0;font-size:13px;color:var(--muted)}.ri-crumbs button{all:unset;cursor:pointer;color:var(--ink);text-decoration:underline;text-underline-offset:3px}.ri-crumbs b{color:var(--ink);font-size:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
302
306
|
.ri-group{position:absolute;left:0;right:0;height:36px;display:flex;align-items:center;gap:10px;padding:0 18px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);background:var(--panel-2);border-bottom:1px solid var(--line)}.ri-group small{text-transform:none;letter-spacing:0}
|
|
@@ -304,12 +308,11 @@ body.embed .helpwrap{display:none}
|
|
|
304
308
|
.ri-row.is-page{padding-left:42px}.ri-row.is-page .t b{font-weight:500}
|
|
305
309
|
.ri-row .t b .kind{display:inline-grid;vertical-align:-3px;margin-right:6px}
|
|
306
310
|
.ri-row .num{font-family:var(--mono,monospace);font-size:12.5px;text-align:right}
|
|
307
|
-
.ri-fan{position:relative;width:96px;height:58px}.ri-fan img,.ri-fan .ph{position:absolute;width:84px!important;height:50px!important}.ri-fan>:nth-child(1){left:0;top:8px;z-index:3}.ri-fan>:nth-child(2){left:6px;top:4px;z-index:2}.ri-fan>:nth-child(3){left:12px;top:0;z-index:1}
|
|
311
|
+
.ri-fan{position:relative;width:96px;height:58px}.ri-fan img,.ri-fan .ph{position:absolute;width:84px!important;height:50px!important}.ri-fan>:is(img,.ph):nth-child(1){left:0;top:8px;z-index:3}.ri-fan>:is(img,.ph):nth-child(2){left:6px;top:4px;z-index:2}.ri-fan>:is(img,.ph):nth-child(3){left:12px;top:0;z-index:1}
|
|
308
312
|
.ri-fan .cnt{position:absolute;right:0;bottom:0;z-index:4;font-size:10.5px;font-weight:700;background:var(--ink);color:var(--panel);border-radius:4px;padding:0 5px}
|
|
309
313
|
.ri-x{display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:12px;color:var(--muted)}
|
|
310
|
-
|
|
311
|
-
.
|
|
312
|
-
.si__box{background:var(--panel);border-radius:10px;display:grid;grid-template-rows:auto auto 1fr auto;min-height:0;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.35)}
|
|
314
|
+
.si{position:fixed;inset:0;z-index:19;background:rgba(12,12,11,.62);display:grid;grid-template-columns:minmax(0,1fr);padding:clamp(0px,2vw,24px)}
|
|
315
|
+
.si__box{background:var(--panel);border-radius:10px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto 1fr auto;min-height:0;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.35)}
|
|
313
316
|
.si__head{display:flex;gap:12px;align-items:center;padding:14px 18px;border-bottom:1px solid var(--line)}.si__head h3{margin:0;font-size:18px}
|
|
314
317
|
.si__tools{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:10px 18px;border-bottom:1px solid var(--line)}
|
|
315
318
|
.si__tools input[type=search]{flex:1 1 240px;min-width:180px;padding:7px 10px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);font:inherit}
|
|
@@ -326,6 +329,31 @@ body.embed .helpwrap{display:none}
|
|
|
326
329
|
.si-card img,.si-card .ph{width:100%;height:120px}.si-card b{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.si-card small{color:var(--muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
327
330
|
.si__foot{display:flex;gap:10px;align-items:center;padding:8px 18px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted)}
|
|
328
331
|
.si__empty{padding:40px;text-align:center;color:var(--muted)}
|
|
332
|
+
.si__title{flex:1;min-width:0}
|
|
333
|
+
/* phones: placed after the base rules above, which used to win and keep the 96 px thumbnail over the title.
|
|
334
|
+
Row: thumbnail | title and path; under them the status and the criteria that need attention */
|
|
335
|
+
@media (max-width:760px){
|
|
336
|
+
.si{padding:0;grid-template-columns:minmax(0,1fr)}.si__box{border-radius:0}
|
|
337
|
+
.si__head{flex-wrap:wrap;gap:8px 10px;padding:10px 12px 10px 14px}
|
|
338
|
+
.si__title{flex:1 1 calc(100% - 84px)}
|
|
339
|
+
.si__head h3,.ri-crumbs b{font-size:17px;line-height:1.25;overflow-wrap:anywhere}.ri-crumbs b{white-space:normal}
|
|
340
|
+
.si__head .seg,#siCsv,#riCsv{order:2}#siClose{order:1;align-self:flex-start;width:32px;height:32px}
|
|
341
|
+
.si__tools{padding:8px 14px;gap:6px 8px}
|
|
342
|
+
.si__tools input[type=search]{flex:1 1 100%;min-width:0;font-size:16px}
|
|
343
|
+
.si__tools select{flex:1 1 150px;min-width:0;max-width:100%}
|
|
344
|
+
.si__foot{padding:8px 14px}.si__foot>:last-child{display:none}
|
|
345
|
+
.ri-row,.ri-head,.si-row,.si-headrow{grid-template-columns:64px max-content minmax(0,1fr)!important;column-gap:10px;padding-left:14px;padding-right:14px}
|
|
346
|
+
.ri-row.is-page{padding-left:28px}
|
|
347
|
+
.ri-head>:nth-child(n+3),.si-headrow>:nth-child(n+3){display:none}.ri-head>:nth-child(2),.si-headrow>:nth-child(2){grid-column:2/4}
|
|
348
|
+
.ri-row>:nth-child(n+4),.si-row>:nth-child(n+4){display:none}
|
|
349
|
+
.ri-row,.si-row{grid-template-rows:auto auto;row-gap:4px;align-content:center}
|
|
350
|
+
.ri-row>:first-child,.si-row>:first-child{grid-row:1/3;grid-column:1}
|
|
351
|
+
.ri-row>.t,.si-row>.t{grid-column:2/4;grid-row:1}
|
|
352
|
+
.ri-row>.badge,.si-row>.badge{grid-column:2;grid-row:2;justify-self:start}
|
|
353
|
+
.ri-row>.si-fails,.si-row>.si-fails{display:flex!important;grid-column:3;grid-row:2;overflow:hidden;flex-wrap:nowrap;min-width:0}
|
|
354
|
+
.ri-row img,.ri-row .ph,.si-row img,.si-row .ph{width:64px;height:40px}
|
|
355
|
+
.ri-fan{width:64px;height:40px}.ri-fan img,.ri-fan .ph{width:54px!important;height:32px!important}
|
|
356
|
+
}
|
|
329
357
|
.node__head{display:flex;align-items:flex-start;gap:8px;padding:9px 10px 8px}
|
|
330
358
|
.kind{width:22px;height:22px;display:grid;place-items:center;border-radius:5px;background:var(--panel-2);color:var(--muted);flex:none}
|
|
331
359
|
.node--email .kind{background:var(--accent);color:var(--accent-ink)}
|
|
@@ -385,7 +413,7 @@ body.embed .helpwrap{display:none}
|
|
|
385
413
|
.lb__head{padding:16px 18px 12px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:8px;position:sticky;top:0;background:var(--panel);z-index:1}
|
|
386
414
|
.lb__head h3{margin:0;font-size:19px;line-height:1.25;letter-spacing:-.01em;text-wrap:balance}
|
|
387
415
|
.lb__row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
388
|
-
.lb__nav{display:flex;gap:6px;margin-left:auto}
|
|
416
|
+
.lb__nav{display:flex;gap:6px;margin-left:auto}.lb__meta{display:contents}
|
|
389
417
|
.sec{padding:14px 18px;border-bottom:1px solid var(--line)}
|
|
390
418
|
.sec h4{margin:0 0 8px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:600;display:flex;align-items:center;gap:6px}
|
|
391
419
|
.purpose{margin:0;max-width:41.3em}
|
|
@@ -401,7 +429,7 @@ body.embed .helpwrap{display:none}
|
|
|
401
429
|
.k-err{color:#ff7a85;font-weight:600}.k-url{color:#8ab4ff}.k-pos{color:#ffd166}.k-str{color:#9ee493}.k-at{color:#8d8d86}.k-num{color:#ffd166}
|
|
402
430
|
.vhead{display:flex;align-items:center;gap:8px}.vhead .badge{text-transform:none;letter-spacing:0}.vmore{display:flex;flex-wrap:wrap;gap:4px 16px;margin-top:10px;font-size:12.5px}.vmore .linkish{color:var(--muted)}.vmore .linkish:hover{color:var(--ink)}
|
|
403
431
|
/* how a criterion was checked: kind, meaning, AI score between the thresholds */
|
|
404
|
-
.how{grid-column:2;display:grid;gap:4px;margin-top:4px;font-size:12.5px}.how__kind{justify-self:start;font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:1px 7px}.how--ai{color:var(--ink);border-color:var(--ink)}.how__q{color:var(--ink)}.how__line{color:var(--muted)}
|
|
432
|
+
.how{grid-column:2;display:grid;gap:4px;margin-top:4px;font-size:12.5px}.how__kind{justify-self:start;font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:1px 7px}.how--ai{color:var(--ink);border-color:var(--ink)}.how__q{color:var(--ink)}.how__line{color:var(--muted)}.how__why{color:var(--ink)}.how__why b{font-weight:600;margin-right:4px}
|
|
405
433
|
.gauge{position:relative;display:block;height:8px;border-radius:4px;background:var(--skip-bg);overflow:visible;margin:2px 0}.gauge__lo{position:absolute;left:0;top:0;bottom:0;background:var(--fail-bg);border-radius:4px 0 0 4px}.gauge__hi{position:absolute;right:0;top:0;bottom:0;background:var(--pass-bg);border-radius:0 4px 4px 0}.gauge b{position:absolute;top:-3px;width:3px;height:14px;margin-left:-1.5px;background:var(--ink);border-radius:2px}
|
|
406
434
|
.ac li .tech{grid-column:2;font-size:11.5px}.ac li .tech summary{cursor:pointer;color:var(--muted)}
|
|
407
435
|
.ac{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
|
|
@@ -442,6 +470,12 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
442
470
|
#treeSec summary{cursor:pointer;list-style:none}#treeSec summary h4{display:inline}
|
|
443
471
|
.hlbox{position:absolute;outline:3px solid #ffd200;background:rgba(255,228,82,.22);pointer-events:none;border-radius:2px}
|
|
444
472
|
.hlbox[hidden]{display:none}
|
|
473
|
+
/* regions left out of pixel diffs, regions that moved between shots (#20) and tab stops of the keyboard walk (#19) */
|
|
474
|
+
.ignbox{position:absolute;border:1.5px dashed #6b6b66;background:repeating-linear-gradient(45deg,rgba(107,107,102,.16) 0 6px,transparent 6px 12px);pointer-events:none;z-index:4}
|
|
475
|
+
.ignbox.unst{border-color:#d9822b;background:repeating-linear-gradient(45deg,rgba(217,130,43,.14) 0 6px,transparent 6px 12px)}
|
|
476
|
+
.kbstop{position:absolute;outline:2px solid #2f6fde;pointer-events:none;z-index:4}.kbstop.bad{outline-color:#e4572e}
|
|
477
|
+
.kbstop i{position:absolute;left:-2px;top:-18px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:#2f6fde;color:#fff;font:600 10px/16px system-ui,sans-serif;font-style:normal;text-align:center}.kbstop.bad i{background:#e4572e}
|
|
478
|
+
.kb-list{margin:6px 0 0;padding-left:20px;font-size:12.5px;color:var(--muted)}.kb-list .bad{color:var(--ink)}
|
|
445
479
|
.hlbox.on{outline:3px solid #ffd200;background:transparent}
|
|
446
480
|
.hlbox.on.first{box-shadow:0 0 0 9999px rgba(15,15,14,.42)}
|
|
447
481
|
.elabel{z-index:4;white-space:normal;max-width:180px;text-align:center;line-height:1.25;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
|
@@ -570,6 +604,27 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
570
604
|
@media (max-width:700px){.pt__title{max-width:60vw}.pt__bar{padding:6px 10px}.pt__stage{padding:6px}}
|
|
571
605
|
@media (prefers-reduced-motion:no-preference){.pt__dev{transition:transform .15s ease-out}}
|
|
572
606
|
.insp{font:12px/1.4 var(--mono)}
|
|
607
|
+
.dev h5{margin:14px 0 4px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
608
|
+
.dev table{width:100%;border-collapse:collapse;font:12px/1.45 var(--mono)}.dev td{padding:4px;border-bottom:1px solid var(--line);vertical-align:top}.dev td:first-child{color:var(--muted);white-space:nowrap;width:1%}.dev td:last-child{width:1%}
|
|
609
|
+
.dev tr.devchg td{background:color-mix(in srgb,var(--accent) 30%,transparent)}
|
|
610
|
+
.devtabs{margin:0 0 10px}.devhead{margin:0 0 8px}.devhead b{font-size:14px}
|
|
611
|
+
.devval{display:inline-flex;align-items:center;gap:6px;overflow-wrap:anywhere}
|
|
612
|
+
.devsw{display:inline-block;width:14px;height:14px;border-radius:3px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);flex:none;vertical-align:-2px;margin-right:4px}
|
|
613
|
+
.devtokrow{margin-top:2px;display:flex;flex-wrap:wrap;gap:4px;align-items:center}
|
|
614
|
+
.devtok{all:unset;cursor:pointer;color:#2e6bff;font:inherit;border-radius:3px;padding:0 2px}.devtok:hover,.devtok:focus-visible{background:color-mix(in srgb,#2e6bff 14%,transparent);outline:none}
|
|
615
|
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .devtok{color:#8fb0ff}}:root[data-theme="dark"] .devtok{color:#8fb0ff}
|
|
616
|
+
.devmode{font:600 10px/16px system-ui,sans-serif;padding:0 5px;border-radius:4px;background:var(--panel-2);color:var(--muted)}
|
|
617
|
+
.devwarn{color:var(--fail-ink);font-weight:600}.devok{color:var(--pass-ink);font-weight:600}.devmuted{color:var(--muted)}
|
|
618
|
+
.devline{width:26px;height:26px}.devline .i{width:14px;height:14px}
|
|
619
|
+
.devnav{display:flex;flex-wrap:wrap;gap:6px;margin:4px 0 6px}.devnav .btn{padding:4px 8px;font-size:12px}
|
|
620
|
+
.devstates{flex-wrap:wrap}.devstates button[disabled]{opacity:.4;cursor:not-allowed}
|
|
621
|
+
.devdist p{margin:4px 0;font-size:13px}.devdist code{font:12px var(--mono)}
|
|
622
|
+
.devassets{list-style:none;margin:0;padding:0}.devassets li{display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid var(--line);font-size:12.5px}.devassets img{width:28px;height:28px;object-fit:contain;background:#fff;border-radius:4px;box-shadow:inset 0 0 0 1px var(--line)}.devassets__k{font:600 10px/28px system-ui,sans-serif;width:28px;text-align:center;background:var(--panel-2);border-radius:4px}.devassets__n{flex:1;min-width:0;overflow-wrap:anywhere}
|
|
623
|
+
.devcopy{display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 6px}
|
|
624
|
+
.devtoks{list-style:none;margin:0;padding:0}.devtoks li{border-bottom:1px solid var(--line)}.devtoks button{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:8px;width:100%;padding:5px 4px;cursor:pointer;font-size:12.5px}.devtoks button:hover,.devtoks button:focus-visible{background:var(--panel-2)}.devtoks button[aria-pressed="true"]{background:color-mix(in srgb,#c026d3 14%,transparent)}
|
|
625
|
+
.devtoks__n{font:12px var(--mono);overflow-wrap:anywhere}.devtoks__c{margin-left:auto;color:var(--muted);font-variant-numeric:tabular-nums}.devtoks--plain li{padding:5px 4px;font-size:12.5px;overflow-wrap:anywhere}.devunused{margin-top:12px}.devunused summary{cursor:pointer;font-size:12.5px;color:var(--muted)}
|
|
626
|
+
.devshot{position:absolute;z-index:4;cursor:crosshair}.devshot > div{position:absolute;pointer-events:none;box-sizing:border-box}.devshot__hov{outline:1px solid #2e6bff;background:rgba(111,168,220,.25)}.devshot__sel{outline:2px solid #2e6bff}.devshot__tgt{outline:1px dashed #e4572e}.devshot__hl{outline:2px dashed #c026d3;background:rgba(192,38,211,.08)}
|
|
627
|
+
.devshot span{position:absolute;left:0;top:-19px;font:600 11px/18px system-ui,sans-serif;background:#151514;color:#fff;padding:0 6px;border-radius:4px;white-space:nowrap}.devshot__sel span{background:#2e6bff}
|
|
573
628
|
.insp table{width:100%;border-collapse:collapse}.insp td{padding:3px 4px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
574
629
|
.insp td:first-child{color:var(--muted);white-space:nowrap}
|
|
575
630
|
.insp .tok{color:#2e6bff}.insp .raw{color:#c73e1d}
|
|
@@ -615,7 +670,13 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
615
670
|
.lb__head h3{font-size:16px}
|
|
616
671
|
.lb[data-sheet="peek"] .lb__head h3{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
617
672
|
.lb[data-sheet="peek"] .lb__side:not(.is-drag) .lbtabs,.lb[data-sheet="peek"] .lb__side:not(.is-drag) .lbtab{visibility:hidden}
|
|
618
|
-
.
|
|
673
|
+
/* the header row keeps its actions at the top right; status, position and "back to index" wrap on the left.
|
|
674
|
+
The tabs stick under the header (its height is --lbhead), so the two never cover each other */
|
|
675
|
+
.lb__row{flex-wrap:nowrap;align-items:flex-start}
|
|
676
|
+
.lb__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;flex:1;min-width:0;min-height:32px}
|
|
677
|
+
.lb__meta #lbBack{padding:4px 10px}
|
|
678
|
+
.lb__nav{flex:none;gap:4px}.lb__nav .icon-btn{width:32px;height:32px}
|
|
679
|
+
.lbtabs{top:calc(16px + var(--lbhead,0px))}
|
|
619
680
|
}
|
|
620
681
|
@media (max-width:359px){.lb__tools #lbFocus span{display:none}}
|
|
621
682
|
@media (max-width:700px) and (prefers-reduced-motion:no-preference){.lb__side{transition:height .2s ease}.lb__side.is-drag{transition:none}}
|
|
@@ -638,7 +699,10 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
638
699
|
.lb.is-focus.is-idle .lb__fshow{display:grid}
|
|
639
700
|
@media (prefers-reduced-motion:no-preference){.lb__fbar{transition:opacity .2s ease}}
|
|
640
701
|
/* ---------- review mode (Kontrola): one step at a time from the queue of steps that need a person ---------- */
|
|
641
|
-
.rvm{position:fixed;inset:0;z-index:23;background:var(--ground);display:grid;grid-template-rows:auto 1fr auto}
|
|
702
|
+
.rvm{position:fixed;inset:0;z-index:23;background:var(--ground);display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto 1fr auto}
|
|
703
|
+
/* many locales: the switches scroll sideways instead of widening review mode past the screen */
|
|
704
|
+
.rvm__top .seg{max-width:100%;min-width:0;overflow-x:auto;scrollbar-width:none}.rvm__top .seg::-webkit-scrollbar{display:none}.rvm__top .seg button{flex:none}
|
|
705
|
+
.rvm__foot .note{min-width:0;overflow-wrap:anywhere}
|
|
642
706
|
.rvm__top{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--line-strong)}
|
|
643
707
|
.rvm__top .spacer,.rvm__foot .spacer{flex:1}
|
|
644
708
|
.rvm__prog{font-size:13px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
@@ -763,13 +827,14 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
763
827
|
const EMBEDDED = /*__QAWALK_RUN__*/null;
|
|
764
828
|
if (!window.QAWALK_APP) { const el = document.getElementById('qawalk-app'); if (el) { try { window.QAWALK_APP = JSON.parse(el.textContent); } catch {} } }
|
|
765
829
|
const I18N = {
|
|
766
|
-
cs: { makePublicVersion:'Zveřejnit verzi', draft:'Koncept', liveShared:'živě, sdíleno', publishHint:'Publikování uloží celé flow jako %V; nezměněné kroky se použijí znovu.', htmlView:'HTML pohled', htmlViewNote:'Statická kopie stránky: bez skriptů webu, s přenesenými styly a obrázky, bez tajných údajů.', editTitle:'Upravit na plachtě', editText:'Text', editKeep:'ponechat', editSave:'Uložit úpravu', editSaved:'Úprava uložena jako nová revize', editRebased:'Krok se mezitím změnil; úprava se uložila do nejnovější revize', editUnapplied:'Neuplatněno (prvek už neexistuje)', staleCapture:'Snímek je z předchozí revize; Živě ukazuje poslední úpravu.', branches:'Větve', mergeBranch:'Sloučit do main', mergeConflict:'Krok se změnil v obou větvích:', mergeTakeBranch:'Vzít z větve? (Zrušit = ponechat main)', branchMerged:'Sloučeno', acceptVersion:'Přijmout verzi', returnVersion:'Vrátit verzi', versionVerdict:'Verdikt verze', draftInfo:'Koncept je živý a sdílený: kdo ho má otevřený, vidí změny hned, i ty od agentů. Kroky ukládá kaloko sync nebo úprava na plachtě; tlačítko Publikovat verzi uloží celé flow jako novou verzi.', changedSince:'Změněno od', noChangeSince:'Beze změny od', designEnv:'design', version:'Verze', versions:'Verze flow', publish:'Publikovat verzi', publishName:'Název verze (nepovinný)', published:'Publikováno', designRef:'reference designu', setDesignRef:'Nastavit jako referenci designu', unsetDesignRef:'Zrušit referenci designu', designRefHint:'Implementace se kontroluje proti referenci designu (pack design).', openDraft:'Otevřít koncept', liveMode:'Živě', shotMode:'Snímek', inspect:'Inspektor', pinComment:'Komentář k prvku', pickHint:'Klikněte na prvek v náhledu.', pinnedOn:'K prvku', unchangedSince:'beze změny od', unchangedSteps:'kroků beze změny', changedOnly:'Jen změněné', stepStatus:'Stav kroku', st_in_progress:'rozpracováno', st_ready_for_review:'k revizi', st_ready_for_dev:'připraveno pro vývoj', present:'Prezentovat', presenting:'Prezentujete', stopPresent:'Ukončit prezentaci', updatedSteps:'upravil', viaAgent:'přes', here:'tady', copyCss:'Kopírovat CSS', cssCopied:'CSS zkopírováno', noToken:'mimo sadu tokenů', tokenSet:'token', revisionBy:'revize', noLiveOrigin:'Živé kroky nejsou na této instanci zapnuté.', draftEmpty:'Koncept zatím nemá žádné kroky; spusťte kaloko sync.', prototypeTo:'Prototyp: přechod na', outdated:'zastaralé', prototype:'Prototyp', protoTitle:'Prezentovat prototyp', protoHint:'Klikejte v prototypu; ← → předchozí / další krok, Esc konec.', protoShot:'Jen snímky: živé kroky tu nejsou k dispozici. ← → procházejí flow.', protoLocal:'Odkazy otevírají stránky z disku; ← → procházejí flow.', protoDevice:'Rámeček zařízení', protoFull:'Celá obrazovka', protoExit:'Ukončit prototyp', protoNoStep:'Odkaz nevede na žádný krok flow', focus:'Jen náhled', focusTitle:'Ukázat jen náhled přes celou obrazovku', focusExit:'Zpět do detailu', focusBar:'Ovládání náhledu', focusShow:'Ukázat ovládání', moreTools:'Další nástroje', sheetHandle:'Detail kroku: klepněte nebo táhněte', hint:'Táhněte pro posun · Ctrl + kolečko zoom · <span class="kbd">F</span> režim soustředění (v detailu jen náhled) · <span class="kbd">K</span> scénáře a běhy · <span class="kbd">R</span> projít · <span class="kbd">P</span> prototyp · <span class="kbd">I</span> mapa / index · <span class="kbd">←</span><span class="kbd">→</span> předchozí / další krok (v detailu) · <span class="kbd">Esc</span> zavřít', locale:'Testovaný jazyk', view:'Náhled', both:'Oba', shownRun:'Zobrazený běh', allRuns:'Všechny běhy', steps:'Kroky flow', fit:'Celé flow', stepsWord:'kroků', passedWord:'prošlo', fitLane:'Zobrazit celou sekci', following:'Sledujete', followEsc:'Esc ukončí', followHint:'Sledovat, co dělá', visOn:'Vidí mě ostatní', visOff:'Jsem skrytý',
|
|
830
|
+
cs: { makePublicVersion:'Zveřejnit verzi', draft:'Koncept', liveShared:'živě, sdíleno', publishHint:'Publikování uloží celé flow jako %V; nezměněné kroky se použijí znovu.', htmlView:'HTML pohled', htmlViewNote:'Statická kopie stránky: bez skriptů webu, s přenesenými styly a obrázky, bez tajných údajů.', editTitle:'Upravit na plachtě', editText:'Text', editKeep:'ponechat', editSave:'Uložit úpravu', editSaved:'Úprava uložena jako nová revize', editRebased:'Krok se mezitím změnil; úprava se uložila do nejnovější revize', editUnapplied:'Neuplatněno (prvek už neexistuje)', staleCapture:'Snímek je z předchozí revize; Živě ukazuje poslední úpravu.', branches:'Větve', mergeBranch:'Sloučit do main', mergeConflict:'Krok se změnil v obou větvích:', mergeTakeBranch:'Vzít z větve? (Zrušit = ponechat main)', branchMerged:'Sloučeno', acceptVersion:'Přijmout verzi', returnVersion:'Vrátit verzi', versionVerdict:'Verdikt verze', draftInfo:'Koncept je živý a sdílený: kdo ho má otevřený, vidí změny hned, i ty od agentů. Kroky ukládá kaloko sync nebo úprava na plachtě; tlačítko Publikovat verzi uloží celé flow jako novou verzi.', changedSince:'Změněno od', noChangeSince:'Beze změny od', designEnv:'design', version:'Verze', versions:'Verze flow', publish:'Publikovat verzi', publishName:'Název verze (nepovinný)', published:'Publikováno', designRef:'reference designu', setDesignRef:'Nastavit jako referenci designu', unsetDesignRef:'Zrušit referenci designu', designRefHint:'Implementace se kontroluje proti referenci designu (pack design).', openDraft:'Otevřít koncept', liveMode:'Živě', shotMode:'Snímek', inspect:'Inspektor', pinComment:'Komentář k prvku', pickHint:'Klikněte na prvek v náhledu.', pinnedOn:'K prvku', unchangedSince:'beze změny od', unchangedSteps:'kroků beze změny', changedOnly:'Jen změněné', stepStatus:'Stav kroku', st_in_progress:'rozpracováno', st_ready_for_review:'k revizi', st_ready_for_dev:'připraveno pro vývoj', present:'Prezentovat', presenting:'Prezentujete', stopPresent:'Ukončit prezentaci', updatedSteps:'upravil', viaAgent:'přes', here:'tady', copyCss:'Kopírovat CSS', cssCopied:'CSS zkopírováno', noToken:'mimo sadu tokenů', tokenSet:'token', revisionBy:'revize', noLiveOrigin:'Živé kroky nejsou na této instanci zapnuté.', draftEmpty:'Koncept zatím nemá žádné kroky; spusťte kaloko sync.', prototypeTo:'Prototyp: přechod na', outdated:'zastaralé', prototype:'Prototyp', protoTitle:'Prezentovat prototyp', protoHint:'Klikejte v prototypu; ← → předchozí / další krok, Esc konec.', protoShot:'Jen snímky: živé kroky tu nejsou k dispozici. ← → procházejí flow.', protoLocal:'Odkazy otevírají stránky z disku; ← → procházejí flow.', protoDevice:'Rámeček zařízení', protoFull:'Celá obrazovka', protoExit:'Ukončit prototyp', protoNoStep:'Odkaz nevede na žádný krok flow', focus:'Jen náhled', focusTitle:'Ukázat jen náhled přes celou obrazovku', focusExit:'Zpět do detailu', focusBar:'Ovládání náhledu', focusShow:'Ukázat ovládání', moreTools:'Další nástroje', sheetHandle:'Detail kroku: klepněte nebo táhněte', hint:'Táhněte pro posun · Ctrl + kolečko zoom · <span class="kbd">F</span> režim soustředění (v detailu jen náhled) · <span class="kbd">K</span> scénáře a běhy · <span class="kbd">R</span> projít · <span class="kbd">P</span> prototyp · <span class="kbd">D</span> Dev mode (v detailu) · <span class="kbd">I</span> mapa / index · <span class="kbd">←</span><span class="kbd">→</span> předchozí / další krok (v detailu) · <span class="kbd">Esc</span> zavřít', locale:'Testovaný jazyk', view:'Náhled', both:'Oba', shownRun:'Zobrazený běh', allRuns:'Všechny běhy', steps:'Kroky flow', fit:'Celé flow', stepsWord:'kroků', passedWord:'prošlo', fitLane:'Zobrazit celou sekci', following:'Sledujete', followEsc:'Esc ukončí', followHint:'Sledovat, co dělá', visOn:'Vidí mě ostatní', visOff:'Jsem skrytý',
|
|
767
831
|
pass:'Splněno', partial:'Částečně', fail:'Nesplněno', skip:'Neprověřeno', crop:'výřez', html:'HTML snapshot', email:'e-mail', external:'třetí strana',
|
|
768
832
|
branch:'Větev', commit:'Commit', author:'Autor', pr:'PR', issue:'Issue', env:'Prostředí', duration:'Délka', noPr:'bez PR', open:'otevřený', merged:'sloučený', closed:'zavřený',
|
|
769
833
|
older:'Starší běh', newer:'Novější běh', purpose:'Účel obrazovky ze scénáře', criteria:'Akceptační kritéria', jevTitle:'Sémantické hodnocení',
|
|
770
834
|
jevNote:'Evaluátor dostal zjednodušený stav stránky a ke každé otázce vrátil pravděpodobnost. Verdikt podle prahů a důkazy doplnil runner.', evalLabel:'Hodnocení', metaTitle:'Metadata stránky', mdBasic:'Základ', mdHeadings:'Osnova nadpisů', mdHreflang:'Jazykové verze (hreflang)', mdOg:'Open Graph', mdTwitter:'Twitter / X', mdLd:'Strukturovaná data · JSON-LD', mdMicro:'Strukturovaná data · microdata', mdLinks:'Další odkazy v hlavičce', mdOther:'Ostatní meta', missing:'Chybí', duplicate:'duplicitní', size:'Velikost', runVerdict:'Verdikt běhu', accept:'Přijmout běh', ret:'Vrátit běh', pending:'čeká', accepted:'přijato', returned:'vráceno', baseline:'Referenční běh', setBaseline:'Nastavit jako referenční', unsetBaseline:'Zrušit referenci', verdictNotePh:'Poznámka k verdiktu (u vrácení povinná)', manualPass:'Splněno', manualFail:'Nesplněno', manualNotePh:'Důvod (u nesplněno povinný)', reviewerVerdict:'verdikt kontrolujícího', decideManual:'Rozhodnout ručně', consoleErrors:'Chyby konzole', consoleLoading:'Načítám zprávy…', noConsole:'U tohoto snímku se zprávy konzole neuložily.', consoleMore:(n) => `Dalších ${n} se neuložilo (ukládá se prvních 20).`, blockedTitle:'Zablokované požadavky', copyAll:'Kopírovat vše', copyOne:'Kopírovat tuto zprávu', copied:'Zkopírováno', tabOverview:'Kritéria', tabPage:'Stránka', tabTech:'Technické', toSolve:(n) => `k řešení ${n}`, passedN:(n) => `splněno ${n}`, whyMatters:'Proč na tom záleží:', howFix:'Jak opravit:', whyAndFix:'Proč na tom záleží a jak to opravit', aiShort:(p) => `AI ${p} % ano`, noneFailed:'Nic neselhalo.', toDecide:'Rozhodne člověk', toDecideLead:'AI si nebyla jistá nebo nemohla vyhodnotit; otevřete krok, podívejte se na snímek a rozhodněte.', worthLook:'Stojí za kontrolu', jevScoreHint:'Průměrná jistota AI, že sémantická kritéria této obrazovky jsou splněná', sigTitle:'Rychlé signály', sigLead:'AI se na ně ptá u každé obrazovky spolu s kritérii. Výsledek nemění, jen ukáže obrazovky, na které se vyplatí podívat.', sigPurpose:'Plní účel kroku', sigLanguage:'Text v očekávaném jazyce', sigBroken:'Vypadá rozbitě (chyba, zástupný text, prázdno)', sigWarn:(n) => `Signály k prověření: ${n}`, sigYes:(p) => `${p} % ano`,
|
|
771
835
|
howAuto:'Automatická kontrola', howAi:'Vyhodnocení AI', howManual:'Rozhoduje člověk', howTech:'Technický detail',
|
|
772
836
|
aiPass:(p, t) => `Evaluátor si je na ${p} % jistý, že odpověď je ano; projde od ${t} %.`, aiFail:(p, t) => `Jen ${p} % ano; neprojde při ${t} % a méně.`, aiUnsure:(p, lo, hi) => `Nejisté: ${p} % ano, mezi ${lo} % a ${hi} %. Podívejte se na snímek a rozhodněte níže.`,
|
|
837
|
+
aiWhy:'Proč:', aiSaw:'AI viděla i snímek', kbTitle:'Klávesnice', kbLead:(n, all) => all ? `Tabulátor projde všechny prvky (${n}) a opustí stránku.` : `Zastávky tabulátoru, prvních ${n}.`, kbShow:'Ukázat pořadí na snímku', kbHide:'Skrýt pořadí', kbNone:'bez viditelného fokusu', kbWeak:(c) => `slabý fokus ${c} : 1`, kbHidden:'skrytý prvek', kbCovered:'zakrytý', ignTitle:'Ignorované oblasti', ignLead:(n) => `Oblasti, které se nepočítají do pixelového rozdílu (hodiny, reklamy, karusely): ${n}.`, unstLead:(n) => `Oblasti, které se mezi opakovanými snímky hýbaly: ${n}. Příkaz kaloko stability z nich navrhne ignore.`,
|
|
773
838
|
aiNotEval:'Zatím nevyhodnoceno.', aiVia:{ qawalk:'vyhodnotilo Kaloko', 'own-key':'vyhodnoceno vaším klíčem' }, aiAsked:'Otázka',
|
|
774
839
|
a_exists:(s) => `${s} je na stránce`, a_absent:(s) => `${s} na stránce není`, a_count:(s, mn, mx) => `${s} se vyskytuje ${mn != null && mn === mx ? `přesně ${mn}×` : mn != null && mx != null ? `${mn}–${mx}×` : mn != null ? `aspoň ${mn}×` : mx != null ? `nejvýš ${mx}×` : ''}`, a_text_matches:(s, p) => `${s || 'Stránka'} obsahuje ${p}`, a_text_absent:(s, p) => `${s || 'Stránka'} neobsahuje ${p}`, a_attr_equals:(s, a, v) => `${s} má ${a} = ${v}`,
|
|
775
840
|
a_no_overflow:'Nic nepřetéká šířku obrazovky', a_no_console_errors:'V konzoli prohlížeče nejsou chyby', a_title_matches:(p) => `Titulek stránky odpovídá ${p}`, a_meta_matches:(s, p) => `Metadata ${s} odpovídají ${p}`, a_link_matches:(s) => `Odkazy ${s} vedou, kam mají`, a_http_status:(v) => `Server odpovídá ${v || '2xx'}`, a_link_home:'Stránka odkazuje zpět na úvod', a_links_ok:'Interní odkazy fungují (žádné 404)', a_hreflang_pairs:'Jazykové verze (hreflang) na sebe vzájemně odkazují',
|
|
@@ -787,13 +852,14 @@ const I18N = {
|
|
|
787
852
|
downloadZip:'Stáhnout ZIP', downloadZipNote:'Všechny soubory v otevřených formátech a plachta, která se otevře offline z disku', filesWord:'souborů', publicAccess:'veřejný odkaz', orgAccess:'přístup po přihlášení do organizace', projectAll:'Všechny projekty', packNote:'Kritérium z balíčku kontrol (packs ve scénáři)', coverage:'Pokrytí plánu', coverageLine:'kritérií z ACCEPTANCE.md má kontrolu', coverageExtra:'kontrol mimo plán', coverageFull:'Každé plánované kritérium má kontrolu.', coverageSteps:'Kroky v plánu bez uzlu', flakyTitle:'Nestabilní kritéria', flaky:'nestabilní', flakyNote:'Verdikt se liší mezi běhy na stejném commitu a prostředí.', people:'Lidé ve flow', peopleHint:'Kliknutím na osobu zvýrazníte její kroky; přerušované šipky ukazují předání mezi lidmi.', awaiting:'Čeká na schválení od', approvedBy:'schválili', approvedByAll:'Schválili všichni povinní schvalovatelé.', viaToken:'přes token', viaAgent:'přes agenta', approvedByTokens:'přes token nebo agenta', publicRun:'Veřejný běh', publicNote:'Plachta je čitelná bez přihlášení; jména hodnotitelů jsou skrytá.', makePublic:'Zveřejnit běh', makePrivate:'Zrušit zveřejnění', publicHint:'Jen pro scénáře bez osobních údajů', publicConfirm:'Zveřejnit tento běh? Uvidí ho kdokoli s odkazem, bez přihlášení. Použijte jen u scénářů bez osobních údajů.', openInApp:'Otevřít v Kaloku', signInLink:'Přihlásit', replay:'Projít', rpStep:'Krok', rpNext:'Další', rpPrev:'Zpět', rpEnd:'Konec průchodu.', rpRestart:'Znovu od začátku', rpChoose:'Kudy dál?', rpOr:'nebo', rpKeys:'← → krok · čísla volí větev · Esc zavře', rpDecision:'Rozhodnutí', rpBranch:'větev', rpAltOnly:'jiná větev',
|
|
788
853
|
compare:'Porovnat', cmpWith:'Porovnáno s', cmpStop:'Ukončit porovnání', cmpOff:'nic', cmpBaseline:'referenční', cmpPrev:'předchozí', cmpTitle:'Rozdíly oproti', cmpNone:'Statusy, verdikty i evidence jsou stejné.', pxChanged:'pixelů jinak', cmpSide:'Vedle sebe', cmpDiff:'Rozdíl', cmpLoadErr:'Porovnávaný běh se nepodařilo načíst.', pxUnavail:'Pixelový rozdíl se počítá jen ve službě (snímky ze stejného původu).', was:'předtím', thisRun:'tento běh', cmpNoCap:'v porovnávaném běhu bez snímku', cmpAdded:'nové kroky', cmpRemoved:'kroky, které zmizely', cmpHint:'Ctrl/⌘ + klik na krok otevře porovnání',
|
|
789
854
|
reviewTitle:'Akceptace a komentáře', approve:'Schválit', reject:'Zamítnout', comment:'Komentovat', notePh:'Co je potřeba změnit, nebo poznámka · @jméno pošle kolegovi upozornění', approved:'schváleno', rejected:'zamítnuto', commented:'komentář', resolved:'vyřešeno', resolve:'Vyřešit', openFeedback:'Otevřená zpětná vazba', noFeedback:'Žádná otevřená zpětná vazba.', saved:'Uloženo', noteRequired:'Napište, co je potřeba změnit.', expires:'Platí do' },
|
|
790
|
-
en: { makePublicVersion:'Make version public', draft:'Draft', liveShared:'live, shared', publishHint:'Publish version saves the whole flow as %V; unchanged steps are reused.', htmlView:'HTML view', htmlViewNote:'A static copy of the page: the site’s scripts removed, styles and images rehosted, secrets stripped.', editTitle:'Edit in the canvas', editText:'Text', editKeep:'keep', editSave:'Save edit', editSaved:'Edit saved as a new revision', editRebased:'The step changed meanwhile; the edit is on the latest revision', editUnapplied:'Not applied (the element no longer exists)', staleCapture:'The screenshot is from the previous revision; Live shows the latest edit.', branches:'Branches', mergeBranch:'Merge into main', mergeConflict:'Step changed on both branches:', mergeTakeBranch:'Take the branch version? (Cancel = keep main)', branchMerged:'Merged', acceptVersion:'Accept version', returnVersion:'Return version', versionVerdict:'Version verdict', draftInfo:'This draft is live and shared: everyone who has it open sees changes as they happen, including the agents’. kaloko sync and canvas edits save steps; Publish version saves the whole flow as a new version.', changedSince:'Changed since', noChangeSince:'No change since', designEnv:'design', version:'Version', versions:'Flow versions', publish:'Publish version', publishName:'Version name (optional)', published:'Published', designRef:'design reference', setDesignRef:'Set as design reference', unsetDesignRef:'Unset design reference', designRefHint:'Implementation is checked against the design reference (pack design).', openDraft:'Open the draft', liveMode:'Live', shotMode:'Screenshot', inspect:'Inspect', pinComment:'Comment on element', pickHint:'Click an element in the preview.', pinnedOn:'On element', unchangedSince:'unchanged since', unchangedSteps:'steps unchanged', changedOnly:'Changed only', stepStatus:'Step status', st_in_progress:'in progress', st_ready_for_review:'ready for review', st_ready_for_dev:'ready for dev', present:'Present', presenting:'You are presenting', stopPresent:'Stop presenting', updatedSteps:'updated', viaAgent:'via', here:'here', copyCss:'Copy CSS', cssCopied:'CSS copied', noToken:'outside the token set', tokenSet:'token', revisionBy:'revision', noLiveOrigin:'Live steps are not enabled on this instance.', draftEmpty:'The draft has no steps yet; run kaloko sync.', prototypeTo:'Prototype: go to', outdated:'outdated', prototype:'Prototype', protoTitle:'Present the prototype', protoHint:'Click through the prototype; ← → previous / next step, Esc to leave.', protoShot:'Screenshots only: live steps are not available here. ← → move through the flow.', protoLocal:'Links open the pages from disk; ← → move through the flow.', protoDevice:'Device frame', protoFull:'Full screen', protoExit:'Leave the prototype', protoNoStep:'The link does not lead to a step of the flow', focus:'Focus', focusTitle:'Show only the preview, full screen', focusExit:'Back to the detail', focusBar:'Preview controls', focusShow:'Show controls', moreTools:'More tools', sheetHandle:'Step detail: tap or drag', hint:'Drag to pan · Ctrl + wheel to zoom · <span class="kbd">F</span> focus mode (in the detail: the preview only) · <span class="kbd">K</span> scenarios and runs · <span class="kbd">R</span> replay · <span class="kbd">P</span> prototype · <span class="kbd">I</span> map / index · <span class="kbd">←</span><span class="kbd">→</span> previous / next step (in the detail) · <span class="kbd">Esc</span> close', locale:'Tested locale', view:'View', both:'Both', shownRun:'Shown run', allRuns:'All runs', steps:'Flow steps', fit:'Fit flow', stepsWord:'steps', passedWord:'passed', fitLane:'Show the whole section', following:'Following', followEsc:'Esc to stop', followHint:'Follow what they do', visOn:'Others see me', visOff:'Hidden',
|
|
855
|
+
en: { makePublicVersion:'Make version public', draft:'Draft', liveShared:'live, shared', publishHint:'Publish version saves the whole flow as %V; unchanged steps are reused.', htmlView:'HTML view', htmlViewNote:'A static copy of the page: the site’s scripts removed, styles and images rehosted, secrets stripped.', editTitle:'Edit in the canvas', editText:'Text', editKeep:'keep', editSave:'Save edit', editSaved:'Edit saved as a new revision', editRebased:'The step changed meanwhile; the edit is on the latest revision', editUnapplied:'Not applied (the element no longer exists)', staleCapture:'The screenshot is from the previous revision; Live shows the latest edit.', branches:'Branches', mergeBranch:'Merge into main', mergeConflict:'Step changed on both branches:', mergeTakeBranch:'Take the branch version? (Cancel = keep main)', branchMerged:'Merged', acceptVersion:'Accept version', returnVersion:'Return version', versionVerdict:'Version verdict', draftInfo:'This draft is live and shared: everyone who has it open sees changes as they happen, including the agents’. kaloko sync and canvas edits save steps; Publish version saves the whole flow as a new version.', changedSince:'Changed since', noChangeSince:'No change since', designEnv:'design', version:'Version', versions:'Flow versions', publish:'Publish version', publishName:'Version name (optional)', published:'Published', designRef:'design reference', setDesignRef:'Set as design reference', unsetDesignRef:'Unset design reference', designRefHint:'Implementation is checked against the design reference (pack design).', openDraft:'Open the draft', liveMode:'Live', shotMode:'Screenshot', inspect:'Inspect', pinComment:'Comment on element', pickHint:'Click an element in the preview.', pinnedOn:'On element', unchangedSince:'unchanged since', unchangedSteps:'steps unchanged', changedOnly:'Changed only', stepStatus:'Step status', st_in_progress:'in progress', st_ready_for_review:'ready for review', st_ready_for_dev:'ready for dev', present:'Present', presenting:'You are presenting', stopPresent:'Stop presenting', updatedSteps:'updated', viaAgent:'via', here:'here', copyCss:'Copy CSS', cssCopied:'CSS copied', noToken:'outside the token set', tokenSet:'token', revisionBy:'revision', noLiveOrigin:'Live steps are not enabled on this instance.', draftEmpty:'The draft has no steps yet; run kaloko sync.', prototypeTo:'Prototype: go to', outdated:'outdated', prototype:'Prototype', protoTitle:'Present the prototype', protoHint:'Click through the prototype; ← → previous / next step, Esc to leave.', protoShot:'Screenshots only: live steps are not available here. ← → move through the flow.', protoLocal:'Links open the pages from disk; ← → move through the flow.', protoDevice:'Device frame', protoFull:'Full screen', protoExit:'Leave the prototype', protoNoStep:'The link does not lead to a step of the flow', focus:'Focus', focusTitle:'Show only the preview, full screen', focusExit:'Back to the detail', focusBar:'Preview controls', focusShow:'Show controls', moreTools:'More tools', sheetHandle:'Step detail: tap or drag', hint:'Drag to pan · Ctrl + wheel to zoom · <span class="kbd">F</span> focus mode (in the detail: the preview only) · <span class="kbd">K</span> scenarios and runs · <span class="kbd">R</span> replay · <span class="kbd">P</span> prototype · <span class="kbd">D</span> Dev mode (in the detail) · <span class="kbd">I</span> map / index · <span class="kbd">←</span><span class="kbd">→</span> previous / next step (in the detail) · <span class="kbd">Esc</span> close', locale:'Tested locale', view:'View', both:'Both', shownRun:'Shown run', allRuns:'All runs', steps:'Flow steps', fit:'Fit flow', stepsWord:'steps', passedWord:'passed', fitLane:'Show the whole section', following:'Following', followEsc:'Esc to stop', followHint:'Follow what they do', visOn:'Others see me', visOff:'Hidden',
|
|
791
856
|
pass:'Passed', partial:'Partial', fail:'Failed', skip:'Not checked', crop:'crop', html:'HTML snapshot', email:'e-mail', external:'third party',
|
|
792
857
|
branch:'Branch', commit:'Commit', author:'Author', pr:'PR', issue:'Issue', env:'Environment', duration:'Duration', noPr:'no PR', open:'open', merged:'merged', closed:'closed',
|
|
793
858
|
older:'Older run', newer:'Newer run', purpose:'Screen purpose from the scenario', criteria:'Acceptance criteria', jevTitle:'Semantic evaluation',
|
|
794
859
|
jevNote:'The evaluator got a reduced page state and returned a probability for each question. The runner turned them into verdicts by threshold and added the evidence.', evalLabel:'Evaluation', metaTitle:'Page metadata', mdBasic:'Basics', mdHeadings:'Heading outline', mdHreflang:'Language versions (hreflang)', mdOg:'Open Graph', mdTwitter:'Twitter / X', mdLd:'Structured data · JSON-LD', mdMicro:'Structured data · microdata', mdLinks:'Other head links', mdOther:'Other meta', missing:'Missing', duplicate:'duplicate', size:'Size', runVerdict:'Run verdict', accept:'Accept run', ret:'Return run', pending:'pending', accepted:'accepted', returned:'returned', baseline:'Baseline run', setBaseline:'Set as baseline', unsetBaseline:'Unset baseline', verdictNotePh:'Verdict note (required when returning)', manualPass:'Pass', manualFail:'Fail', manualNotePh:'Reason (required for fail)', reviewerVerdict:'reviewer verdict', decideManual:'Decide manually', consoleErrors:'Console errors', consoleLoading:'Loading the messages…', noConsole:'No console messages were kept for this capture.', consoleMore:(n) => `${n} more not stored (the first 20 are kept).`, blockedTitle:'Blocked requests', copyAll:'Copy all', copyOne:'Copy this message', copied:'Copied', tabOverview:'Criteria', tabPage:'Page', tabTech:'Technical', toSolve:(n) => `${n} to solve`, passedN:(n) => `${n} passed`, whyMatters:'Why it matters:', howFix:'How to fix:', whyAndFix:'Why it matters and how to fix it', aiShort:(p) => `AI ${p} % yes`, noneFailed:'Nothing failed.', toDecide:'For a person to decide', toDecideLead:'The AI was unsure or could not evaluate; open the step, look at the screenshot and decide.', worthLook:'Worth a look', jevScoreHint:'Average confidence of the AI that the semantic criteria of this screen are met', sigTitle:'Quick signals', sigLead:'The AI asks these on every screen next to the criteria. They never change the result, only point at screens worth a look.', sigPurpose:'Does what the step is for', sigLanguage:'Text in the expected language', sigBroken:'Looks broken (error, placeholder, blank)', sigWarn:(n) => `${n} signal(s) worth a look`, sigYes:(p) => `${p} % yes`,
|
|
795
860
|
howAuto:'Automatic check', howAi:'AI evaluation', howManual:'Decided by a person', howTech:'Technical detail',
|
|
796
861
|
aiPass:(p, t) => `The evaluator is ${p} % sure the answer is yes; passes from ${t} %.`, aiFail:(p, t) => `Only ${p} % yes; fails at ${t} % or less.`, aiUnsure:(p, lo, hi) => `Unsure: ${p} % yes, between ${lo} % and ${hi} %. Look at the screenshot and decide below.`,
|
|
862
|
+
aiWhy:'Why:', aiSaw:'the AI also saw the screenshot', kbTitle:'Keyboard', kbLead:(n, all) => all ? `Tab goes through ${n} elements and leaves the page.` : `The first ${n} tab stops.`, kbShow:'Show the order on the screenshot', kbHide:'Hide the order', kbNone:'no visible focus', kbWeak:(c) => `weak focus ${c} : 1`, kbHidden:'hidden element', kbCovered:'covered', ignTitle:'Ignored regions', ignLead:(n) => `${n} region(s) do not count in the pixel diff (clocks, ads, carousels).`, unstLead:(n) => `${n} region(s) moved between repeated shots; kaloko stability suggests ignoring them.`,
|
|
797
863
|
aiNotEval:'Not evaluated yet.', aiVia:{ qawalk:'evaluated by Kaloko', 'own-key':'evaluated with your key' }, aiAsked:'Asked',
|
|
798
864
|
a_exists:(s) => `${s} is on the page`, a_absent:(s) => `${s} is not on the page`, a_count:(s, mn, mx) => `${s} appears ${mn != null && mn === mx ? `exactly ${mn}×` : mn != null && mx != null ? `${mn}–${mx}×` : mn != null ? `at least ${mn}×` : mx != null ? `at most ${mx}×` : ''}`, a_text_matches:(s, p) => `${s || 'The page'} contains ${p}`, a_text_absent:(s, p) => `${s || 'The page'} does not contain ${p}`, a_attr_equals:(s, a, v) => `${s} has ${a} = ${v}`,
|
|
799
865
|
a_no_overflow:'Nothing overflows the screen width', a_no_console_errors:'No errors in the browser console', a_title_matches:(p) => `The page title matches ${p}`, a_meta_matches:(s, p) => `Page metadata ${s} matches ${p}`, a_link_matches:(s) => `Links ${s} point where they should`, a_http_status:(v) => `The server answers ${v || '2xx'}`, a_link_home:'The page links back home', a_links_ok:'Internal links work (no 404)', a_hreflang_pairs:'Language versions (hreflang) link to each other',
|
|
@@ -812,6 +878,9 @@ const I18N = {
|
|
|
812
878
|
compare:'Compare', cmpWith:'Compared with', cmpStop:'Stop comparing', cmpOff:'nothing', cmpBaseline:'baseline', cmpPrev:'previous', cmpTitle:'Differences from', cmpNone:'Statuses, verdicts and evidence are the same.', pxChanged:'pixels changed', cmpSide:'Side by side', cmpDiff:'Diff', cmpLoadErr:'Could not load the compared run.', pxUnavail:'Pixel diff works in the service only (same-origin images).', was:'was', thisRun:'this run', cmpNoCap:'no capture in the compared run', cmpAdded:'new steps', cmpRemoved:'steps that disappeared', cmpHint:'Ctrl/⌘ + click a step to open the comparison',
|
|
813
879
|
reviewTitle:'Sign-off and comments', approve:'Approve', reject:'Reject', comment:'Comment', notePh:'What should change, or a note · @name notifies a colleague', approved:'approved', rejected:'rejected', commented:'comment', resolved:'resolved', resolve:'Resolve', openFeedback:'Open feedback', noFeedback:'No open feedback.', saved:'Saved', noteRequired:'Write what should change.', expires:'Expires' }
|
|
814
880
|
};
|
|
881
|
+
// Dev mode (#42)
|
|
882
|
+
Object.assign(I18N.cs, { devMode:'Dev mode', devTab:'Dev', devTitle:'Míry, tokeny a stavy prvků pro vývoj', devTabEl:'Prvek', devTabTokens:'Tokeny', devEmpty:'Najeďte myší na prvek v náhledu a klikněte na něj. Shift + šipky procházejí strom prvků, se stisknutým Alt uvidíte vzdálenosti k dalšímu prvku.', devStart:'Vybrat hlavní obsah', devNavLabel:'Procházet prvky', navParent:'Rodič', navChild:'Potomek', navPrev:'Předchozí', navNext:'Další', g_type:'Typografie', g_color:'Barvy', g_space:'Mezery', g_size:'Rozměry', g_shape:'Tvar', g_layout:'Rozložení', notToken:'není token', nearest:'nejbližší', typedRaw:'Hodnota je v CSS napsaná natvrdo, i když token existuje; použijte proměnnou.', typedRawShort:'natvrdo', copyTokenPath:'Kopírovat cestu tokenu', contrast:'Kontrast', aaPass:'splňuje AA', aaFail:'nesplňuje AA', aaNeed:'potřeba', largeText:'velký text', contrastApprox:'Pod textem je obrázek, kontrast je přibližný.', states:'Stavy', stDefault:'výchozí', stNone:'Styly stránky pro tento prvek žádné stavy nedefinují.', changedInState:'Zvýrazněné hodnoty se v tomto stavu mění.', distances:'Vzdálenosti', toParent:'K rodiči', toTarget:'K prvku', measure:'Měřit', measureHint:'Podržte Alt a najeďte na jiný prvek, nebo zapněte Měřit a klepněte na něj.', assets:'Obrázky a ikony', download:'Stáhnout', assetFail:'Soubor se nepodařilo stáhnout.', copyCssVars:'Kopírovat CSS s proměnnými', copyTokens:'Kopírovat tokeny (JSON)', copyPrompt:'Kopírovat pokyn pro agenta', copyLine:'Kopírovat pokyn k této hodnotě', pinFromDev:'Komentovat tento prvek', tokUsed:'Použité na tomto kroku', tokOff:'Hodnoty mimo sadu tokenů', tokHard:'V CSS natvrdo místo tokenu', tokUnused:'Nepoužité', tokNone:'Krok nemá sadu tokenů (tokens.json).', tokHlN:'Zvýrazněno prvků:', tokHint:'Klikněte na token a náhled zvýrazní prvky, které ho používají.', devShotNote:'Míry a tokeny jsou ze snímku, jak ho zachytilo CLI; stavy a stahování obrázků potřebují živou stránku.', devNoSpec:'Tento snímek nemá spec. Projděte krok znovu aktuálním CLI (kaloko walk).', devLocalLive:'Ze souboru na disku čte Dev mode snímek; živou stránku měří na kaloko.app.', devAgentHint:'Totéž dostane agent: kaloko spec %N nebo MCP get_step_spec.', devLoading:'Načítám…', keyDev:'Dev mode v detailu kroku: míry, tokeny a stavy prvků (M měří, Shift + šipky procházejí prvky)', promptHead:'Postavte %E podle schváleného návrhu v Kaloku (krok %S).', promptUse:'Pro %W použijte token %T.', promptOff:'%P %V není token; nejbližší je %T. Pevnou hodnotu použijte jen po domluvě s designérem.', promptOffNone:'%P %V není token.', promptSpec:'Spec celého kroku: kaloko spec %N' });
|
|
883
|
+
Object.assign(I18N.en, { devMode:'Dev mode', devTab:'Dev', devTitle:'Measurements, tokens and states of elements for development', devTabEl:'Element', devTabTokens:'Tokens', devEmpty:'Hover an element in the preview and click it. Shift + arrows move through the element tree; hold Alt to see the distances to another element.', devStart:'Select the main content', devNavLabel:'Move through elements', navParent:'Parent', navChild:'Child', navPrev:'Previous', navNext:'Next', g_type:'Typography', g_color:'Colors', g_space:'Spacing', g_size:'Size', g_shape:'Shape', g_layout:'Layout', notToken:'not a token', nearest:'nearest', typedRaw:'The CSS has this value typed raw although a token exists; use the custom property.', typedRawShort:'typed raw', copyTokenPath:'Copy the token path', contrast:'Contrast', aaPass:'passes AA', aaFail:'fails AA', aaNeed:'needs', largeText:'large text', contrastApprox:'There is an image behind the text; the contrast is approximate.', states:'States', stDefault:'default', stNone:'The page styles define no states for this element.', changedInState:'Highlighted values change in this state.', distances:'Distances', toParent:'To the parent', toTarget:'To', measure:'Measure', measureHint:'Hold Alt and hover another element, or turn on Measure and tap it.', assets:'Images and icons', download:'Download', assetFail:'The file could not be downloaded.', copyCssVars:'Copy CSS with variables', copyTokens:'Copy tokens (JSON)', copyPrompt:'Copy prompt for an agent', copyLine:'Copy the prompt line for this value', pinFromDev:'Comment on this element', tokUsed:'Used on this step', tokOff:'Values outside the token set', tokHard:'Typed raw in the CSS instead of a token', tokUnused:'Not used', tokNone:'This step has no token set (tokens.json).', tokHlN:'Elements highlighted:', tokHint:'Click a token to highlight the elements that use it.', devShotNote:'Measurements and tokens come from the screenshot as the CLI captured it; states and image downloads need the live page.', devNoSpec:'This capture has no spec. Walk the step again with the current CLI (kaloko walk).', devLocalLive:'From disk, Dev mode reads the screenshot; the live page is measured on kaloko.app.', devAgentHint:'An agent gets the same: kaloko spec %N or MCP get_step_spec.', devLoading:'Loading…', keyDev:'Dev mode in the step detail: measurements, tokens and states of elements (M measures, Shift + arrows move through elements)', promptHead:'Build %E as approved in Kaloko (step %S).', promptUse:'Use token %T for the %W.', promptOff:'%P %V is not a token; the nearest is %T. Use the raw value only if the designer agrees.', promptOffNone:'%P %V is not a token.', promptSpec:'Spec of the whole step: kaloko spec %N' });
|
|
815
884
|
const storedLang = (() => { try { return localStorage.getItem('qawalk.lang'); } catch { return null; } })();
|
|
816
885
|
const UI = (() => { const l = window.QAWALK_APP?.lang || new URLSearchParams(location.search).get('lang') || storedLang || (navigator.language || 'en').slice(0, 2); return l === 'cs' || l === 'sk' ? 'cs' : 'en'; })();
|
|
817
886
|
function switchLang (l) {
|
|
@@ -820,6 +889,9 @@ function switchLang (l) {
|
|
|
820
889
|
location.reload();
|
|
821
890
|
}
|
|
822
891
|
const T = (k) => I18N[UI][k] ?? I18N.en[k] ?? k;
|
|
892
|
+
// "n pages" with the right plural (cs: 1 stránka, 2–4 stránky, 5+ stránek; en: 1 page, n pages)
|
|
893
|
+
const pagesN = (n) => { n = Number(n) || 0; return UI === 'cs' ? (n === 1 ? 'stránka' : n >= 2 && n <= 4 ? 'stránky' : 'stránek') : (n === 1 ? 'page' : 'pages') };
|
|
894
|
+
const needAttention = (n) => { n = Number(n) || 0; return UI === 'cs' ? `${n} ${pagesN(n)} ${n === 1 ? 'chce' : 'chtějí'} pozornost` : `${n} ${pagesN(n)} ${n === 1 ? 'needs' : 'need'} attention` };
|
|
823
895
|
const ICON = {
|
|
824
896
|
comment:'<svg class="i" viewBox="0 0 24 24"><path d="M4 5h16v11H9l-5 4z"/></svg>',
|
|
825
897
|
pin:'<svg class="i" viewBox="0 0 24 24"><path d="M12 21s-6-5.6-6-11a6 6 0 0 1 12 0c0 5.4-6 11-6 11z"/><circle cx="12" cy="10" r="2.2"/></svg>',
|
|
@@ -1219,7 +1291,7 @@ function cmpCritChanges (n) {
|
|
|
1219
1291
|
}
|
|
1220
1292
|
function loadImg (src) { return new Promise((res, rej) => { const i = new Image(); if (/^https?:/.test(src) && new URL(src, location.href).origin !== location.origin) i.crossOrigin = 'anonymous'; i.onload = () => res(i); i.onerror = () => rej(new Error('image')); i.src = src; }); }
|
|
1221
1293
|
/* pixel diff of two captures scaled to width W: changed pixels in red on a faded copy of the current one */
|
|
1222
|
-
async function pixelDiff (key, srcA, srcB, W) {
|
|
1294
|
+
async function pixelDiff (key, srcA, srcB, W, ign = null) {
|
|
1223
1295
|
const k = `${key}|${W}`;
|
|
1224
1296
|
if (CMP.pix.has(k)) return CMP.pix.get(k);
|
|
1225
1297
|
const job = (async () => {
|
|
@@ -1229,18 +1301,29 @@ async function pixelDiff (key, srcA, srcB, W) {
|
|
|
1229
1301
|
const A = draw(a, ha), B = draw(b, hb);
|
|
1230
1302
|
const out = document.createElement('canvas'); out.width = W; out.height = H; const ox = out.getContext('2d'); const O = ox.createImageData(W, H);
|
|
1231
1303
|
let n = 0; const da = A.data, db = B.data, dout = O.data;
|
|
1232
|
-
|
|
1304
|
+
const skip = ignoreMask(ign, W, H); let counted = 0;
|
|
1305
|
+
for (let i = 0, p = 0; i < da.length; i += 4, p++) {
|
|
1306
|
+
if (skip && skip[p]) { const g = ((p % W) + ((p / W) | 0)) % 10 < 5 ? 205 : 232; dout[i] = dout[i + 1] = dout[i + 2] = g; dout[i + 3] = 255; continue; }
|
|
1307
|
+
counted++;
|
|
1233
1308
|
const d = Math.abs(da[i] - db[i]) + Math.abs(da[i + 1] - db[i + 1]) + Math.abs(da[i + 2] - db[i + 2]);
|
|
1234
1309
|
if (d > 48) { n++; dout[i] = 220; dout[i + 1] = 20; dout[i + 2] = 60; dout[i + 3] = 255; }
|
|
1235
1310
|
else { dout[i] = 255 - (255 - da[i]) * 0.35; dout[i + 1] = 255 - (255 - da[i + 1]) * 0.35; dout[i + 2] = 255 - (255 - da[i + 2]) * 0.35; dout[i + 3] = 255; }
|
|
1236
1311
|
}
|
|
1237
1312
|
ox.putImageData(O, 0, 0);
|
|
1238
|
-
return { ratio: n / (
|
|
1313
|
+
return { ratio: n / Math.max(1, counted), canvas: out, ha, hb, H, ignored: W * H - counted };
|
|
1239
1314
|
})();
|
|
1240
1315
|
CMP.pix.set(k, job);
|
|
1241
1316
|
job.catch(() => CMP.pix.delete(k));
|
|
1242
1317
|
return job;
|
|
1243
1318
|
}
|
|
1319
|
+
/* ignored regions of both captures (#20) as a W × H mask: CSS px of the page scaled to the diff width */
|
|
1320
|
+
function ignoreMask (ign, W, H) {
|
|
1321
|
+
if (!ign?.rects?.length) return null;
|
|
1322
|
+
const k = W / (ign.cssWidth || W); const m = new Uint8Array(W * H);
|
|
1323
|
+
for (const r of ign.rects) { const x0 = Math.max(0, Math.floor(r.x * k)), x1 = Math.min(W, Math.ceil((r.x + r.w) * k)); for (let y = Math.max(0, Math.floor(r.y * k)); y < Math.min(H, Math.ceil((r.y + r.h) * k)); y++) m.fill(1, y * W + x0, y * W + x1); }
|
|
1324
|
+
return m;
|
|
1325
|
+
}
|
|
1326
|
+
const ignOf = (a, b) => { const rects = [...(a?.ignore || []), ...(b?.ignore || [])]; return rects.length ? { rects, cssWidth: a?.size?.[0] || b?.size?.[0] || null } : null; };
|
|
1244
1327
|
const pxClass = (r) => r > 0.1 ? 'lv2' : r > 0.01 ? 'lv1' : 'lv0';
|
|
1245
1328
|
const pxText = (r) => `${(r * 100).toFixed(r < 0.1 ? 1 : 0)} % ${T('pxChanged')}`;
|
|
1246
1329
|
/* fills the per-node pixel badges after the nodes are rendered */
|
|
@@ -1261,7 +1344,7 @@ function fillPixelBadges () {
|
|
|
1261
1344
|
if (!ca || !cb) { el.textContent = '—'; el.title = T('cmpNoCap'); return; }
|
|
1262
1345
|
const small = ca.thumb && cb.thumb;
|
|
1263
1346
|
try {
|
|
1264
|
-
const r = await pixelDiff(`${n.id}|${S.loc}|${v.name}${small ? '|thumb' : ''}`, asset(small ? ca.thumb : ca.image || ca.png), S.cmp.base + (small ? cb.thumb : cb.image || cb.png), 256);
|
|
1347
|
+
const r = await pixelDiff(`${n.id}|${S.loc}|${v.name}${small ? '|thumb' : ''}`, asset(small ? ca.thumb : ca.image || ca.png), S.cmp.base + (small ? cb.thumb : cb.image || cb.png), 256, ignOf(ca, cb));
|
|
1265
1348
|
el.textContent = `◐ ${pxText(r.ratio)}`; el.className = `pxd ${pxClass(r.ratio)}`;
|
|
1266
1349
|
} catch { el.textContent = '◐ ?'; el.title = T('pxUnavail'); }
|
|
1267
1350
|
};
|
|
@@ -1475,10 +1558,10 @@ function stackNodeHtml (n, p) {
|
|
|
1475
1558
|
const thumbs = (st.sample || []).slice(0, 3).map((x) => x.thumb ? `<img loading="lazy" src="${esc(asset(x.thumb))}" alt="">` : '<span class="ph"></span>').join('') || '<span class="ph"></span><span class="ph"></span><span class="ph"></span>';
|
|
1476
1559
|
const bad = (st.summary.fail || 0) + (st.summary.partial || 0);
|
|
1477
1560
|
return `<div class="node node--stack st-${s} ${S.sess && n.session !== S.sess ? 'dim' : ''}" data-id="${esc(n.id)}" style="left:${p.x}px;top:${p.y}px">
|
|
1478
|
-
<div class="node__head"><span class="kind">${ICON.stack}</span><div class="node__t"><div class="node__title">${esc(loc(n.title))}</div><div class="node__path">${esc(st.count)} ${
|
|
1561
|
+
<div class="node__head"><span class="kind">${ICON.stack}</span><div class="node__t"><div class="node__title">${esc(loc(n.title))}</div><div class="node__path">${esc(st.count)} ${pagesN(st.count)}${st.source ? ` · ${esc(st.source)}` : ''}</div></div><span class="ok st-${s}">${esc(st.summary.pass || 0)}/${esc(st.count)}</span></div>
|
|
1479
1562
|
<button class="stack-fan" data-open="${esc(n.id)}" aria-label="${esc(T('openIndex'))}: ${esc(loc(n.title))}">${thumbs}<span class="count">${esc(st.count)}</span></button>
|
|
1480
1563
|
<div class="stack-bar">${['pass', 'partial', 'fail', 'skip'].map((k) => st.summary[k] ? `<i style="width:${st.summary[k] / tot * 100}%;background:var(--${k})"></i>` : '').join('')}</div>
|
|
1481
|
-
<div class="node__foot"><span class="note">${bad ?
|
|
1564
|
+
<div class="node__foot"><span class="note">${bad ? needAttention(bad) : T('allPagesPass')}</span>${cmtBadge(n.id)}<span class="note">${T('openIndex')} →</span></div>
|
|
1482
1565
|
</div>`;
|
|
1483
1566
|
}
|
|
1484
1567
|
async function loadStack (n) {
|
|
@@ -1528,7 +1611,7 @@ async function openStack (id, opts = {}) {
|
|
|
1528
1611
|
const crits = (data.criteria || []).map((c) => { const k = n.criteria.find((x) => x.id === c.id)?.counts || {}; return { ...c, bad: (k.fail || 0) + (k.needs_review || 0) }; });
|
|
1529
1612
|
const el = st.inline ? $('ri') : $('si');
|
|
1530
1613
|
el.innerHTML = `<div class="si__box">
|
|
1531
|
-
<div class="si__head"><span class="kind" style="background:var(--ink);color:var(--panel)">${ICON.stack}</span><div
|
|
1614
|
+
<div class="si__head"><span class="kind" style="background:var(--ink);color:var(--panel)">${ICON.stack}</span><div class="si__title">${st.inline ? `<div class="ri-crumbs"><button id="siUp">${esc(RUN.scenario.title)}</button><span>›</span><b id="siTitle">${esc(loc(n.title))}</b></div>` : `<h3 id="siTitle">${esc(loc(n.title))}</h3>`}<div class="note">${esc(n.stack.count)} ${pagesN(n.stack.count)}${n.stack.source ? ` · ${esc(n.stack.source)}` : ''}${n.stack.semantic_sample != null && crits.some((c) => c.type === 'semantic') ? ` · ${T('semanticSample')} ${esc(Math.min(n.stack.semantic_sample, n.stack.count))}` : ''}</div></div>
|
|
1532
1615
|
<div class="seg" id="siView"></div><button class="btn" id="siCsv">${T('downloadCsv')}</button>${st.inline ? '' : `<button class="icon-btn" id="siClose" aria-label="Close">${ICON.close}</button>`}</div>
|
|
1533
1616
|
<div class="si__tools">
|
|
1534
1617
|
${['all', 'fail', 'partial', 'pass', 'skip'].filter((k) => k === 'all' || sum[k]).map((k) => `<button class="si-chip" data-st="${k}" aria-pressed="${st.status === k}">${k === 'all' ? T('allWord') : `${MARK[k]} ${T(k)}`} ${k === 'all' ? esc(n.stack.count) : esc(sum[k] || 0)}</button>`).join('')}
|
|
@@ -1568,7 +1651,7 @@ function paintStack () {
|
|
|
1568
1651
|
const st = S.st; const box = $('siList'); if (!st || !box || (st.inline ? $('ri').hidden : $('si').hidden)) return;
|
|
1569
1652
|
const data = STACKS.get(st.id); const n = byId(st.id); const cols = (data.fields || []).filter((f) => f.id !== 'title').slice(0, 4);
|
|
1570
1653
|
const list = st.filtered; const head = st.view === 'grid' ? 0 : $('siHead').offsetHeight;
|
|
1571
|
-
$('siCount').textContent = `${list.length} / ${data.items.length} ${
|
|
1654
|
+
$('siCount').textContent = `${list.length} / ${data.items.length} ${pagesN(data.items.length)}`;
|
|
1572
1655
|
const img = (it) => { const t = stackThumb(it); return t ? `<img loading="lazy" src="${esc(asset(t))}" alt="">` : '<span class="ph"></span>'; };
|
|
1573
1656
|
const fails = (it) => `<span class="si-fails">${(it.fails || []).map((id) => `<span title="${esc((data.criteria || []).find((c) => c.id === id)?.text || '')}">${esc(id)}</span>`).join('')}${(it.review || []).slice(0, 3).map((id) => `<span class="rv" title="${esc(T('needs_review'))}">${esc(id)}</span>`).join('')}</span>`;
|
|
1574
1657
|
if (!list.length) { $('siSpacer').style.height = '0px'; $('siRows').innerHTML = `<div class="si__empty">${T('noPagesMatch')}</div>`; return; }
|
|
@@ -1665,7 +1748,7 @@ function riRender (opts = {}) {
|
|
|
1665
1748
|
const r = S.ri; const sum = RUN.summary || {}; const all = screens();
|
|
1666
1749
|
const count = (k) => all.filter((n) => nodeSt(n) === k).length;
|
|
1667
1750
|
$('ri').innerHTML = `<div class="si__box">
|
|
1668
|
-
<div class="si__head"><span class="kind" style="background:var(--ink);color:var(--panel)">${ICON.stack}</span><div
|
|
1751
|
+
<div class="si__head"><span class="kind" style="background:var(--ink);color:var(--panel)">${ICON.stack}</span><div class="si__title"><h3>${esc(RUN.scenario.title)}</h3><div class="note">${all.length} ${T('stepsWord')} · ${esc(sum.pass || 0)} ${T('passedWord')}${all.some((n) => n.kind === 'stack') ? ` · ${all.filter((n) => n.kind === 'stack').reduce((k, n) => k + (n.stack?.count || 0), 0)} ${pagesN(all.filter((n) => n.kind === 'stack').reduce((k, n) => k + (n.stack?.count || 0), 0))}` : ''}</div></div>
|
|
1669
1752
|
<button class="btn" id="riCsv">${T('downloadCsv')}</button></div>
|
|
1670
1753
|
<div class="si__tools">
|
|
1671
1754
|
${['all', 'fail', 'partial', 'pass', 'skip'].filter((k) => k === 'all' || count(k)).map((k) => `<button class="si-chip" data-st="${k}" aria-pressed="${r.status === k}">${k === 'all' ? T('allWord') : `${MARK[k]} ${T(k)}`} ${k === 'all' ? all.length : count(k)}</button>`).join('')}
|
|
@@ -1694,7 +1777,7 @@ function riPaint () {
|
|
|
1694
1777
|
const tops = []; let y = 0; for (const e of out) { tops.push(y); y += e.type === 'group' ? RI_H.group : RI_H.row; }
|
|
1695
1778
|
S.ri.tops = tops;
|
|
1696
1779
|
const nSteps = out.filter((e) => e.type === 'step').length, nPages = out.filter((e) => e.type === 'page').length;
|
|
1697
|
-
$('riCount').textContent = `${nSteps} / ${screens().length} ${T('stepsWord')}${nPages ? ` · ${nPages} ${
|
|
1780
|
+
$('riCount').textContent = `${nSteps} / ${screens().length} ${T('stepsWord')}${nPages ? ` · ${nPages} ${pagesN(nPages)}` : ''}${waiting ? ` · ${T('loadingPages')}` : ''}`;
|
|
1698
1781
|
if (!out.length) { $('riSpacer').style.height = '0px'; $('riRows').innerHTML = `<div class="si__empty">${T('noStepsMatch')}</div>`; return; }
|
|
1699
1782
|
$('riSpacer').style.height = `${y}px`;
|
|
1700
1783
|
const head = $('riHead').offsetHeight; const top = Math.max(0, box.scrollTop - head); const bottom = top + box.clientHeight;
|
|
@@ -1719,7 +1802,7 @@ function riRow (e, k, top) {
|
|
|
1719
1802
|
const thumb = n.kind === 'stack'
|
|
1720
1803
|
? `<span class="ri-fan">${((n.stack?.sample || []).slice(0, 3).map((x) => x.thumb ? `<img loading="lazy" src="${esc(asset(x.thumb))}" alt="">` : '<span class="ph"></span>').join('') || '<span class="ph"></span><span class="ph"></span><span class="ph"></span>')}<span class="cnt">${esc(n.stack?.count || 0)}</span></span>`
|
|
1721
1804
|
: (() => { const t = riThumb(n); return t ? `<img loading="lazy" src="${esc(asset(t))}" alt="">` : '<span class="ph"></span>'; })();
|
|
1722
|
-
const sub = n.kind === 'stack' ? `${esc(n.stack?.count || 0)} ${
|
|
1805
|
+
const sub = n.kind === 'stack' ? `${esc(n.stack?.count || 0)} ${pagesN(n.stack?.count || 0)}${n.stack?.source ? ` · ${esc(n.stack.source)}` : ''}` : esc(loc(n.path) || '');
|
|
1723
1806
|
const cons = Object.values(n.captures?.[S.loc] || {}).reduce((k, c) => k + (c?.console_errors || 0), 0);
|
|
1724
1807
|
const chips = bad.map((c) => `<span class="${critSt(c) === 'fail' ? '' : 'rv'}" title="${esc(c.text)}">${esc(c.id)}${c.counts ? `<i>${esc((c.counts.fail || 0) + (c.counts.needs_review || 0))}</i>` : ''}</span>`).join('');
|
|
1725
1808
|
const crits = n.kind === 'stack' ? `${esc(n.stack?.summary?.pass || 0)}/${esc(n.stack?.count || 0)}` : s === 'skip' ? '—' : `${ok}/${crit.length}`;
|
|
@@ -1957,7 +2040,7 @@ function renderChrome () {
|
|
|
1957
2040
|
$('appNav').querySelectorAll('[data-lang]').forEach((b) => { b.onclick = () => { if (b.dataset.lang !== UI) switchLang(b.dataset.lang); }; });
|
|
1958
2041
|
document.querySelectorAll('[data-t]').forEach((el) => { el.textContent = T(el.dataset.t); });
|
|
1959
2042
|
$('helpBtn').title = `${T('helpTitle')} (?)`; $('helpBtn').setAttribute('aria-label', T('helpTitle'));
|
|
1960
|
-
$('hint').innerHTML = `<ul>${T('hint').split(' · ').map((x) => `<li>${x}</li>`).join('')}<li><span class="kbd">C</span> ${esc(T('keyComment'))}</li><li><span class="kbd">?</span> ${esc(T('keyHelp'))}</li></ul>`;
|
|
2043
|
+
$('hint').innerHTML = `<ul>${T('hint').split(' · ').map((x) => `<li>${x}</li>`).join('')}<li><span class="kbd">C</span> ${esc(T('keyComment'))}</li><li><span class="kbd">D</span> ${esc(T('keyDev'))}</li><li><span class="kbd">?</span> ${esc(T('keyHelp'))}</li></ul>`;
|
|
1961
2044
|
$('hint').querySelector('ul')?.insertAdjacentHTML('beforeend', rvHintItem()); // review mode (Q)
|
|
1962
2045
|
const ib = $('inboxBtn');
|
|
1963
2046
|
if (app?.user) { ib.hidden = false; ib.href = `/${app.org}/inbox`; ib.innerHTML = `<svg class="i" viewBox="0 0 24 24"><path d="M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15z"/><path d="M10 20a2 2 0 0 0 4 0"/></svg>${app.unread ? `<b>${esc(app.unread)}</b>` : ''}`; ib.title = 'Inbox'; ib.setAttribute('aria-label', `Inbox${app.unread ? ` (${app.unread})` : ''}`); }
|
|
@@ -1990,7 +2073,7 @@ function renderChrome () {
|
|
|
1990
2073
|
<dt>${T('duration')}</dt><dd>${dur()}</dd></dl>${isDesign() ? '<div class="dvers" id="dvers"></div>' : ''}`;
|
|
1991
2074
|
renderDesignBar();
|
|
1992
2075
|
const order = screens().slice().sort((a, b) => a.col - b.col || a.lane - b.lane);
|
|
1993
|
-
const stepLi = (n) => `<li><button data-step="${esc(n.id)}" class="st-${nodeSt(n)}"><span class="dot"></span><span>${n.kind === 'stack' ? `${ICON.stack} ` : ''}${esc(loc(n.title))}</span><span class="path">${n.kind === 'stack' ? `${esc(n.stack?.count || 0)} ${
|
|
2076
|
+
const stepLi = (n) => `<li><button data-step="${esc(n.id)}" class="st-${nodeSt(n)}"><span class="dot"></span><span>${n.kind === 'stack' ? `${ICON.stack} ` : ''}${esc(loc(n.title))}</span><span class="path">${n.kind === 'stack' ? `${esc(n.stack?.count || 0)} ${pagesN(n.stack?.count || 0)}` : esc(loc(n.path))}</span></button></li>`;
|
|
1994
2077
|
$('steps').innerHTML = sessions().length
|
|
1995
2078
|
? sessions().map((k) => { const mine = order.filter((n) => n.session === k); if (!mine.length) return ''; const c = RUN.summary.by_session?.[k] || {}; return `<h3 data-sess="${esc(k)}" aria-pressed="${S.sess === k}" title="${esc(k)}"><i class="dot" style="background:${sessColor(k)}"></i>${esc(sessTitle(k))}${bar5(c)}</h3>${mine.map(stepLi).join('')}`; }).join('') + (order.some((n) => !n.session) ? order.filter((n) => !n.session).map(stepLi).join('') : '')
|
|
1996
2079
|
: laneNamesOf().length > 1
|
|
@@ -2022,6 +2105,13 @@ const EVIDENCE = [
|
|
|
2022
2105
|
[/^heading level skipped: (.+)$/, (m) => [`A heading level is skipped: ${m[1]}`, `Přeskočená úroveň nadpisu: ${m[1]}`]],
|
|
2023
2106
|
[/^no main landmark$/, () => ['The page has no <main> area', 'Stránka nemá hlavní oblast <main>']],
|
|
2024
2107
|
[/^duplicate ids: (.+)$/, (m) => [`Repeated ids: ${m[1]}`, `Opakovaná id: ${m[1]}`]],
|
|
2108
|
+
[/^(\d+) of (\d+) focus stop\(s\) without a clear indicator: (.+)$/, (m) => [`${m[1]} of ${m[2]} tab stops show no clear focus: ${m[3]}`, `${m[1]} z ${m[2]} zastávek tabulátoru nemá zřetelný fokus: ${m[3]}`]],
|
|
2109
|
+
[/^all (\d+) checked focus stop\(s\) show an indicator with at least 3:1 contrast$/, (m) => [`All ${m[1]} checked tab stops show a clear focus`, `Všechny kontrolované zastávky (${m[1]}) mají zřetelný fokus`]],
|
|
2110
|
+
[/^Tab goes through all (\d+) stop\(s\) and leaves the page$/, (m) => [`Tab goes through all ${m[1]} stops and leaves the page`, `Tabulátor projde všechny zastávky (${m[1]}) a opustí stránku`]],
|
|
2111
|
+
[/^skip link (.+)$/, (m) => [`Skip link ${m[1]}`, `Odkaz pro přeskočení ${m[1]}`]],
|
|
2112
|
+
[/^(\d+) of (\d+) control\(s\) have no name in the accessibility tree: (.+)$/, (m) => [`${m[1]} of ${m[2]} controls have no name for screen readers: ${m[3]}`, `${m[1]} z ${m[2]} ovládacích prvků nemá pro čtečky název: ${m[3]}`]],
|
|
2113
|
+
[/^all (\d+) control\(s\) in the accessibility tree have a name$/, (m) => [`All ${m[1]} controls have a name for screen readers`, `Všechny ovládací prvky (${m[1]}) mají pro čtečky název`]],
|
|
2114
|
+
[/^roles and aria-\* attributes are valid$/, () => ['ARIA roles and attributes are valid', 'Role a atributy ARIA jsou platné']],
|
|
2025
2115
|
[/^(\d+) text\(s\) below WCAG AA contrast: (.+)$/, (m) => [`${m[1]} text(s) with too little contrast: ${m[2]}`, `Texty s nízkým kontrastem (${m[1]}): ${m[2]}`]],
|
|
2026
2116
|
[/^(\d+) texts meet WCAG AA contrast$/, (m) => [`All ${m[1]} texts have enough contrast`, `Všech ${m[1]} textů má dostatečný kontrast`]],
|
|
2027
2117
|
[/^LCP (.+) since page load \(limit (.+)\)$/, (m) => [`The largest content appeared after ${m[1]} (limit ${m[2]})`, `Největší obsah se zobrazil za ${m[1]} (limit ${m[2]})`]],
|
|
@@ -2114,7 +2204,33 @@ function aiGauge (r) {
|
|
|
2114
2204
|
const [lo, hi] = r.th || [0.15, 0.85];
|
|
2115
2205
|
const pct = (x) => Math.round(x * 100);
|
|
2116
2206
|
const line = r.verdict === 'pass' ? T('aiPass')(pct(r.score), pct(hi)) : r.verdict === 'fail' ? T('aiFail')(pct(r.score), pct(lo)) : T('aiUnsure')(pct(r.score), pct(lo), pct(hi));
|
|
2117
|
-
return `<span class="gauge" role="img" aria-label="${esc(line)}"><i class="gauge__lo" style="width:${pct(lo)}%"></i><i class="gauge__hi" style="left:${pct(hi)}%"></i><b style="left:${pct(r.score)}%"></b></span><span class="how__line">${esc(line)}${r.via ? ` <span class="muted">· ${esc(T('aiVia')[r.via] || '')}</span>` : ''}</span
|
|
2207
|
+
return `<span class="gauge" role="img" aria-label="${esc(line)}"><i class="gauge__lo" style="width:${pct(lo)}%"></i><i class="gauge__hi" style="left:${pct(hi)}%"></i><b style="left:${pct(r.score)}%"></b></span><span class="how__line">${esc(line)}${r.via ? ` <span class="muted">· ${esc(T('aiVia')[r.via] || '')}</span>` : ''}</span>${aiReason(r)}`;
|
|
2208
|
+
}
|
|
2209
|
+
/* the evaluator's one sentence next to the score (#17); marked when it saw the screenshot */
|
|
2210
|
+
/* dashed boxes for ignored and moving regions, numbered tab stops when asked (step detail → Keyboard) */
|
|
2211
|
+
function overlayRegions (box, cap, v) {
|
|
2212
|
+
const im = box.querySelector('img'); if (!im) return;
|
|
2213
|
+
const place = () => {
|
|
2214
|
+
box.querySelectorAll('.ignbox, .kbstop').forEach((e) => e.remove());
|
|
2215
|
+
const k = im.clientWidth / (cap.size?.[0] || v.w);
|
|
2216
|
+
const rect = (cls, r, title) => { const d = document.createElement('span'); d.className = cls; if (title) d.title = title; Object.assign(d.style, { left: `${r.x * k}px`, top: `${r.y * k}px`, width: `${Math.max(2, r.w * k)}px`, height: `${Math.max(2, r.h * k)}px` }); box.appendChild(d); return d; };
|
|
2217
|
+
for (const r of cap.ignore || []) rect('ignbox', r, `${T('ignTitle')}${r.selector ? ': ' + r.selector : ''}`);
|
|
2218
|
+
for (const r of cap.unstable || []) rect('ignbox unst', r, T('unstLead')(1));
|
|
2219
|
+
if (S.kbShow && cap.keyboard) for (const s of cap.keyboard.stops || []) { if (s.hidden || !s.w) continue; const d = rect(`kbstop${s.focus && s.focus !== 'ok' ? ' bad' : ''}`, s, `${s.n} ${s.name || s.el}`); const i = document.createElement('i'); i.textContent = s.n; d.appendChild(i); }
|
|
2220
|
+
};
|
|
2221
|
+
im.complete ? place() : im.addEventListener('load', place);
|
|
2222
|
+
}
|
|
2223
|
+
/* Keyboard section of the step detail: the tab order with what is wrong at each stop, and a toggle for the overlay */
|
|
2224
|
+
function keyboardBlock (cap) {
|
|
2225
|
+
const kb = cap?.keyboard; const ign = cap?.ignore?.length || 0; const unst = cap?.unstable?.length || 0;
|
|
2226
|
+
if (!kb && !ign && !unst) return '';
|
|
2227
|
+
const flag = (s) => s.hidden ? T('kbHidden') : s.focus === 'none' ? T('kbNone') : s.focus === 'weak' ? T('kbWeak')(s.contrast) : s.focus === 'obscured' ? T('kbCovered') : '';
|
|
2228
|
+
const list = kb ? `<ol class="kb-list">${(kb.stops || []).slice(0, 12).map((s) => { const f = flag(s); return `<li class="${f ? 'bad' : ''}">${esc(s.name || s.el)}${f ? ` · ${esc(f)}` : ''}</li>`; }).join('')}</ol>` : '';
|
|
2229
|
+
return `${kb ? `<div class="sec"><h4>${T('kbTitle')}</h4><p class="note">${esc(T('kbLead')(kb.total, kb.complete))}</p><button type="button" class="pr" id="kbToggle">${S.kbShow ? T('kbHide') : T('kbShow')}</button>${list}</div>` : ''}${ign || unst ? `<div class="sec"><h4>${T('ignTitle')}</h4>${ign ? `<p class="note">${esc(T('ignLead')(ign))}</p>` : ''}${unst ? `<p class="note">${esc(T('unstLead')(unst))}</p>` : ''}</div>` : ''}`;
|
|
2230
|
+
}
|
|
2231
|
+
function aiReason (r) {
|
|
2232
|
+
if (!r.reason) return '';
|
|
2233
|
+
return `<span class="how__why"><b>${T('aiWhy')}</b>${esc(r.reason)}${r.vision ? ` <span class="muted">· ${T('aiSaw')}</span>` : ''}</span>`;
|
|
2118
2234
|
}
|
|
2119
2235
|
function howBlock (c, r) {
|
|
2120
2236
|
const k = c.check || {};
|
|
@@ -2205,18 +2321,22 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2205
2321
|
if (RUN.details_deferred) loadDetails();
|
|
2206
2322
|
const n0 = byId(id); if (n0?.kind === 'stack') { openStack(id); return; }
|
|
2207
2323
|
const n = n0; const list = n.item ? (S.itemList || [id]).map(byId).filter(Boolean) : S.mode === 'index' && S.riList?.includes(id) ? S.riList.map(byId) : flowOrder(), i = list.indexOf(n);
|
|
2208
|
-
if (S.open !== id) { LIVE.insp = null; LIVE.anchor = null; LIVE.mode = null; }
|
|
2324
|
+
if (S.open !== id) { LIVE.insp = null; LIVE.anchor = null; LIVE.mode = null; devReset(); }
|
|
2209
2325
|
S.lbMode ||= isDesign() ? 'live' : 'shot';
|
|
2210
2326
|
S.open = id; S.lbVp = vpName || (S.vp === 'mobile' ? mVp() : dVp())?.name || viewports()[0].name;
|
|
2211
2327
|
if (n.item && !n.captures?.[S.loc]?.[S.lbVp]) S.lbVp = Object.keys(n.captures?.[S.loc] || {})[0] || S.lbVp; // stack pages: the captured viewport
|
|
2212
2328
|
const v = viewports().find((x) => x.name === S.lbVp) || viewports()[0];
|
|
2213
2329
|
const cap = n.captures?.[S.loc]?.[v.name];
|
|
2330
|
+
// Dev mode stays on from step to step: on the live page where the frame helper runs, else on the screenshot
|
|
2331
|
+
if (DEV.on && !devAvailable(cap)) { DEV.on = false; devReset(); }
|
|
2332
|
+
if (DEV.on) { const shot = !devLive(cap); if (shot !== DEV.shot) devReset(); DEV.shot = shot; LIVE.mode = shot ? (LIVE.mode === 'dev' ? null : LIVE.mode) : 'dev'; }
|
|
2333
|
+
if (!DEV.on && S.lbTab === 'dev') S.lbTab = 'overview';
|
|
2214
2334
|
const s = nodeSt(n); const j = jevFor(n, v.name);
|
|
2215
2335
|
const g = RUN.git || {};
|
|
2216
2336
|
const lb = $('lb');
|
|
2217
2337
|
const html = `<div class="lb__box">
|
|
2218
2338
|
<div class="lb__stage">
|
|
2219
|
-
<div class="lb__tools"><div class="seg" id="lbVp"></div><button class="btn" id="lbFocus" title="${esc(T('focusTitle'))} (F)" aria-pressed="${!!S.focus}">${ICON.focus}<span>${T('focus')}</span></button><div class="lb__more" id="lbMore">${isDesign() || liveSrc(cap) ? `<button class="btn" id="lbProto" title="${esc(T('protoTitle'))} (P)">${T('prototype')}</button>` : ''}${liveSrc(cap) || viewSrc(cap) ? `<div class="seg" id="lbMode"></div>` : ''}${(liveSrc(cap) && S.lbMode === 'live') || (viewSrc(cap) && S.lbMode === 'view') ? `<span id="lbLiveTools" class="share__row"></span>` : ''}${S.cmp ? `<div class="seg" id="lbCmpSeg"></div>` : ''}<span class="note">${v.w} × ${v.h}, ${cap?.platform && cap.platform !== 'electron' ? T('screenWord') : cap?.first_screen ? T('firstScreen') : T('fullPage')} · ${esc(S.loc)}</span><span class="spacer"></span><span class="note lb__hint">${S.cmp ? `${T('cmpTitle')} ${esc(cmpLabel())}` : T('hover')}</span></div><button class="icon-btn lb__moreBtn" id="lbMoreBtn" aria-expanded="false" aria-controls="lbMore" aria-label="${esc(T('moreTools'))}" title="${esc(T('moreTools'))}">${ICON.more}</button></div>
|
|
2339
|
+
<div class="lb__tools"><div class="seg" id="lbVp"></div><button class="btn" id="lbFocus" title="${esc(T('focusTitle'))} (F)" aria-pressed="${!!S.focus}">${ICON.focus}<span>${T('focus')}</span></button><div class="lb__more" id="lbMore">${isDesign() || liveSrc(cap) ? `<button class="btn" id="lbProto" title="${esc(T('protoTitle'))} (P)">${T('prototype')}</button>` : ''}${devAvailable(cap) ? `<button class="btn" id="lbDev" aria-pressed="${DEV.on}" title="${esc(T('devTitle'))} (D)">${T('devMode')}</button>` : ''}${liveSrc(cap) || viewSrc(cap) ? `<div class="seg" id="lbMode"></div>` : ''}${(liveSrc(cap) && S.lbMode === 'live') || (viewSrc(cap) && S.lbMode === 'view') ? `<span id="lbLiveTools" class="share__row"></span>` : ''}${S.cmp ? `<div class="seg" id="lbCmpSeg"></div>` : ''}<span class="note">${v.w} × ${v.h}, ${cap?.platform && cap.platform !== 'electron' ? T('screenWord') : cap?.first_screen ? T('firstScreen') : T('fullPage')} · ${esc(S.loc)}</span><span class="spacer"></span><span class="note lb__hint">${S.cmp ? `${T('cmpTitle')} ${esc(cmpLabel())}` : T('hover')}</span></div><button class="icon-btn lb__moreBtn" id="lbMoreBtn" aria-expanded="false" aria-controls="lbMore" aria-label="${esc(T('moreTools'))}" title="${esc(T('moreTools'))}">${ICON.more}</button></div>
|
|
2220
2340
|
<div class="lb__scroll" id="lbScroll">${(() => {
|
|
2221
2341
|
const bn0 = S.cmp && cmpNode(n.id), bcap0 = bn0?.captures?.[S.loc]?.[v.name];
|
|
2222
2342
|
if (S.lbMode === 'view' && viewSrc(cap) && !S.cmp) return `<p class="note" style="margin:0 0 8px">${T('htmlViewNote')}</p>` + liveStage(cap, v, 'lbFrame', RUN.live_bases, true);
|
|
@@ -2233,13 +2353,14 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2233
2353
|
<div class="lb__side" id="lbSide">
|
|
2234
2354
|
<div class="lb__grab" id="lbGrab" role="button" tabindex="0" aria-controls="lbSide" aria-expanded="${(S.lbSheet || 'peek') !== 'peek'}" aria-label="${esc(T('sheetHandle'))}"></div>
|
|
2235
2355
|
<div class="lb__head">
|
|
2236
|
-
<div class="lb__row"><span class="kind">${ICON[n.kind]}</span><span class="badge st-${s}">${MARK[s]} ${T(s)}</span><span class="note">${n.item ? T('pageWord') : T('step')} ${i + 1} ${T('of')} ${list.length}</span>${n.item ? `<button class="btn" id="lbBack">${ICON.stack} ${T('backToIndex')}</button>` : ''}
|
|
2356
|
+
<div class="lb__row"><span class="kind">${ICON[n.kind]}</span><span class="lb__meta"><span class="badge st-${s}">${MARK[s]} ${T(s)}</span><span class="note">${n.item ? T('pageWord') : T('step')} ${i + 1} ${T('of')} ${list.length}</span>${n.item ? `<button class="btn" id="lbBack">${ICON.stack} ${T('backToIndex')}</button>` : ''}</span>
|
|
2237
2357
|
<span class="lb__nav">${APP() ? `<button class="btn c-jump" id="lbComments" title="${esc(T('commentsTitle'))} (C)">${ICON.comment}${openCount('step', n.id) ? ` ${openCount('step', n.id)}` : ''}</button>` : ''}<button class="btn" id="lbLink" title="${T('copyLink')}">${ICON.external} <span class="lb__linkTxt">${T('copyLink')}</span></button><button class="icon-btn" id="lbPrev" aria-label="Previous" ${i === 0 ? 'disabled' : ''}>${ICON.prev}</button><button class="icon-btn" id="lbNext" aria-label="Next" ${i === list.length - 1 ? 'disabled' : ''}>${ICON.next}</button><button class="icon-btn" id="lbClose" aria-label="Close">${ICON.close}</button></span></div>
|
|
2238
2358
|
<h3 id="lbTitle">${esc(loc(n.title))}</h3>
|
|
2239
2359
|
<div class="node__path">${n.session ? sessChip(n.session) + ' · ' : ''}${cap?.http_status >= 300 ? `<span class="http ${cap.http_status >= 500 ? 'http--5' : ''}" title="HTTP ${esc(cap.http_status)}">${esc(cap.http_status)}</span>` : ''}${esc(cap?.live ? cap.live.entry : cap?.url || loc(n.path))}</div>
|
|
2240
2360
|
${sameAsCmp(n) ? `<p class="note">${T('unchangedSince')} ${esc(cmpLabel())}</p>` : ''}${cap?.live?.stale_capture ? `<p class="note">${T('staleCapture')}</p>` : ''}
|
|
2241
2361
|
</div>
|
|
2242
|
-
<div class="lbtabs" role="tablist">${[['overview', T('tabOverview')], ['page', T('tabPage')], ['tech', T('tabTech')]].map(([k, l]) => `<button role="tab" data-lbtab="${k}" aria-selected="${(S.lbTab || 'overview') === k}">${l}</button>`).join('')}</div>
|
|
2362
|
+
<div class="lbtabs" role="tablist">${[...(DEV.on ? [['dev', T('devTab')]] : []), ['overview', T('tabOverview')], ['page', T('tabPage')], ['tech', T('tabTech')]].map(([k, l]) => `<button role="tab" data-lbtab="${k}" aria-selected="${(S.lbTab || 'overview') === k}">${l}</button>`).join('')}</div>
|
|
2363
|
+
${DEV.on ? `<div class="lbtab" data-pane="dev" ${S.lbTab === 'dev' ? '' : 'hidden'}>${devPaneHtml()}</div>` : ''}
|
|
2243
2364
|
<div class="lbtab" data-pane="overview" ${(S.lbTab || 'overview') === 'overview' ? '' : 'hidden'}>
|
|
2244
2365
|
<div id="lbSideTop"></div>${inspectorHtml()}
|
|
2245
2366
|
${stepStatusHtml(n)}
|
|
@@ -2253,6 +2374,7 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2253
2374
|
</div>
|
|
2254
2375
|
<div class="lbtab" data-pane="page" ${S.lbTab === 'page' ? '' : 'hidden'}>
|
|
2255
2376
|
${sigSection(cap)}
|
|
2377
|
+
${keyboardBlock(cap)}
|
|
2256
2378
|
${metaSection(n, cap, v)}
|
|
2257
2379
|
${(n.attachments || []).length ? `<div class="sec"><h4>${T('attachments')}</h4><div class="files">${n.attachments.map((a) => `<a class="file" href="${esc(asset(a.file))}" target="_blank" rel="noopener"><span class="file__prev"><img src="${esc(asset(a.file))}" alt="" style="max-width:100%;max-height:74px"></span><span class="file__name">${ICON.crop}${esc(a.title)}${a.locale ? ` · ${esc(a.locale)}` : ''}</span></a>`).join('')}</div></div>` : ''}
|
|
2258
2380
|
${cap?.tree ? `<details class="sec" id="treeSec"><summary><h4>${ICON.html} ${T('uiTree')}</h4></summary><p class="note" id="treeNote">${T('treeHint')}</p><input type="search" class="tree-q" id="treeQ" placeholder="${esc(T('treeSearch'))}"><div class="tree" id="treeBody"></div></details>` : ''}
|
|
@@ -2273,6 +2395,7 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2273
2395
|
wireLbOnce(lb);
|
|
2274
2396
|
renderFocusBar(lb, n, v, cap, list, i);
|
|
2275
2397
|
lb.hidden = false;
|
|
2398
|
+
lbSheetFit(); LB_RO?.disconnect(); LB_RO?.observe(lb.querySelector('.lb__head'));
|
|
2276
2399
|
fitLive(); requestAnimationFrame(fitLive);
|
|
2277
2400
|
setTimeout(() => presenceView(true), 0);
|
|
2278
2401
|
setHash({ step: id, loc: S.loc, vp: v.name, focus: S.focus ? '1' : null });
|
|
@@ -2288,14 +2411,21 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2288
2411
|
if ($('lbMode')) seg($('lbMode'), [...(liveSrc(cap) ? [['live', T('liveMode')]] : []), ...(viewSrc(cap) ? [['view', T('htmlView')]] : []), ['shot', T('shotMode')]], S.lbMode, (m) => { S.lbMode = m; LIVE.mode = null; openLb(id, v.name); });
|
|
2289
2412
|
if ($('lbLiveTools')) renderLiveTools();
|
|
2290
2413
|
if ($('lbProto')) $('lbProto').onclick = () => ptOpen(id, v.name);
|
|
2291
|
-
|
|
2414
|
+
if ($('lbDev')) $('lbDev').onclick = () => setDev(!DEV.on);
|
|
2415
|
+
wireStepStatus(n); wireCopyCss(); wireDev();
|
|
2416
|
+
if (DEV.on && DEV.shot) {
|
|
2417
|
+
const im = $('lbImg')?.querySelector('img');
|
|
2418
|
+
loadShotSpec(cap).then(() => { if (S.open !== id) return; if (im && !im.complete) im.addEventListener('load', drawShot, { once: true }); else drawShot(); renderDev(); });
|
|
2419
|
+
}
|
|
2292
2420
|
// pins of comments made on elements, drawn over the screenshot too
|
|
2293
2421
|
const pimg = $('lbImg');
|
|
2422
|
+
if (pimg && cap) overlayRegions(pimg, cap, v);
|
|
2423
|
+
if ($('kbToggle')) $('kbToggle').onclick = () => { S.kbShow = !S.kbShow; $('kbToggle').textContent = S.kbShow ? T('kbHide') : T('kbShow'); overlayRegions(pimg, cap, v); };
|
|
2294
2424
|
if (pimg) { const place = () => { const k = pimg.querySelector('img').clientWidth / (cap?.size?.[0] || v.w); for (const p of pinsFor(n, v.name)) { if (!p.box) continue; const d = document.createElement('span'); d.className = 'pinmark'; d.textContent = p.n; Object.assign(d.style, { left: `${(p.box[0] + p.box[2]) * k}px`, top: `${p.box[1] * k}px`, opacity: p.resolved ? 0.5 : 1 }); pimg.appendChild(d); } }; const im = pimg.querySelector('img'); if (im) im.complete ? place() : im.addEventListener('load', place); }
|
|
2295
2425
|
if (S.cmp) {
|
|
2296
2426
|
if ($('lbCmpSeg')) seg($('lbCmpSeg'), [['side', T('cmpSide')], ['diff', T('cmpDiff')]], S.lbCmp, (m) => { S.lbCmp = m; openLb(id, v.name); });
|
|
2297
2427
|
const bcap = cmpNode(n.id)?.captures?.[S.loc]?.[v.name];
|
|
2298
|
-
if (cap && bcap) pixelDiff(`${n.id}|${S.loc}|${v.name}`, asset(cap.image || cap.png), S.cmp.base + (bcap.image || bcap.png), Math.min(1000, v.w)).then((r) => {
|
|
2428
|
+
if (cap && bcap) pixelDiff(`${n.id}|${S.loc}|${v.name}`, asset(cap.image || cap.png), S.cmp.base + (bcap.image || bcap.png), Math.min(1000, v.w), ignOf(cap, bcap)).then((r) => {
|
|
2299
2429
|
const px = $('lbPx'); if (px) { px.innerHTML = `<span class="pxd ${pxClass(r.ratio)}">◐ ${pxText(r.ratio)}</span>`; }
|
|
2300
2430
|
const d = $('lbDiff'); if (d) { d.innerHTML = ''; d.appendChild(r.canvas); }
|
|
2301
2431
|
}).catch(() => { const px = $('lbPx'); if (px) px.textContent = T('pxUnavail'); });
|
|
@@ -2362,9 +2492,20 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2362
2492
|
if (li) { const im = img?.querySelector('img'); const go = () => setTimeout(() => li.click(), 60); im && !im.complete ? im.addEventListener('load', go, { once: true }) : go(); }
|
|
2363
2493
|
}
|
|
2364
2494
|
}
|
|
2365
|
-
function closeLb () { const lb = $('lb'); lb.hidden = true; lb.classList.remove('is-focus', 'is-idle'); clearTimeout(FX.timer); S.open = null; S.focus = false; LIVE.mode = null; LIVE.insp = null; setHash(null); presenceView(); }
|
|
2495
|
+
function closeLb () { DEV.on = false; devReset(); if (S.lbTab === 'dev') S.lbTab = 'overview'; const lb = $('lb'); lb.hidden = true; lb.classList.remove('is-focus', 'is-idle'); clearTimeout(FX.timer); S.open = null; S.focus = false; LIVE.mode = null; LIVE.insp = null; setHash(null); presenceView(); }
|
|
2366
2496
|
const lbPhone = () => matchMedia('(max-width:700px)').matches;
|
|
2367
2497
|
function closeLbMore () { const m = $('lbMore'); if (!m) return; m.classList.remove('is-open'); $('lbMoreBtn')?.setAttribute('aria-expanded', 'false'); }
|
|
2498
|
+
/** Phone sheet geometry: the collapsed sheet shows the whole header row and one line of the title (the row wraps
|
|
2499
|
+
when a page has "back to index"), and the tabs stick right under the header instead of over it. */
|
|
2500
|
+
function lbSheetFit () {
|
|
2501
|
+
const lb = $('lb'), box = lb.querySelector('.lb__box'), head = lb.querySelector('.lb__head'), side = $('lbSide'); if (!box || !head || !side) return;
|
|
2502
|
+
if (!lbPhone() || S.focus) { box.style.removeProperty('--peek'); side.style.removeProperty('--lbhead'); return; }
|
|
2503
|
+
const row = head.querySelector('.lb__row'), h3 = head.querySelector('h3');
|
|
2504
|
+
const line = h3 ? parseFloat(getComputedStyle(h3).lineHeight) || 20 : 20;
|
|
2505
|
+
box.style.setProperty('--peek', `calc(${Math.max(98, Math.ceil(16 + (row?.offsetHeight || 32) + 4 + line + 10))}px + env(safe-area-inset-bottom))`);
|
|
2506
|
+
side.style.setProperty('--lbhead', `${head.offsetHeight}px`);
|
|
2507
|
+
}
|
|
2508
|
+
const LB_RO = typeof ResizeObserver === 'function' ? new ResizeObserver(() => lbSheetFit()) : null;
|
|
2368
2509
|
/** The phone bottom sheet of the detail: peek (title and status), half, full. */
|
|
2369
2510
|
function setSheet (st) {
|
|
2370
2511
|
S.lbSheet = st; $('lb').dataset.sheet = st;
|
|
@@ -2376,7 +2517,7 @@ function setSheet (st) {
|
|
|
2376
2517
|
function setFocus (on) {
|
|
2377
2518
|
if (!S.open) return;
|
|
2378
2519
|
S.focus = !!on; closeLbMore();
|
|
2379
|
-
if (on) { C.pinMode = false; LIVE.mode = null; toFrame({ type: 'qawalk:mode', mode: null }); }
|
|
2520
|
+
if (on) { C.pinMode = false; LIVE.mode = null; DEV.on = false; devReset(); if (S.lbTab === 'dev') S.lbTab = 'overview'; toFrame({ type: 'qawalk:mode', mode: null }); }
|
|
2380
2521
|
openLb(S.open, S.lbVp, { keepFrame: true });
|
|
2381
2522
|
}
|
|
2382
2523
|
const FX = { timer: null, over: false };
|
|
@@ -2503,7 +2644,7 @@ function renderReplay () {
|
|
|
2503
2644
|
foot = '';
|
|
2504
2645
|
} else if (n.kind === 'stack') {
|
|
2505
2646
|
const st = n.stack || { sample: [], summary: {} };
|
|
2506
|
-
stage = `<div class="rp__decision"><div class="seg-label">${ICON.stack} ${esc(st.count)} ${
|
|
2647
|
+
stage = `<div class="rp__decision"><div class="seg-label">${ICON.stack} ${esc(st.count)} ${pagesN(st.count)}</div><h3>${esc(loc(n.title))}</h3><div class="stack-fan" style="max-width:520px;height:220px;margin:12px auto">${(st.sample || []).slice(0, 3).map((x) => x.thumb ? `<img src="${esc(asset(x.thumb))}" alt="" style="height:180px">` : '<span class="ph" style="height:180px"></span>').join('')}</div><p class="note">${['pass', 'partial', 'fail', 'skip'].filter((k) => st.summary[k]).map((k) => `${MARK[k]} ${T(k)} ${st.summary[k]}`).join(' · ')}</p><button class="btn btn--primary" data-stack-open="${esc(n.id)}">${T('openIndex')}</button></div>`;
|
|
2507
2648
|
foot = opts.length ? `${optBtn(opts[0], true)}${opts.length > 1 ? `<span class="rp__alts">${T('rpOr')} ${opts.slice(1).map((o) => optBtn(o, false)).join('')}</span>` : ''}` : `<span class="note">${T('rpEnd')}</span>`;
|
|
2508
2649
|
} else {
|
|
2509
2650
|
stage = cap ? `<div class="lb__img ${v.mobile ? 'lb__img--m' : ''}" style="max-width:${v.mobile ? v.w + 16 : v.w}px;width:100%"><img src="${esc(asset(cap.image || cap.png))}" alt="${esc(loc(n.title))}"></div>` : `<p class="lb__empty">${T('notReached')}</p>`;
|
|
@@ -2543,7 +2684,7 @@ function rpKey (e) {
|
|
|
2543
2684
|
/* The run's problems, sorted by what to do about them: what failed (with the reason in words), what a person
|
|
2544
2685
|
should decide (AI unsure or not evaluated) and screens whose quick signals look off. */
|
|
2545
2686
|
function whyOf (n, x) {
|
|
2546
|
-
if (x.counts) return `${(x.counts.fail || 0) + (x.counts.needs_review || 0)}/${n.stack.count} ${
|
|
2687
|
+
if (x.counts) return `${(x.counts.fail || 0) + (x.counts.needs_review || 0)}/${n.stack.count} ${pagesN(n.stack.count)}`;
|
|
2547
2688
|
const cells = viewports().map((v) => ({ v, r: x.by_locale?.[S.loc]?.[v.name] || {} })).filter(({ r }) => r.verdict && r.verdict !== 'pass' && r.verdict !== 'not_reached');
|
|
2548
2689
|
const c = cells.find(({ r }) => r.verdict === 'fail') || cells[0];
|
|
2549
2690
|
if (!c) return x.evidence || '';
|
|
@@ -2831,7 +2972,10 @@ addEventListener('message', (e) => {
|
|
|
2831
2972
|
if (m.type === 'qawalk:key') ptKey({ key: String(m.key), preventDefault () {} });
|
|
2832
2973
|
return;
|
|
2833
2974
|
}
|
|
2834
|
-
if (m.type === '
|
|
2975
|
+
if (typeof m.type === 'string' && m.type.startsWith('qawalk:dev-')) { if (DEV.on && !DEV.shot) devMessage(m); return; }
|
|
2976
|
+
// keys pressed while the page has focus: Dev mode (D, M, Esc)
|
|
2977
|
+
if (m.type === 'qawalk:key' && !$('lb').hidden) { devKey(String(m.key).slice(0, 12), false); return; }
|
|
2978
|
+
if (m.type === 'qawalk:ready' && n) { toFrame({ type: 'qawalk:pins', pins: pinsFor(n, S.lbVp) }); if (LIVE.mode) toFrame({ type: 'qawalk:mode', mode: LIVE.mode }); if (LIVE.mode === 'dev') { DEV.spec = DEV.base = DEV.measure = null; DEV.usage = null; toFrame({ type: 'qawalk:dev-usage' }); renderDev(); } }
|
|
2835
2979
|
if (m.type === 'qawalk:go' && typeof m.node === 'string' && byId(m.node)) {
|
|
2836
2980
|
toast(`${T('prototypeTo')} ${loc(byId(m.node).title)}`); openLb(m.node, S.lbVp);
|
|
2837
2981
|
// a link to a page inside the step's folder opens that page in the live frame, not the step's entry
|
|
@@ -2882,8 +3026,319 @@ function wireCopyCss () {
|
|
|
2882
3026
|
function renderLiveTools () {
|
|
2883
3027
|
const t = $('lbLiveTools'); if (!t) return;
|
|
2884
3028
|
t.innerHTML = `<button class="btn" id="lbInspect" aria-pressed="${LIVE.mode === 'inspect'}">${T('inspect')}</button>${canWrite() ? `<button class="btn" id="lbPin" aria-pressed="${LIVE.mode === 'pick'}">${T('pinComment')}</button>` : ''}`;
|
|
2885
|
-
|
|
2886
|
-
|
|
3029
|
+
const devOff = () => { if (!DEV.on) return; DEV.on = false; devReset(); if (S.lbTab === 'dev') S.lbTab = 'overview'; openLb(S.open, S.lbVp, { keepFrame: true }); };
|
|
3030
|
+
$('lbInspect').onclick = () => { devOff(); LIVE.mode = LIVE.mode === 'inspect' ? null : 'inspect'; toFrame({ type: 'qawalk:mode', mode: LIVE.mode }); if (!LIVE.mode) { LIVE.insp = null; $('inspSec')?.remove(); } renderLiveTools(); presenceSend({ type: 'editing', step: S.open, on: !!LIVE.mode }); };
|
|
3031
|
+
const pin = $('lbPin'); if (pin) pin.onclick = () => { devOff(); LIVE.mode = LIVE.mode === 'pick' ? null : 'pick'; toFrame({ type: 'qawalk:mode', mode: LIVE.mode }); if (LIVE.mode) toast(T('pickHint')); renderLiveTools(); };
|
|
3032
|
+
}
|
|
3033
|
+
// ---- Dev mode (#42): measurements, tokens, states and assets of design steps, for developers and their agents ----
|
|
3034
|
+
// Live steps: the frame helper runs the spec engine (src/lib/devspec.mjs) in the page and answers qawalk:dev-*.
|
|
3035
|
+
// Screenshots: the step spec the CLI captured with the same engine (steps/<step>/<locale>/<viewport>.spec.json).
|
|
3036
|
+
const DEV = { on: false, tab: 'el', spec: null, base: null, measure: null, measureMode: false, usage: null, hl: null, shot: false, shotSpec: undefined, shotKey: null, shotSel: -1, shotTarget: -1 };
|
|
3037
|
+
const DEV_STATES = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
3038
|
+
const DEV_GROUPS = ['type', 'color', 'space', 'size', 'shape', 'layout'];
|
|
3039
|
+
const HEX = /^#[0-9a-f]{3,8}$/i;
|
|
3040
|
+
const devCapOf = () => { const n = S.open && byId(S.open); return n?.captures?.[S.loc]?.[S.lbVp] || null; };
|
|
3041
|
+
/** Dev mode works on a live step (frame helper on the service) or on a capture with a spec or boxes. */
|
|
3042
|
+
function devAvailable (cap) { return !!cap && (isDesign() || !!liveSrc(cap) || !!viewSrc(cap) || !!cap.spec || !!cap.spec_inline); }
|
|
3043
|
+
const devLive = (cap = devCapOf()) => !!APP() && ['live', 'view'].includes(S.lbMode) && !S.cmp && !!(S.lbMode === 'view' ? viewSrc(cap) : liveSrc(cap));
|
|
3044
|
+
function devReset () { Object.assign(DEV, { spec: null, base: null, measure: null, measureMode: false, usage: null, hl: null, shotSel: -1, shotTarget: -1 }); }
|
|
3045
|
+
function setDev (on) {
|
|
3046
|
+
if (!S.open) return;
|
|
3047
|
+
const cap = devCapOf();
|
|
3048
|
+
if (on && !devAvailable(cap)) return;
|
|
3049
|
+
DEV.on = !!on; devReset();
|
|
3050
|
+
if (on && S.focus) S.focus = false; // the inspector lives in the side panel
|
|
3051
|
+
if (on && !APP() && S.lbMode === 'live') { S.lbMode = 'shot'; toast(T('devLocalLive')); } // from disk the live page has no helper
|
|
3052
|
+
DEV.shot = on && !devLive(cap);
|
|
3053
|
+
if (on) { LIVE.insp = null; C.pinMode = false; }
|
|
3054
|
+
LIVE.mode = on && !DEV.shot ? 'dev' : LIVE.mode === 'dev' ? null : LIVE.mode;
|
|
3055
|
+
if (on) S.lbTab = 'dev'; else if (S.lbTab === 'dev') S.lbTab = 'overview';
|
|
3056
|
+
openLb(S.open, S.lbVp, { keepFrame: true });
|
|
3057
|
+
$('lbDev')?.setAttribute('aria-pressed', String(DEV.on)); // a kept frame keeps its tool bar too
|
|
3058
|
+
if (!DEV.on) renderLiveTools();
|
|
3059
|
+
toFrame({ type: 'qawalk:mode', mode: LIVE.mode });
|
|
3060
|
+
if (on && !DEV.shot) toFrame({ type: 'qawalk:dev-usage' });
|
|
3061
|
+
if (on && lbPhone() && S.lbSheet === 'peek') setSheet('half');
|
|
3062
|
+
presenceSend({ type: 'editing', step: S.open, on: !!on });
|
|
3063
|
+
}
|
|
3064
|
+
/** Escape in Dev mode: first the measurement, then the selection, then the mode itself. */
|
|
3065
|
+
function devEsc () {
|
|
3066
|
+
if (DEV.measureMode || DEV.measure || DEV.shotTarget >= 0) { devMeasureMode(false); DEV.measure = null; DEV.shotTarget = -1; drawShot(); renderDev(); return; }
|
|
3067
|
+
if (DEV.spec) { DEV.spec = DEV.base = null; DEV.shotSel = -1; toFrame({ type: 'qawalk:dev-clear' }); drawShot(); renderDev(); return; }
|
|
3068
|
+
setDev(false);
|
|
3069
|
+
}
|
|
3070
|
+
function devMeasureMode (on) { DEV.measureMode = !!on; toFrame({ type: 'qawalk:dev-measure-mode', on: DEV.measureMode }); if (!on) { DEV.measure = null; DEV.shotTarget = -1; drawShot(); } renderDev(); }
|
|
3071
|
+
function devNav (dir) { if (DEV.shot) shotNav(dir); else toFrame({ type: 'qawalk:dev-nav', dir }); }
|
|
3072
|
+
|
|
3073
|
+
// ---- data from the frame: never trusted, every field cut to size ----
|
|
3074
|
+
const sStr = (x, n) => typeof x === 'string' ? x.slice(0, n) : x == null ? '' : String(x).slice(0, n);
|
|
3075
|
+
const sNum = (x) => Number.isFinite(+x) ? +x : 0;
|
|
3076
|
+
function sTok (t) {
|
|
3077
|
+
if (!t || typeof t !== 'object' || !['exact', 'nearest', 'none'].includes(t.m)) return null;
|
|
3078
|
+
const o = { m: t.m };
|
|
3079
|
+
for (const [k, n] of [['name', 160], ['value', 200], ['mode', 40], ['via', 80], ['hard', 200]]) if (t[k] != null && t[k] !== false) o[k] = sStr(typeof t[k] === 'object' ? JSON.stringify(t[k]) : t[k], n);
|
|
3080
|
+
return o;
|
|
3081
|
+
}
|
|
3082
|
+
function sanitizeSpec (s) {
|
|
3083
|
+
if (!s || typeof s !== 'object') return null;
|
|
3084
|
+
const rows = (Array.isArray(s.rows) ? s.rows : []).slice(0, 60).filter((r) => r && DEV_GROUPS.includes(r.g)).map((r) => ({ g: r.g, prop: sStr(r.prop, 40), value: sStr(r.value, 300), tok: sTok(r.tok), ...(Array.isArray(r.parts) ? { parts: r.parts.slice(0, 4).map((p) => ({ value: sStr(p?.value, 60), tok: sTok(p?.tok) })) } : {}) }));
|
|
3085
|
+
const c = s.contrast && typeof s.contrast === 'object' ? { ratio: sNum(s.contrast.ratio), aa: !!s.contrast.aa, aaa: !!s.contrast.aaa, large: !!s.contrast.large, approx: !!s.contrast.approx, fg: HEX.test(s.contrast.fg) ? s.contrast.fg : null, bg: HEX.test(s.contrast.bg) ? s.contrast.bg : null } : null;
|
|
3086
|
+
const preview = (p) => typeof p === 'string' && p.length < 60000 && /^data:image\/svg\+xml;base64,[A-Za-z0-9+/=]+$/.test(p) ? p : null;
|
|
3087
|
+
return {
|
|
3088
|
+
sel: sStr(s.sel, 500), tag: sStr(s.tag, 20), role: s.role ? sStr(s.role, 40) : null, name: sStr(s.name, 80),
|
|
3089
|
+
box: Array.isArray(s.box) ? s.box.slice(0, 4).map(sNum) : [0, 0, 0, 0], state: DEV_STATES.includes(s.state) ? s.state : 'default',
|
|
3090
|
+
rows, contrast: c, states: (Array.isArray(s.states) ? s.states : []).filter((x) => DEV_STATES.includes(x)),
|
|
3091
|
+
parent: s.parent && typeof s.parent === 'object' ? { sel: sStr(s.parent.sel, 500), tag: sStr(s.parent.tag, 20), inset: Array.isArray(s.parent.inset) ? s.parent.inset.slice(0, 4).map(sNum) : null } : null,
|
|
3092
|
+
assets: (Array.isArray(s.assets) ? s.assets : []).slice(0, 30).map((a, i) => ({ i: Number.isInteger(a?.i) ? a.i : i, kind: ['img', 'svg', 'background'].includes(a?.kind) ? a.kind : 'img', name: sStr(a?.name, 80).replace(/[^\w.\- ]+/g, '-') || 'asset', size: Array.isArray(a?.size) ? a.size.slice(0, 2).map(sNum) : null, alt: a?.alt ? sStr(a.alt, 120) : null, preview: preview(a?.preview) }))
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function sanitizeUsage (u) {
|
|
3096
|
+
if (!u || typeof u !== 'object') return null;
|
|
3097
|
+
const used = {}; for (const [k, v] of Object.entries(u.used || {}).slice(0, 600)) used[sStr(k, 160)] = sNum(v);
|
|
3098
|
+
return {
|
|
3099
|
+
used,
|
|
3100
|
+
off: (Array.isArray(u.off) ? u.off : []).slice(0, 60).map((o) => ({ prop: sStr(o?.prop, 40), value: sStr(o?.value, 200), count: sNum(o?.count), nearest: o?.nearest ? sStr(o.nearest, 160) : null, sel: sStr(o?.sel, 300) })),
|
|
3101
|
+
hard: (Array.isArray(u.hard) ? u.hard : []).slice(0, 60).map((o) => ({ sel: sStr(o?.sel, 160), prop: sStr(o?.prop, 40), value: sStr(o?.value, 200), token: sStr(o?.token, 160) })),
|
|
3102
|
+
all: (Array.isArray(u.all) ? u.all : []).slice(0, 600).map((t) => ({ name: sStr(t?.name, 160), value: sStr(t?.value, 120), kind: sStr(t?.kind, 10), mode: t?.mode ? sStr(t.mode, 40) : null })),
|
|
3103
|
+
modes: (Array.isArray(u.modes) ? u.modes : []).slice(0, 10).map((m) => sStr(m, 40))
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// ---- screenshots: the captured spec drawn over the image ----
|
|
3108
|
+
const SPEC_GROUP = (p) => /^(font|line-height|letter-spacing|text-)/.test(p) ? 'type' : /color$|^background/.test(p) ? 'color' : /^(padding|margin|gap|row-gap|column-gap)$/.test(p) ? 'space' : /^(border-radius|border-width|box-shadow|opacity)$/.test(p) ? 'shape' : /^(display|flex|justify|align|grid|position)/.test(p) ? 'layout' : 'size';
|
|
3109
|
+
/** A spec element (compact styles) in the inspector's form. */
|
|
3110
|
+
function specElView (e, list) {
|
|
3111
|
+
const tokOf = (s) => s.token ? { m: 'exact', name: s.token, value: s.value, ...(s.mode ? { mode: s.mode } : {}), ...(s.hard ? { hard: s.value } : {}) } : s.nearest ? { m: 'nearest', name: s.nearest } : s.token === null ? { m: 'none' } : null;
|
|
3112
|
+
const rows = Object.entries(e.styles || {}).map(([prop, s]) => {
|
|
3113
|
+
const parts = String(s.value).split(' ');
|
|
3114
|
+
const names = String(s.token || s.nearest || '').split(' ');
|
|
3115
|
+
const multi = parts.length > 1 && names.length === parts.length && /^(padding|margin|border-radius|border-width)$/.test(prop);
|
|
3116
|
+
return { g: SPEC_GROUP(prop), prop, value: String(s.value), tok: multi ? null : tokOf(s), ...(multi ? { parts: parts.map((v, i) => ({ value: v, tok: names[i] === '0' ? null : s.token ? { m: 'exact', name: names[i] } : { m: 'nearest', name: names[i] } })) } : {}) };
|
|
3117
|
+
});
|
|
3118
|
+
if (e.box) rows.push({ g: 'size', prop: 'width', value: `${e.box[2]}px` }, { g: 'size', prop: 'height', value: `${e.box[3]}px` });
|
|
3119
|
+
const p = e.parent >= 0 ? list[e.parent] : null;
|
|
3120
|
+
const inset = p && e.box && p.box ? [e.box[1] - p.box[1], p.box[0] + p.box[2] - e.box[0] - e.box[2], p.box[1] + p.box[3] - e.box[1] - e.box[3], e.box[0] - p.box[0]] : null;
|
|
3121
|
+
return sanitizeSpec({ sel: e.sel, tag: e.tag, role: e.role, name: e.name, box: e.box, rows, contrast: e.contrast ? { ratio: e.contrast.ratio, aa: e.contrast.aa } : null, states: [], parent: p ? { sel: p.sel, tag: p.tag, inset } : null, assets: [] });
|
|
3122
|
+
}
|
|
3123
|
+
async function loadShotSpec (cap) {
|
|
3124
|
+
const key = `${S.open}|${S.loc}|${S.lbVp}`;
|
|
3125
|
+
if (DEV.shotKey === key) return DEV.shotSpec;
|
|
3126
|
+
DEV.shotKey = key; DEV.shotSpec = null;
|
|
3127
|
+
if (cap?.spec_inline) DEV.shotSpec = cap.spec_inline;
|
|
3128
|
+
else if (cap?.spec && APP()) { try { const r = await fetch(asset(cap.spec), { credentials: 'same-origin' }); if (r.ok) DEV.shotSpec = await r.json(); } catch {} }
|
|
3129
|
+
if (DEV.shotSpec && !Array.isArray(DEV.shotSpec.elements)) DEV.shotSpec = null;
|
|
3130
|
+
return DEV.shotSpec;
|
|
3131
|
+
}
|
|
3132
|
+
/** Elements to pick on a screenshot: the spec's, else the criteria boxes the capture recorded. */
|
|
3133
|
+
function shotElements (cap) {
|
|
3134
|
+
if (DEV.shotSpec) return DEV.shotSpec.elements.filter((e) => Array.isArray(e.box)).slice(0, 400);
|
|
3135
|
+
return Object.entries(cap?.boxes || {}).flatMap(([id, list]) => (list || []).map((b) => ({ i: -1, parent: -1, tag: id, sel: id, box: [b.x, b.y, b.w, b.h], styles: {} }))).filter((e) => e.box[2] > 0 && e.box[3] > 0);
|
|
3136
|
+
}
|
|
3137
|
+
function shotGaps (a, b) {
|
|
3138
|
+
const R = (x) => ({ l: x[0], t: x[1], r: x[0] + x[2], b: x[1] + x[3] });
|
|
3139
|
+
const A = R(a), B = R(b), inside = (o, i) => i.l >= o.l && i.r <= o.r && i.t >= o.t && i.b <= o.b;
|
|
3140
|
+
if (inside(B, A) || inside(A, B)) { const [o, i] = inside(B, A) ? [B, A] : [A, B]; return { inside: true, d: { top: i.t - o.t, right: o.r - i.r, bottom: o.b - i.b, left: i.l - o.l } }; }
|
|
3141
|
+
const d = {};
|
|
3142
|
+
if (B.l >= A.r) d.right = B.l - A.r; else if (A.l >= B.r) d.left = A.l - B.r;
|
|
3143
|
+
if (B.t >= A.b) d.bottom = B.t - A.b; else if (A.t >= B.b) d.top = A.t - B.b;
|
|
3144
|
+
return { inside: false, d };
|
|
3145
|
+
}
|
|
3146
|
+
function drawShot () {
|
|
3147
|
+
const host = $('lbImg'); if (!host) return;
|
|
3148
|
+
host.querySelector('.devshot')?.remove();
|
|
3149
|
+
if (!DEV.on || !DEV.shot) return;
|
|
3150
|
+
const cap = devCapOf(), img = host.querySelector('img'); if (!img || !cap) return;
|
|
3151
|
+
const k = img.clientWidth / (cap.size?.[0] || img.naturalWidth || 1);
|
|
3152
|
+
const els = shotElements(cap);
|
|
3153
|
+
const layer = document.createElement('div'); layer.className = 'devshot';
|
|
3154
|
+
Object.assign(layer.style, { left: `${img.offsetLeft}px`, top: `${img.offsetTop}px`, width: `${img.clientWidth}px`, height: `${img.clientHeight}px` });
|
|
3155
|
+
const box = (b, cls, label) => { const d = document.createElement('div'); d.className = cls; Object.assign(d.style, { left: `${b[0] * k}px`, top: `${b[1] * k}px`, width: `${b[2] * k}px`, height: `${b[3] * k}px` }); if (label) { const t = document.createElement('span'); t.textContent = label; d.appendChild(t); } layer.appendChild(d); };
|
|
3156
|
+
const sel = els[DEV.shotSel], tgt = els[DEV.shotTarget];
|
|
3157
|
+
if (DEV.hl) for (const e of els) { const hit = Object.entries(e.styles || {}).some(([p, s]) => DEV.hl.token ? String(s.token || '').split(' ').includes(DEV.hl.token) : p === DEV.hl.prop && s.value === DEV.hl.value); if (hit) box(e.box, 'devshot__hl'); }
|
|
3158
|
+
if (tgt) box(tgt.box, 'devshot__tgt');
|
|
3159
|
+
if (sel) box(sel.box, 'devshot__sel', `${sel.box[2]} × ${sel.box[3]}`);
|
|
3160
|
+
const hover = document.createElement('div'); hover.className = 'devshot__hov'; hover.hidden = true; const hl = document.createElement('span'); hover.appendChild(hl); layer.appendChild(hover);
|
|
3161
|
+
const at = (ev) => { const r = layer.getBoundingClientRect(), x = (ev.clientX - r.left) / k, y = (ev.clientY - r.top) / k; let best = -1, area = Infinity; els.forEach((e, i) => { const [bx, by, bw, bh] = e.box; if (x >= bx && x <= bx + bw && y >= by && y <= by + bh && bw * bh < area) { area = bw * bh; best = i; } }); return best; };
|
|
3162
|
+
layer.addEventListener('mousemove', (ev) => {
|
|
3163
|
+
const i = at(ev);
|
|
3164
|
+
if (i >= 0 && DEV.shotSel >= 0 && i !== DEV.shotSel && (ev.altKey || DEV.measureMode)) { if (i !== DEV.shotTarget) shotMeasure(i); hover.hidden = true; return; }
|
|
3165
|
+
if (i < 0) { hover.hidden = true; return; }
|
|
3166
|
+
const b = els[i].box; Object.assign(hover.style, { left: `${b[0] * k}px`, top: `${b[1] * k}px`, width: `${b[2] * k}px`, height: `${b[3] * k}px` }); hl.textContent = `${els[i].tag} · ${b[2]} × ${b[3]}`; hover.hidden = false;
|
|
3167
|
+
});
|
|
3168
|
+
layer.addEventListener('mouseleave', () => { hover.hidden = true; });
|
|
3169
|
+
layer.addEventListener('click', (ev) => { const i = at(ev); if (i < 0) return; if (DEV.shotSel >= 0 && (ev.altKey || DEV.measureMode)) { shotMeasure(i); return; } shotSelect(i); });
|
|
3170
|
+
host.appendChild(layer);
|
|
3171
|
+
}
|
|
3172
|
+
function shotSelect (i) {
|
|
3173
|
+
const els = shotElements(devCapOf()); if (!els[i]) return;
|
|
3174
|
+
DEV.shotSel = i; DEV.shotTarget = -1; DEV.measure = null;
|
|
3175
|
+
DEV.spec = DEV.base = specElView(els[i], DEV.shotSpec?.elements || els);
|
|
3176
|
+
if (lbPhone() && S.lbSheet === 'peek') setSheet('half');
|
|
3177
|
+
drawShot(); renderDev();
|
|
3178
|
+
}
|
|
3179
|
+
function shotMeasure (i) {
|
|
3180
|
+
const els = shotElements(devCapOf()); const a = els[DEV.shotSel], b = els[i]; if (!a || !b) return;
|
|
3181
|
+
DEV.shotTarget = i; const g = shotGaps(a.box, b.box);
|
|
3182
|
+
DEV.measure = { target: { sel: b.sel, tag: b.tag, name: b.name || '' }, inside: g.inside, d: g.d };
|
|
3183
|
+
drawShot(); renderDev();
|
|
3184
|
+
}
|
|
3185
|
+
function shotNav (dir) {
|
|
3186
|
+
const els = shotElements(devCapOf()); if (!els.length) return;
|
|
3187
|
+
const cur = els[DEV.shotSel];
|
|
3188
|
+
let i = -1;
|
|
3189
|
+
if (!cur || dir === 'start') i = Math.max(0, els.findIndex((e) => e.role === 'main'));
|
|
3190
|
+
else if (dir === 'parent') i = cur.parent >= 0 ? els.findIndex((e) => e.i === cur.parent) : -1;
|
|
3191
|
+
else if (dir === 'child') i = els.findIndex((e) => e.parent === cur.i && cur.i >= 0);
|
|
3192
|
+
else { const sib = els.map((e, k) => [e, k]).filter(([e]) => e.parent === cur.parent); const at = sib.findIndex(([, k]) => k === DEV.shotSel); const nx = sib[at + (dir === 'next' ? 1 : -1)]; i = nx ? nx[1] : -1; }
|
|
3193
|
+
if (i >= 0) shotSelect(i);
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// ---- the panel ----
|
|
3197
|
+
const W_NAMES = { color: ['barvu textu', 'text color'], 'background-color': ['pozadí', 'background'], 'border-color': ['barvu rámečku', 'border color'], 'font-family': ['písmo', 'font family'], 'font-size': ['velikost písma', 'font size'], 'font-weight': ['řez písma', 'font weight'], 'line-height': ['výšku řádku', 'line height'], 'letter-spacing': ['prostrkání', 'letter spacing'], padding: ['vnitřní odsazení', 'padding'], margin: ['vnější odsazení', 'margin'], gap: ['mezery mezi potomky', 'gap'], 'row-gap': ['mezery mezi řádky', 'row gap'], 'column-gap': ['mezery mezi sloupci', 'column gap'], 'border-radius': ['zaoblení rohů', 'corner radius'], 'border-width': ['šířku rámečku', 'border width'], 'box-shadow': ['stín', 'shadow'] };
|
|
3198
|
+
const devVar = (name) => `var(--${String(name).replace(/\./g, '-')})`;
|
|
3199
|
+
const devTokName = (r) => r.parts ? (r.parts.every((p) => !p.tok || p.tok.m === 'exact') && r.parts.some((p) => p.tok) ? r.parts.map((p) => p.tok ? p.tok.name : '0').join(' ') : null) : r.tok?.m === 'exact' ? r.tok.name : null;
|
|
3200
|
+
const devNearest = (r) => r.parts ? (r.parts.some((p) => p.tok && p.tok.m !== 'exact') ? r.parts.map((p) => p.tok?.name || (p.tok ? '?' : '0')).join(' ') : null) : r.tok?.m === 'nearest' ? r.tok.name : null;
|
|
3201
|
+
function devElLabel (s) { const q = UI === 'cs' ? ['„', '“'] : ['"', '"']; return `${s.tag}${s.name ? ` ${q[0]}${s.name}${q[1]}` : ''}`; }
|
|
3202
|
+
function devPromptLine (r, s) {
|
|
3203
|
+
const w = W_NAMES[r.prop]?.[UI === 'cs' ? 0 : 1] || r.prop, el = devElLabel(s);
|
|
3204
|
+
const tok = devTokName(r), near = devNearest(r);
|
|
3205
|
+
if (tok) return T('promptUse').replace('%W', w).replace('%T', tok).replace('%E', el);
|
|
3206
|
+
if (near) return T('promptOff').replace('%P', r.prop).replace('%V', r.value).replace('%T', near);
|
|
3207
|
+
if (r.tok?.m === 'none') return T('promptOffNone').replace('%P', r.prop).replace('%V', r.value);
|
|
3208
|
+
return null;
|
|
3209
|
+
}
|
|
3210
|
+
function devCopies (s) {
|
|
3211
|
+
const n = byId(S.open);
|
|
3212
|
+
const rows = s.rows.filter((r) => r.g !== 'size');
|
|
3213
|
+
const css = `/* ${s.sel}${s.state !== 'default' ? ` :${s.state}` : ''} */\n` + rows.map((r) => `${r.prop}: ${r.value};`).join('\n');
|
|
3214
|
+
const vars = `/* ${s.sel}${s.state !== 'default' ? ` :${s.state}` : ''} */\n` + rows.map((r) => {
|
|
3215
|
+
const tok = devTokName(r), near = devNearest(r);
|
|
3216
|
+
if (tok) return `${r.prop}: ${tok.split(' ').map((t) => t === '0' ? '0' : devVar(t)).join(' ')}; /* ${r.value} */`;
|
|
3217
|
+
return `${r.prop}: ${r.value};${near ? ` /* ${T('notToken')}, ${T('nearest')} ${near} */` : r.tok?.m === 'none' ? ` /* ${T('notToken')} */` : ''}`;
|
|
3218
|
+
}).join('\n');
|
|
3219
|
+
const tokens = {}, off = {};
|
|
3220
|
+
for (const r of rows) { const tok = devTokName(r); if (tok) tokens[r.prop] = tok.includes(' ') ? tok.split(' ') : tok; else if (devNearest(r) || r.tok?.m === 'none') off[r.prop] = { value: r.value, nearest: devNearest(r) }; }
|
|
3221
|
+
const json = JSON.stringify({ element: s.sel, ...(s.state !== 'default' ? { state: s.state } : {}), tokens, ...(Object.keys(off).length ? { not_tokens: off } : {}) }, null, 2);
|
|
3222
|
+
const lines = rows.map((r) => devPromptLine(r, s)).filter(Boolean);
|
|
3223
|
+
const prompt = [T('promptHead').replace('%E', `${devElLabel(s)} (${s.sel})`).replace('%S', `${n ? loc(n.title) : S.open}${s.state !== 'default' ? `, :${s.state}` : ''}`), ...lines.map((l) => `- ${l}`), T('promptSpec').replace('%N', S.open)].join('\n');
|
|
3224
|
+
return { css, vars, json, prompt };
|
|
3225
|
+
}
|
|
3226
|
+
function devTokCell (t, r) {
|
|
3227
|
+
if (!t) return '';
|
|
3228
|
+
if (t.m === 'exact') return `<button type="button" class="devtok" data-dev-copy="${esc(t.name)}" title="${esc(T('copyTokenPath'))}">${esc(t.name)}</button>${t.mode ? ` <span class="devmode">${esc(t.mode)}</span>` : ''}${t.hard ? ` <span class="devwarn" title="${esc(T('typedRaw'))}">${esc(T('typedRawShort'))}</span>` : ''}`;
|
|
3229
|
+
if (t.m === 'nearest') return `<span class="devwarn">${esc(T('notToken'))}</span> · ${esc(T('nearest'))} <button type="button" class="devtok devtok--near" data-dev-copy="${esc(t.name)}">${esc(t.name)}</button>${t.value ? ` <span class="devmuted">${esc(t.value)}</span>` : ''}`;
|
|
3230
|
+
return `<span class="devwarn">${esc(T('notToken'))}</span>${t.via ? ` <span class="devmuted">${esc(t.via)}</span>` : ''}`;
|
|
3231
|
+
}
|
|
3232
|
+
function devRowHtml (r, s, i) {
|
|
3233
|
+
const changed = DEV.base && s.state !== 'default' && DEV.base.rows.find((b) => b.prop === r.prop)?.value !== r.value;
|
|
3234
|
+
const sw = r.g === 'color' && HEX.test(r.value) ? `<span class="devsw" style="background:${r.value}"></span>` : '';
|
|
3235
|
+
const tok = r.parts ? r.parts.map((p) => p.tok ? devTokCell(p.tok) : '<span class="devmuted">0</span>').join(' ') : devTokCell(r.tok);
|
|
3236
|
+
const line = devPromptLine(r, s);
|
|
3237
|
+
return `<tr class="${changed ? 'devchg' : ''}" data-prop="${esc(r.prop)}"><td>${esc(r.prop)}</td><td><span class="devval">${sw}${esc(r.value)}</span>${tok ? `<div class="devtokrow">${tok}</div>` : ''}</td><td>${line ? `<button type="button" class="icon-btn devline" data-dev-line="${i}" aria-label="${esc(T('copyLine'))}" title="${esc(T('copyLine'))}">${ICON.copy}</button>` : ''}</td></tr>`;
|
|
3238
|
+
}
|
|
3239
|
+
function devElHtml () {
|
|
3240
|
+
const s = DEV.spec;
|
|
3241
|
+
const nav = `<div class="devnav" role="group" aria-label="${esc(T('devNavLabel'))}">${[['parent', '↑', T('navParent')], ['child', '↓', T('navChild')], ['prev', '←', T('navPrev')], ['next', '→', T('navNext')]].map(([d, a, l]) => `<button type="button" class="btn" data-dev-nav="${d}" title="${esc(l)} (Shift + ${a})" ${s ? '' : 'disabled'}>${a} ${esc(l)}</button>`).join('')}</div>`;
|
|
3242
|
+
if (!s) return `${DEV.shot ? `<p class="note">${esc(DEV.shotSpec ? T('devShotNote') : T('devNoSpec'))}</p>` : ''}<p class="note">${esc(T('devEmpty'))}</p><button type="button" class="btn" data-dev-nav="start">${esc(T('devStart'))}</button>`;
|
|
3243
|
+
const groups = DEV_GROUPS.map((g) => {
|
|
3244
|
+
const rows = s.rows.map((r, i) => [r, i]).filter(([r]) => r.g === g);
|
|
3245
|
+
const contrast = g === 'color' && s.contrast ? `<tr class="devcontrast"><td>${esc(T('contrast'))}</td><td colspan="2"><b>${s.contrast.ratio} : 1</b> · <span class="${s.contrast.aa ? 'devok' : 'devwarn'}">${esc(s.contrast.aa ? T('aaPass') : T('aaFail'))}</span>${s.contrast.aa ? '' : ` (${esc(T('aaNeed'))} ${s.contrast.large ? 3 : 4.5} : 1)`}${s.contrast.large ? ` · ${esc(T('largeText'))}` : ''}${s.contrast.approx ? `<div class="devmuted">${esc(T('contrastApprox'))}</div>` : ''}</td></tr>` : '';
|
|
3246
|
+
return rows.length || contrast ? `<h5>${esc(T('g_' + g))}</h5><table>${rows.map(([r, i]) => devRowHtml(r, s, i)).join('')}${contrast}</table>` : '';
|
|
3247
|
+
}).join('');
|
|
3248
|
+
const st = DEV.shot ? '' : `<h5>${esc(T('states'))}</h5><div class="seg devstates" role="group" aria-label="${esc(T('states'))}">${DEV_STATES.map((x) => `<button type="button" data-dev-state="${x}" aria-pressed="${s.state === x}" ${x === 'default' || s.states.includes(x) ? '' : 'disabled'}>${x === 'default' ? esc(T('stDefault')) : x === 'disabled' ? 'disabled' : ':' + x}</button>`).join('')}</div>${s.states.length ? (s.state !== 'default' ? `<p class="note">${esc(T('changedInState'))}</p>` : '') : `<p class="note">${esc(T('stNone'))}</p>`}`;
|
|
3249
|
+
const inset = s.parent?.inset;
|
|
3250
|
+
const arrows = (d) => ['top', 'right', 'bottom', 'left'].filter((k) => d[k] != null).map((k) => `<span data-gap="${k}">${{ top: '↑', right: '→', bottom: '↓', left: '←' }[k]} ${Math.round(d[k] * 10) / 10}</span>`).join(' · ');
|
|
3251
|
+
const dist = `<h5>${esc(T('distances'))}</h5><div class="devdist">${inset ? `<p>${esc(T('toParent'))} <code>${esc(s.parent.tag)}</code>: ${arrows({ top: inset[0], right: inset[1], bottom: inset[2], left: inset[3] })}</p>` : ''}${DEV.measure ? `<p id="devMeasure">${esc(T('toTarget'))} <code>${esc(DEV.measure.target.tag)}</code>${DEV.measure.target.name ? ` ${esc(devElLabel(DEV.measure.target).slice(DEV.measure.target.tag.length))}` : ''}: ${arrows(DEV.measure.d) || '0'}</p>` : ''}<p class="note"><button type="button" class="btn" id="devMeasureBtn" aria-pressed="${DEV.measureMode}">${esc(T('measure'))} (M)</button> ${esc(T('measureHint'))}</p></div>`;
|
|
3252
|
+
const assets = s.assets.length ? `<h5>${esc(T('assets'))}</h5><ul class="devassets">${s.assets.map((a) => `<li>${a.preview ? `<img src="${esc(a.preview)}" alt="">` : `<span class="devassets__k">${esc(a.kind)}</span>`}<span class="devassets__n">${esc(a.name)}${a.size ? ` <span class="devmuted">${a.size[0]} × ${a.size[1]}</span>` : ''}${a.alt ? `<span class="devmuted"> · alt „${esc(a.alt)}“</span>` : ''}</span><button type="button" class="btn" data-dev-asset="${a.i}">${esc(T('download'))}</button></li>`).join('')}</ul>` : '';
|
|
3253
|
+
const pin = canWrite() && APP() && !DEV.shot ? `<button type="button" class="btn" id="devPin">${esc(T('pinFromDev'))}</button>` : '';
|
|
3254
|
+
return `<div class="devhead"><b>${esc(devElLabel(s))}</b> <span class="devmuted">${s.box[2]} × ${s.box[3]}${s.role && s.role !== s.tag ? ` · ${esc(s.role)}` : ''}</span><div class="anchor-note">${esc(s.sel)}</div></div>${nav}${dist}${st}${groups}${assets}
|
|
3255
|
+
<div class="devcopy"><button type="button" class="btn" data-dev-copyall="css">${esc(T('copyCss'))}</button><button type="button" class="btn" data-dev-copyall="vars">${esc(T('copyCssVars'))}</button><button type="button" class="btn" data-dev-copyall="json">${esc(T('copyTokens'))}</button><button type="button" class="btn btn--primary" data-dev-copyall="prompt">${esc(T('copyPrompt'))}</button>${pin}</div>
|
|
3256
|
+
<p class="note">${esc(T('devAgentHint').replace('%N', S.open))}</p>`;
|
|
3257
|
+
}
|
|
3258
|
+
function devTokensHtml () {
|
|
3259
|
+
const u = DEV.shot ? (DEV.shotSpec ? { used: DEV.shotSpec.used || {}, off: DEV.shotSpec.off || [], hard: [], all: [] } : null) : DEV.usage;
|
|
3260
|
+
if (!u) return `<p class="note">${esc(DEV.shot ? T('devNoSpec') : T('devLoading'))}</p>`;
|
|
3261
|
+
const all = new Map(u.all.map((t) => [t.name, t]));
|
|
3262
|
+
const used = Object.entries(u.used).sort((a, b) => b[1] - a[1]);
|
|
3263
|
+
if (!used.length && !u.all.length && !u.off.length) return `<p class="note">${esc(T('tokNone'))}</p>`;
|
|
3264
|
+
const on = (q) => DEV.hl && ((q.token && DEV.hl.token === q.token) || (q.prop && DEV.hl.prop === q.prop && DEV.hl.value === q.value));
|
|
3265
|
+
const sw = (v) => HEX.test(v || '') ? `<span class="devsw" style="background:${v}"></span>` : '';
|
|
3266
|
+
const usedNames = new Set(used.map(([k]) => k));
|
|
3267
|
+
const unused = u.all.filter((t) => !usedNames.has(t.name) && !t.name.split('.').slice(0, -1).some((_, i, a) => usedNames.has(t.name.split('.').slice(0, a.length - i).join('.'))));
|
|
3268
|
+
return `<p class="note">${esc(T('tokHint'))}</p>
|
|
3269
|
+
<h5>${esc(T('tokUsed'))} (${used.length})</h5><ul class="devtoks">${used.map(([name, n]) => { const t = all.get(name); return `<li><button type="button" data-dev-hl-token="${esc(name)}" aria-pressed="${!!on({ token: name })}">${sw(t?.value)}<span class="devtoks__n">${esc(name)}</span>${t ? `<span class="devmuted">${esc(t.value)}${t.mode ? ` · ${esc(t.mode)}` : ''}</span>` : ''}<span class="devtoks__c">×${n}</span></button></li>`; }).join('')}</ul>
|
|
3270
|
+
${u.off.length ? `<h5>${esc(T('tokOff'))} (${u.off.length})</h5><ul class="devtoks">${u.off.map((o) => `<li><button type="button" data-dev-hl-prop="${esc(o.prop)}" data-dev-hl-value="${esc(o.value)}" aria-pressed="${!!on(o)}">${sw(o.value)}<span class="devtoks__n">${esc(o.prop)}: ${esc(o.value)}</span><span class="devmuted">${o.nearest ? `${esc(T('nearest'))} ${esc(o.nearest)}` : esc(T('notToken'))}</span><span class="devtoks__c">×${o.count}</span></button></li>`).join('')}</ul>` : ''}
|
|
3271
|
+
${u.hard.length ? `<h5>${esc(T('tokHard'))} (${u.hard.length})</h5><ul class="devtoks devtoks--plain">${u.hard.map((h) => `<li><code>${esc(h.sel)}</code> ${esc(h.prop)}: ${esc(h.value)} → <code>${esc(devVar(h.token))}</code></li>`).join('')}</ul>` : ''}
|
|
3272
|
+
${unused.length ? `<details class="devunused"><summary>${esc(T('tokUnused'))} (${unused.length})</summary><ul class="devtoks devtoks--plain">${unused.map((t) => `<li>${sw(t.value)}<code>${esc(t.name)}</code> <span class="devmuted">${esc(t.value)}${t.mode ? ` · ${esc(t.mode)}` : ''}</span></li>`).join('')}</ul></details>` : ''}`;
|
|
3273
|
+
}
|
|
3274
|
+
function devPaneHtml () {
|
|
3275
|
+
if (!DEV.on) return '';
|
|
3276
|
+
return `<div class="sec dev" id="devPane"><div class="devtabs seg" role="tablist">${[['el', T('devTabEl')], ['tokens', T('devTabTokens')]].map(([k, l]) => `<button type="button" role="tab" data-dev-tab="${k}" aria-selected="${DEV.tab === k}" aria-pressed="${DEV.tab === k}">${esc(l)}</button>`).join('')}</div><div id="devBody">${DEV.tab === 'el' ? devElHtml() : devTokensHtml()}</div></div>`;
|
|
3277
|
+
}
|
|
3278
|
+
function renderDev () {
|
|
3279
|
+
const p = $('devPane'); if (!p) return;
|
|
3280
|
+
p.outerHTML = devPaneHtml();
|
|
3281
|
+
wireDev();
|
|
3282
|
+
}
|
|
3283
|
+
async function devCopy (text) { try { await navigator.clipboard.writeText(text); toast(T('copied')); } catch { toast(text.slice(0, 200)); } }
|
|
3284
|
+
function wireDev () {
|
|
3285
|
+
const p = $('devPane'); if (!p) return;
|
|
3286
|
+
p.onclick = (e) => {
|
|
3287
|
+
const b = e.target.closest('button'); if (!b || b.disabled) return;
|
|
3288
|
+
const s = DEV.spec;
|
|
3289
|
+
if (b.dataset.devTab) { DEV.tab = b.dataset.devTab; if (DEV.tab === 'tokens' && !DEV.shot && !DEV.usage) toFrame({ type: 'qawalk:dev-usage' }); renderDev(); return; }
|
|
3290
|
+
if (b.dataset.devNav) { devNav(b.dataset.devNav); return; }
|
|
3291
|
+
if (b.dataset.devState && s) { toFrame({ type: 'qawalk:dev-state', state: b.dataset.devState }); return; }
|
|
3292
|
+
if (b.dataset.devCopy) { devCopy(b.dataset.devCopy); return; }
|
|
3293
|
+
if (b.dataset.devLine && s) { const r = s.rows[Number(b.dataset.devLine)]; const l = r && devPromptLine(r, s); if (l) devCopy(l); return; }
|
|
3294
|
+
if (b.dataset.devCopyall && s) { devCopy(devCopies(s)[b.dataset.devCopyall]); return; }
|
|
3295
|
+
if (b.dataset.devAsset != null) { toFrame({ type: 'qawalk:dev-asset', i: Number(b.dataset.devAsset) }); return; }
|
|
3296
|
+
if (b.id === 'devMeasureBtn') { devMeasureMode(!DEV.measureMode); return; }
|
|
3297
|
+
if (b.id === 'devPin' && s) { LIVE.anchor = { selector: s.sel, box: s.box, text: s.name, viewport: S.lbVp }; showLbTab('overview'); rerenderComments(); focusComposer(); return; }
|
|
3298
|
+
const q = b.dataset.devHlToken ? { token: b.dataset.devHlToken } : b.dataset.devHlProp ? { prop: b.dataset.devHlProp, value: b.dataset.devHlValue } : null;
|
|
3299
|
+
if (q) {
|
|
3300
|
+
const same = DEV.hl && JSON.stringify(DEV.hl) === JSON.stringify(q);
|
|
3301
|
+
DEV.hl = same ? null : q;
|
|
3302
|
+
if (DEV.shot) { drawShot(); if (DEV.hl && lbPhone()) setSheet('peek'); } else toFrame({ type: 'qawalk:dev-highlight', ...(DEV.hl || {}) });
|
|
3303
|
+
renderDev();
|
|
3304
|
+
}
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
/** Messages of the frame helper in Dev mode. */
|
|
3308
|
+
function devMessage (m) {
|
|
3309
|
+
if (m.type === 'qawalk:dev-spec') {
|
|
3310
|
+
const s = sanitizeSpec(m.spec); if (!s) return;
|
|
3311
|
+
const first = !DEV.spec;
|
|
3312
|
+
if (s.state === 'default' || !DEV.base || DEV.base.sel !== s.sel) DEV.base = s;
|
|
3313
|
+
DEV.spec = s; DEV.measure = null;
|
|
3314
|
+
if (DEV.tab !== 'el') DEV.tab = 'el';
|
|
3315
|
+
if (S.lbTab !== 'dev') showLbTab('dev');
|
|
3316
|
+
if (first && lbPhone() && S.lbSheet === 'peek') setSheet('half');
|
|
3317
|
+
renderDev();
|
|
3318
|
+
}
|
|
3319
|
+
if (m.type === 'qawalk:dev-measure') {
|
|
3320
|
+
const x = m.m;
|
|
3321
|
+
DEV.measure = x && typeof x === 'object' ? { target: { sel: sStr(x.target?.sel, 500), tag: sStr(x.target?.tag, 20), name: sStr(x.target?.name, 60) }, inside: !!x.inside, d: Object.fromEntries(['top', 'right', 'bottom', 'left'].filter((k) => x.d?.[k] != null).map((k) => [k, sNum(x.d[k])])) } : null;
|
|
3322
|
+
renderDev();
|
|
3323
|
+
}
|
|
3324
|
+
if (m.type === 'qawalk:dev-usage') { DEV.usage = sanitizeUsage(m.usage); if (DEV.tab === 'tokens') renderDev(); }
|
|
3325
|
+
if (m.type === 'qawalk:dev-highlighted' && DEV.hl) toast(`${T('tokHlN')} ${sNum(m.count)}`);
|
|
3326
|
+
if (m.type === 'qawalk:dev-asset') {
|
|
3327
|
+
if (m.error || !(m.data instanceof ArrayBuffer) || m.data.byteLength > 20 * 1024 * 1024) { toast(T('assetFail')); return; }
|
|
3328
|
+
// an octet-stream blob downloads and never renders on this origin (an SVG could carry script)
|
|
3329
|
+
const url = URL.createObjectURL(new Blob([m.data], { type: 'application/octet-stream' }));
|
|
3330
|
+
const a = document.createElement('a'); a.href = url; a.download = sStr(m.name, 80).replace(/[^\w.\- ]+/g, '-') || 'asset'; document.body.appendChild(a); a.click(); a.remove();
|
|
3331
|
+
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
/** Keys of the step detail in Dev mode; true when handled. */
|
|
3335
|
+
function devKey (key, shift) {
|
|
3336
|
+
if (key === 'd' || key === 'D') { if (DEV.on || devAvailable(devCapOf())) { setDev(!DEV.on); return true; } return false; }
|
|
3337
|
+
if (!DEV.on) return false;
|
|
3338
|
+
if (key === 'm' || key === 'M') { devMeasureMode(!DEV.measureMode); return true; }
|
|
3339
|
+
if (key === 'Escape') { devEsc(); return true; }
|
|
3340
|
+
if (shift && /^Arrow(Up|Down|Left|Right)$/.test(key)) { devNav(DEV.spec ? { ArrowUp: 'parent', ArrowDown: 'child', ArrowLeft: 'prev', ArrowRight: 'next' }[key] : 'start'); return true; }
|
|
3341
|
+
return false;
|
|
2887
3342
|
}
|
|
2888
3343
|
function stepStatusHtml (n) {
|
|
2889
3344
|
if (!isDraft()) return '';
|
|
@@ -2905,7 +3360,7 @@ function ptOpen (id, vpName, extra = {}) {
|
|
|
2905
3360
|
const list = ptList(); if (!list.length) return;
|
|
2906
3361
|
const n = list.includes(byId(id)) ? byId(id) : list[0];
|
|
2907
3362
|
if ($('pt').hidden) { PT.from = $('lb').hidden ? null : S.open; PT.device = innerWidth > 700; }
|
|
2908
|
-
$('lb').hidden = true; LIVE.mode = null;
|
|
3363
|
+
$('lb').hidden = true; LIVE.mode = null; DEV.on = false; devReset(); if (S.lbTab === 'dev') S.lbTab = 'overview';
|
|
2909
3364
|
PT.id = n.id; PT.query = extra.query || ''; PT.hash = extra.hash || ''; PT.path = extra.path || '';
|
|
2910
3365
|
PT.vp = viewports().some((v) => v.name === vpName) ? vpName : PT.vp || S.lbVp || (S.vp === 'mobile' ? mVp() : dVp())?.name || viewports()[0].name;
|
|
2911
3366
|
renderPt();
|
|
@@ -3148,7 +3603,7 @@ function rvRender () {
|
|
|
3148
3603
|
const refSrc = rvRefSrc(base, refPath);
|
|
3149
3604
|
const view = refPath ? (RV.view || (it.reasons.includes('changed') && !lbPhone() ? 'side' : 'this')) : 'this';
|
|
3150
3605
|
const shotHtml = n.kind === 'stack'
|
|
3151
|
-
? `<div class="rp__decision"><div class="seg-label">${ICON.stack} ${esc(n.stack?.count || 0)} ${
|
|
3606
|
+
? `<div class="rp__decision"><div class="seg-label">${ICON.stack} ${esc(n.stack?.count || 0)} ${pagesN(n.stack?.count || 0)}</div><div class="stack-fan" style="max-width:520px;height:220px;margin:12px auto">${(n.stack?.sample || []).slice(0, 3).map((x) => x.thumb ? `<img src="${esc(asset(x.thumb))}" alt="" style="height:180px">` : '<span class="ph" style="height:180px"></span>').join('')}</div><button class="btn btn--primary" id="rvStack">${T('openIndex')}</button></div>`
|
|
3152
3607
|
: cap ? frameImg(cap, v, `<img src="${esc(asset(cap.image || cap.png))}" alt="${esc(loc(n.title))}">`, 'rvImg') : `<p class="lb__empty">${T('notReached')}</p>`;
|
|
3153
3608
|
const refHtml = refSrc ? `<div class="lb__img ${v.mobile ? 'lb__img--m' : ''}" style="max-width:${v.mobile ? v.w + 16 : v.w}px"><img src="${esc(refSrc)}" alt="${esc(T('rvAccepted'))}"></div>` : `<p class="note">${esc(base === null ? T('rvRefFailed') : T('rvRefLoading'))}</p>`;
|
|
3154
3609
|
const refCap = ref ? `<b>${esc(T('rvViewRef'))}</b> ${fmtDate(ref.started_at)}${ref.accepted_by ? ` · ${esc(ref.accepted_by)}` : ''}` : '';
|
|
@@ -3434,10 +3889,11 @@ load().then((data) => {
|
|
|
3434
3889
|
if (!$('lb').hidden) {
|
|
3435
3890
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
3436
3891
|
if (S.focus) fxWake();
|
|
3892
|
+
if (!(e.key === 'Escape' && $('lbMore')?.classList.contains('is-open')) && devKey(e.key, e.shiftKey)) { e.preventDefault(); return; }
|
|
3437
3893
|
if (e.key === 'Escape') { if ($('lbMore')?.classList.contains('is-open')) closeLbMore(); else if (S.focus) setFocus(false); else closeLb(); }
|
|
3438
3894
|
if (e.key === 'f' || e.key === 'F') { e.preventDefault(); setFocus(!S.focus); }
|
|
3439
|
-
if (e.key === 'ArrowRight') $('lbNext')?.click();
|
|
3440
|
-
if (e.key === 'ArrowLeft') $('lbPrev')?.click();
|
|
3895
|
+
if (e.key === 'ArrowRight' && !e.shiftKey) $('lbNext')?.click();
|
|
3896
|
+
if (e.key === 'ArrowLeft' && !e.shiftKey) $('lbPrev')?.click();
|
|
3441
3897
|
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); focusComposer(); }
|
|
3442
3898
|
if (e.key === 'p' || e.key === 'P') { e.preventDefault(); ptOpen(S.open, S.lbVp); }
|
|
3443
3899
|
return;
|