kaloko 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -11
- package/package.json +1 -1
- package/schemas/scenario.v1.json +14 -4
- package/skills/kaloko/SKILL.md +10 -4
- package/skills/kaloko/reference/acceptance.md +21 -0
- package/skills/kaloko/reference/collaboration.md +2 -2
- package/skills/kaloko/reference/design.md +31 -1
- package/skills/kaloko/reference/evaluation.md +2 -3
- package/skills/kaloko/reference/portable-html.md +92 -0
- package/src/cli.mjs +52 -4
- package/src/lib/access.mjs +7 -0
- package/src/lib/build.mjs +4 -1
- package/src/lib/capture.mjs +52 -0
- package/src/lib/config.mjs +8 -2
- package/src/lib/design-export.mjs +162 -0
- package/src/lib/design-lint.mjs +189 -0
- package/src/lib/design.mjs +7 -0
- package/src/lib/evaluate.mjs +1 -1
- package/src/lib/linkcheck.mjs +149 -0
- package/src/lib/recheck.mjs +1 -1
- package/src/lib/run.mjs +1 -1
- package/src/lib/zip.mjs +58 -0
- package/viewer/index.html +146 -20
package/viewer/index.html
CHANGED
|
@@ -77,7 +77,7 @@ svg.i{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.7;stro
|
|
|
77
77
|
|
|
78
78
|
@media (min-width:821px){.bar{flex-wrap:nowrap}.picker{flex:0 1 auto;min-width:0}.task{min-width:0}.task b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar > *{flex-shrink:0}.bar > .picker{flex:0 1 auto;min-width:0;max-width:clamp(200px,30vw,640px);overflow:hidden}.bar > .spacer{flex:0 0 0}.task span{display:block;overflow:hidden;text-overflow:ellipsis}}
|
|
79
79
|
@media (max-width:1680px){.bar .ctl .seg-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}.bar{gap:8px 12px}.picker kbd{display:none}}
|
|
80
|
-
@media (max-width:1400px) and (min-width:1101px){.bar{gap:6px 8px}#viewBtn span[data-t]{display:none}#viewBtn{padding:5px 8px}.bar > .picker{max-width:clamp(140px,18vw,400px)}#rpBtn span{display:none}#rpBtn{padding:5px 8px}#cmpSel{max-width:130px}.appnav a{padding:4px 6px}.brand small{display:none}.seg button{padding:3px 8px}.bar .btn,.seg button,.appnav{font-size:12px}}
|
|
80
|
+
@media (max-width:1400px) and (min-width:1101px){.bar{gap:6px 8px}#viewBtn span[data-t]{display:none}#viewBtn{padding:5px 8px}.bar > .picker{max-width:clamp(140px,18vw,400px)}#rpBtn span,#ptBtn span{display:none}#rpBtn,#ptBtn{padding:5px 8px}#cmpSel{max-width:130px}.appnav a{padding:4px 6px}.brand small{display:none}.seg button{padding:3px 8px}.bar .btn,.seg button,.appnav{font-size:12px}}
|
|
81
81
|
.appnav{display:flex;align-items:center;gap:4px;font-size:13px}
|
|
82
82
|
.appnav a{padding:4px 9px;border-radius:6px;text-decoration:none;color:var(--muted)}
|
|
83
83
|
.appnav a:hover{background:var(--panel-2);color:var(--ink)}
|
|
@@ -236,9 +236,9 @@ table.runs{border-collapse:collapse;width:100%;font-size:12.5px}
|
|
|
236
236
|
@media (prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}
|
|
237
237
|
body.embed .helpwrap{display:none}
|
|
238
238
|
.bar-break{display:none}
|
|
239
|
-
@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,#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}}
|
|
239
|
+
@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}}
|
|
240
240
|
.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}
|
|
241
|
-
@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{display:none}.picker kbd{display:none}.bar > .picker{max-width:clamp(140px,40vw,420px)}}
|
|
241
|
+
@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)}}
|
|
242
242
|
/* canvas */
|
|
243
243
|
.viewport{position:relative;overflow:hidden;cursor:grab;touch-action:none;
|
|
244
244
|
background-color:var(--ground);background-image:radial-gradient(var(--dots) 1px,transparent 1.2px);background-size:22px 22px}
|
|
@@ -534,6 +534,26 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
534
534
|
.lb__stage{min-width:0}.lb__live{margin:0 auto;background:#fff;border:1px solid var(--line);border-radius:6px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.08)}
|
|
535
535
|
.lb__live iframe{display:block;border:0;background:#fff}
|
|
536
536
|
.lb__cmp .lb__live{margin:0}
|
|
537
|
+
/* prototype presentation: full screen, one step at a time, click-through in the live page */
|
|
538
|
+
.pt{position:fixed;inset:0;z-index:30;display:grid;grid-template-rows:auto 1fr auto;color-scheme:dark;--panel:#1f1f1d;--panel-2:#262624;--ink:#f0f0ec;--muted:#9d9d96;--line:#35352f;background:#111110;color:var(--ink)}
|
|
539
|
+
.pt__bar{display:flex;align-items:center;gap:8px 12px;flex-wrap:wrap;padding:8px 14px;background:#161615;border-bottom:1px solid var(--line)}
|
|
540
|
+
.pt__bar--foot{border-bottom:0;border-top:1px solid var(--line);justify-content:center}
|
|
541
|
+
.pt__bar .spacer{flex:1}.pt__title{font-weight:600;font-size:15px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40vw}
|
|
542
|
+
.pt__stage{position:relative;overflow:hidden;display:grid;place-items:center;min-height:0;padding:16px}
|
|
543
|
+
.pt__fit{position:relative}
|
|
544
|
+
.pt__dev{position:absolute;left:0;top:0;transform-origin:0 0;background:#000;box-shadow:0 20px 60px rgba(0,0,0,.5)}
|
|
545
|
+
.pt__dev--phone{border-radius:44px;padding:14px;border:1px solid #3a3a36}
|
|
546
|
+
.pt__dev--phone .pt__screen{border-radius:32px}
|
|
547
|
+
.pt__dev--desk{border-radius:10px;padding:30px 0 0;border:1px solid #3a3a36}
|
|
548
|
+
.pt__dev--desk::before{content:"";position:absolute;left:14px;top:11px;width:44px;height:8px;background:radial-gradient(circle at 4px 4px,#e4572e 3.5px,transparent 4px),radial-gradient(circle at 22px 4px,#f0a83c 3.5px,transparent 4px),radial-gradient(circle at 40px 4px,#52c784 3.5px,transparent 4px)}
|
|
549
|
+
.pt__dev--bare{padding:0;border-radius:0;border:0}.pt__dev--bare::before{display:none}.pt__dev--bare .pt__screen{border-radius:0}
|
|
550
|
+
.pt__screen{overflow:auto;background:#fff}
|
|
551
|
+
.pt__screen iframe{display:block;border:0;background:#fff}
|
|
552
|
+
.pt__screen img{display:block;width:100%;height:auto}
|
|
553
|
+
.pt__empty{color:var(--muted);text-align:center;padding:40px}
|
|
554
|
+
.pt__bar .note{color:var(--muted)}
|
|
555
|
+
@media (max-width:700px){.pt__title{max-width:60vw}.pt__bar{padding:6px 10px}.pt__stage{padding:6px}}
|
|
556
|
+
@media (prefers-reduced-motion:no-preference){.pt__dev{transition:transform .15s ease-out}}
|
|
537
557
|
.insp{font:12px/1.4 var(--mono)}
|
|
538
558
|
.insp table{width:100%;border-collapse:collapse}.insp td{padding:3px 4px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
539
559
|
.insp td:first-child{color:var(--muted);white-space:nowrap}
|
|
@@ -571,6 +591,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
571
591
|
<kbd>K</kbd>
|
|
572
592
|
</button>
|
|
573
593
|
<button class="btn" id="rpBtn" title="R"><svg class="i" viewBox="0 0 24 24"><path d="M7 5v14l11-7z"/></svg><span data-t="replay"></span></button>
|
|
594
|
+
<button class="btn" id="ptBtn" title="P" hidden><svg class="i" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="13" rx="1.5"/><path d="M8 21h8M12 17v4"/></svg><span data-t="prototype"></span></button>
|
|
574
595
|
<div class="dbar" id="dbar" hidden></div>
|
|
575
596
|
<div class="spacer"></div>
|
|
576
597
|
<div class="seg" id="modeSeg" aria-label="Layout"></div>
|
|
@@ -637,6 +658,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
637
658
|
</div>
|
|
638
659
|
</div>
|
|
639
660
|
<div class="rp" id="rp" hidden role="dialog" aria-modal="true" aria-label="Replay"></div>
|
|
661
|
+
<div class="pt" id="pt" hidden role="dialog" aria-modal="true" aria-label="Prototype"></div>
|
|
640
662
|
<div class="toast" id="toast" hidden></div>
|
|
641
663
|
<script>
|
|
642
664
|
/* Kaloko viewer. Data source:
|
|
@@ -646,16 +668,16 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
646
668
|
const EMBEDDED = /*__QAWALK_RUN__*/null;
|
|
647
669
|
if (!window.QAWALK_APP) { const el = document.getElementById('qawalk-app'); if (el) { try { window.QAWALK_APP = JSON.parse(el.textContent); } catch {} } }
|
|
648
670
|
const I18N = {
|
|
649
|
-
cs: { makePublicVersion:'Zveřejnit verzi', draft:'Koncept', liveShared:'živě, sdíleno', publishHint:'
|
|
671
|
+
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', hint:'Táhněte pro posun · Ctrl + kolečko zoom · <span class="kbd">F</span> režim soustředění · <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ý',
|
|
650
672
|
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',
|
|
651
673
|
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ý',
|
|
652
674
|
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í',
|
|
653
|
-
jevNote:'Evaluátor dostal
|
|
675
|
+
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`,
|
|
654
676
|
howAuto:'Automatická kontrola', howAi:'Vyhodnocení AI', howManual:'Rozhoduje člověk', howTech:'Technický detail',
|
|
655
|
-
aiPass:(p, t) => `
|
|
677
|
+
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.`,
|
|
656
678
|
aiNotEval:'Zatím nevyhodnoceno.', aiVia:{ qawalk:'vyhodnotilo Kaloko', 'own-key':'vyhodnoceno vaším klíčem' }, aiAsked:'Otázka',
|
|
657
679
|
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}`,
|
|
658
|
-
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',
|
|
680
|
+
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í',
|
|
659
681
|
a_perf_lcp:(mx) => `Největší obsah se zobrazí do ${mx} ms`, a_perf_cls:'Rozvržení se při načítání nehýbe', a_perf_ttfb:(mx) => `Server začne odpovídat do ${mx} ms`, a_perf_weight:'Stránka je dost lehká na rychlé načtení',
|
|
660
682
|
a_a11y:'Pravidlo přístupnosti', a_design:'Odpovídá referenci designu', a_uses_tokens:'Barvy, písma a velikosti pocházejí z tokenů', a_app:'Pravidlo aplikace', files:'souborů', storage:'Úložiště', copyLink:'Kopírovat odkaz', linkCopied:'Odkaz zkopírován', noPlaceToast:'Toto kritérium nemá místo na stránce ani v metadatech; důkaz je v textu kritéria.', noMeta:'Tento běh je starší a metadata stránky nesbíral. Spusťte nový běh.', noBoxToast:'Prvek se na tomto snímku nenašel.', onPage:'na stránce', inMeta:'v metadatech', inConsole:'v konzoli', noPlace:'bez místa na stránce', clickHl:'Klikněte na kritérium: zvýrazní ho na stránce nebo v metadatech', invalidJson:'neplatný JSON', attachments:'Přílohy kroku', tech:'Technické údaje',
|
|
661
683
|
execution:'Provedení kroku', step:'Krok', of:'z', hover:'Klikněte na kritérium: zvýrazní ho na stránce nebo v metadatech', fullPage:'celá stránka',
|
|
@@ -665,21 +687,21 @@ const I18N = {
|
|
|
665
687
|
allAbove:'Všechny otázky nad prahem', review:'k revizi', notEvaluated:'nehodnoceno', search:'Hledat scénář, task, cestu…', all:'Vše', tasks:'Úkoly', flows:'Flow',
|
|
666
688
|
runs:'běhů', last:'poslední', time:'Čas', result:'Výsledek', noRuns:'Žádný běh neodpovídá filtrům.', noScenario:'Žádný scénář neodpovídá hledání.',
|
|
667
689
|
onlyLocal:'Tento běh je jen lokálně; otevřete jeho složku.', linksExpired:'Odkazy na snímky vypršely. Obnovte stránku.', captured:'Zachyceno', console:'Konzole', errors:'chyb', blocked:'blokovaných požadavků', files:'Soubory',
|
|
668
|
-
pagesWord:'stránek', firstScreen:'první obrazovka', layout:'Rozložení', uploadedBy:'Nahrál', addNote:'Přidat poznámku', viewOptions:'Zobrazení', keyComment:'komentář ke kroku (v detailu)', keyHelp:'tato nápověda', helpTitle:'Zkratky a nápověda', flowComments:'Komentáře k flow', runComments:'Komentáře k běhu', commentsTitle:'Komentáře', allOpen:'Otevřené', openWord:'otevřených', flowCommentsLead:'Platí pro celé flow napříč běhy: chybějící krok, obecná připomínka, nápad.', noStepComments:'Ke kroku zatím nikdo nic nenapsal.', noComments:'Zatím žádné komentáře.', showResolved:'Vyřešené', hideResolved:'Skrýt vyřešené', replyAct:'Odpovědět', reopenAct:'Znovu otevřít', editAct:'Upravit', deleteAct:'Smazat', saveAct:'Uložit', cancel:'Zrušit', edited:'upraveno', commentDeleted:'Komentář byl smazán.', viaAgent:'Napsal agent', showPin:'Ukázat na snímku', pinOnShot:'Připnout na snímek', pinClickShot:'Klikněte do snímku', pinnedOnShot:'Připnuto na snímek', replyPh:'Odpověď… (@jméno upozorní)', flowPh:'Komentář k celému flow… (@jméno upozorní)', runPh:'Komentář k celému běhu… (@jméno upozorní)', commentLabel:'Komentář', ctrlEnter:'Ctrl+Enter odešle', confirmDelete:'Smazat komentář?', deletedToast:'Smazáno', openCommentsN:'Nevyřízené komentáře', showAllN:'Zobrazit všech %N%', screenWord:'obrazovka aplikace', uiTree:'UI strom', platformWord:'Platforma', treeHint:'Klikněte na prvek: ukáže ho na snímku.', treeSearch:'Hledat v UI stromu…', treeOcr:'Text rozpoznaný ze snímku (OCR): bez rolí a ovládacích prvků.', treeLoadFail:'UI strom se nepodařilo načíst.', treeEmpty:'Strom je prázdný.', mapView:'Mapa', indexView:'Index', searchSteps:'Hledat v krocích, kritériích a stránkách…', criteriaCol:'Kritéria', notesCol:'Poznámky', noStepsMatch:'Žádný krok neodpovídá filtru.', riKeys:'/ hledat · ↑↓ vybrat · Enter otevřít · I mapa', consoleWord:'chyb v konzoli', loadingPages:'hledám i ve stránkách stacků…', sortTitle:'Název', runWord:'Běh', openIndex:'Otevřít index', pagesNeedAttention:'stránek chce pozornost', allPagesPass:'všechny stránky prošly', stackLoadFail:'Index stránek se nepodařilo načíst', semanticSample:'sémantický vzorek', downloadCsv:'Stáhnout CSV', allWord:'Vše', criterionFilter:'
|
|
690
|
+
pagesWord:'stránek', firstScreen:'první obrazovka', layout:'Rozložení', uploadedBy:'Nahrál', addNote:'Přidat poznámku', viewOptions:'Zobrazení', keyComment:'komentář ke kroku (v detailu)', keyHelp:'tato nápověda', helpTitle:'Zkratky a nápověda', flowComments:'Komentáře k flow', runComments:'Komentáře k běhu', commentsTitle:'Komentáře', allOpen:'Otevřené', openWord:'otevřených', flowCommentsLead:'Platí pro celé flow napříč běhy: chybějící krok, obecná připomínka, nápad.', noStepComments:'Ke kroku zatím nikdo nic nenapsal.', noComments:'Zatím žádné komentáře.', showResolved:'Vyřešené', hideResolved:'Skrýt vyřešené', replyAct:'Odpovědět', reopenAct:'Znovu otevřít', editAct:'Upravit', deleteAct:'Smazat', saveAct:'Uložit', cancel:'Zrušit', edited:'upraveno', commentDeleted:'Komentář byl smazán.', viaAgent:'Napsal agent', showPin:'Ukázat na snímku', pinOnShot:'Připnout na snímek', pinClickShot:'Klikněte do snímku', pinnedOnShot:'Připnuto na snímek', replyPh:'Odpověď… (@jméno upozorní)', flowPh:'Komentář k celému flow… (@jméno upozorní)', runPh:'Komentář k celému běhu… (@jméno upozorní)', commentLabel:'Komentář', ctrlEnter:'Ctrl+Enter odešle', confirmDelete:'Smazat komentář?', deletedToast:'Smazáno', openCommentsN:'Nevyřízené komentáře', showAllN:'Zobrazit všech %N%', screenWord:'obrazovka aplikace', uiTree:'UI strom', platformWord:'Platforma', treeHint:'Klikněte na prvek: ukáže ho na snímku.', treeSearch:'Hledat v UI stromu…', treeOcr:'Text rozpoznaný ze snímku (OCR): bez rolí a ovládacích prvků.', treeLoadFail:'UI strom se nepodařilo načíst.', treeEmpty:'Strom je prázdný.', mapView:'Mapa', indexView:'Index', searchSteps:'Hledat v krocích, kritériích a stránkách…', criteriaCol:'Kritéria', notesCol:'Poznámky', noStepsMatch:'Žádný krok neodpovídá filtru.', riKeys:'/ hledat · ↑↓ vybrat · Enter otevřít · I mapa', consoleWord:'chyb v konzoli', loadingPages:'hledám i ve stránkách stacků…', sortTitle:'Název', runWord:'Běh', openIndex:'Otevřít index', pagesNeedAttention:'stránek chce pozornost', allPagesPass:'všechny stránky prošly', stackLoadFail:'Index stránek se nepodařilo načíst', semanticSample:'sémantický vzorek', downloadCsv:'Stáhnout CSV', allWord:'Vše', criterionFilter:'Nesplněné kritérium', anyCriterion:'Jakékoli kritérium', searchPages:'Hledat v titulku, URL a polích…', sortBy:'Řadit podle', sortOrder:'Pořadí', sortStatus:'Stav (nejhorší první)', sortDir:'Směr řazení', pageWord:'Stránka', statusWord:'Stav', failingCriteria:'Neprošlo', stackKeys:'/ hledat · ↑↓ vybrat · Enter otevřít · Esc zavřít', listView:'Seznam', gridView:'Mřížka', noPagesMatch:'Filtru neodpovídá žádná stránka.', backToIndex:'Zpět na index',
|
|
669
691
|
subject:'Předmět', from:'Od', to:'Komu', received:'Přijato', mailBeforeRun:'přišel před začátkem běhu', noCapture:'Bez snímku', viewEval:'eval.json', decision:'rozhodnutí', mainLane:'Hlavní průchod',
|
|
670
|
-
downloadZip:'Stáhnout ZIP', 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:'
|
|
692
|
+
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é.', 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',
|
|
671
693
|
compare:'Porovnat', 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í',
|
|
672
694
|
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' },
|
|
673
|
-
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:
|
|
695
|
+
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', hint:'Drag to pan · Ctrl + wheel to zoom · <span class="kbd">F</span> focus mode · <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',
|
|
674
696
|
pass:'Passed', partial:'Partial', fail:'Failed', skip:'Not checked', crop:'crop', html:'HTML snapshot', email:'e-mail', external:'third party',
|
|
675
697
|
branch:'Branch', commit:'Commit', author:'Author', pr:'PR', issue:'Issue', env:'Environment', duration:'Duration', noPr:'no PR', open:'open', merged:'merged', closed:'closed',
|
|
676
698
|
older:'Older run', newer:'Newer run', purpose:'Screen purpose from the scenario', criteria:'Acceptance criteria', jevTitle:'Semantic evaluation',
|
|
677
|
-
jevNote:'The evaluator
|
|
699
|
+
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`,
|
|
678
700
|
howAuto:'Automatic check', howAi:'AI evaluation', howManual:'Decided by a person', howTech:'Technical detail',
|
|
679
701
|
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.`,
|
|
680
702
|
aiNotEval:'Not evaluated yet.', aiVia:{ qawalk:'evaluated by Kaloko', 'own-key':'evaluated with your key' }, aiAsked:'Asked',
|
|
681
703
|
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}`,
|
|
682
|
-
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',
|
|
704
|
+
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',
|
|
683
705
|
a_perf_lcp:(mx) => `Largest content appears within ${mx} ms`, a_perf_cls:'Layout stays stable while loading', a_perf_ttfb:(mx) => `The server starts answering within ${mx} ms`, a_perf_weight:'The page is light enough to load quickly',
|
|
684
706
|
a_a11y:'Accessibility rule', a_design:'Matches the design reference', a_uses_tokens:'Colors, fonts and sizes come from the token set', a_app:'App rule', files:'files', storage:'Storage', copyLink:'Copy link', linkCopied:'Link copied', noPlaceToast:'This criterion has no location on the page or in the metadata; the evidence is in the criterion text.', noMeta:'This run is older and did not collect page metadata. Start a new run.', noBoxToast:'The element was not found in this capture.', onPage:'on the page', inMeta:'in metadata', inConsole:'in the console', noPlace:'no location on the page', clickHl:'Click a criterion to highlight it on the page or in the metadata', invalidJson:'invalid JSON', attachments:'Step attachments', tech:'Technical details',
|
|
685
707
|
execution:'Step execution', step:'Step', of:'of', hover:'Click a criterion to highlight it on the page or in the metadata', fullPage:'full page',
|
|
@@ -691,7 +713,7 @@ const I18N = {
|
|
|
691
713
|
onlyLocal:'This run exists locally only; open its folder.', linksExpired:'Links to the captures expired. Reload the page.', captured:'Captured', console:'Console', errors:'errors', blocked:'blocked requests', files:'Files',
|
|
692
714
|
pagesWord:'pages', firstScreen:'first screen', layout:'Layout', uploadedBy:'Uploaded by', addNote:'Add a note', viewOptions:'View', keyComment:'comment on the step (in the detail)', keyHelp:'this help', helpTitle:'Shortcuts and help', flowComments:'Flow comments', runComments:'Run comments', commentsTitle:'Comments', allOpen:'Open', openWord:'open', flowCommentsLead:'For the whole flow across runs: a missing step, a general remark, an idea.', noStepComments:'No comments on this step yet.', noComments:'No comments yet.', showResolved:'Resolved', hideResolved:'Hide resolved', replyAct:'Reply', reopenAct:'Reopen', editAct:'Edit', deleteAct:'Delete', saveAct:'Save', cancel:'Cancel', edited:'edited', commentDeleted:'This comment was deleted.', viaAgent:'Written by an agent', showPin:'Show on the screenshot', pinOnShot:'Pin on the screenshot', pinClickShot:'Click on the screenshot', pinnedOnShot:'Pinned on the screenshot', replyPh:'Reply… (@name notifies)', flowPh:'Comment on the whole flow… (@name notifies)', runPh:'Comment on the whole run… (@name notifies)', commentLabel:'Comment', ctrlEnter:'Ctrl+Enter sends', confirmDelete:'Delete this comment?', deletedToast:'Deleted', openCommentsN:'Open comments', showAllN:'Show all %N%', screenWord:'app screen', uiTree:'UI tree', platformWord:'Platform', treeHint:'Click an element to show it on the screen.', treeSearch:'Search the UI tree…', treeOcr:'Text recognized from the screenshot (OCR): no roles or controls.', treeLoadFail:'The UI tree could not be loaded.', treeEmpty:'The tree is empty.', mapView:'Map', indexView:'Index', searchSteps:'Search steps, criteria and pages…', criteriaCol:'Criteria', notesCol:'Notes', noStepsMatch:'No step matches the filter.', riKeys:'/ search · ↑↓ select · Enter open · I map', consoleWord:'console errors', loadingPages:'also searching the pages of stacks…', sortTitle:'Title', runWord:'Run', openIndex:'Open index', pagesNeedAttention:'pages need attention', allPagesPass:'all pages pass', stackLoadFail:'The page index could not be loaded', semanticSample:'semantic sample', downloadCsv:'Download CSV', allWord:'All', criterionFilter:'Failing criterion', anyCriterion:'Any criterion', searchPages:'Search title, URL and fields…', sortBy:'Sort by', sortOrder:'Order', sortStatus:'Status (worst first)', sortDir:'Sort direction', pageWord:'Page', statusWord:'Status', failingCriteria:'Failing', stackKeys:'/ search · ↑↓ select · Enter open · Esc close', listView:'List', gridView:'Grid', noPagesMatch:'No page matches the filter.', backToIndex:'Back to index',
|
|
693
715
|
subject:'Subject', from:'From', to:'To', received:'Received', mailBeforeRun:'arrived before the run started', noCapture:'No capture', viewEval:'eval.json', decision:'decision', mainLane:'Main path',
|
|
694
|
-
downloadZip:'Download ZIP', filesWord:'files', publicAccess:'public link', orgAccess:'access after signing in to the organization', projectAll:'All projects', packNote:'Criterion from a check pack (packs in the scenario)', coverage:'Plan coverage', coverageLine:'criteria from ACCEPTANCE.md have a check', coverageExtra:'checks outside the plan', coverageFull:'Every planned criterion has a check.', coverageSteps:'Steps in the plan without a node', flakyTitle:'Flaky criteria', flaky:'flaky', flakyNote:'The verdict differs between runs on the same commit and environment.', people:'People in the flow', peopleHint:'Click a person to highlight their steps; dashed arrows are hand-offs between people.', awaiting:'Awaiting approval from', approvedBy:'approved by', approvedByAll:'Every required approver has approved.', publicRun:'Public run', publicNote:'The canvas is readable without sign-in; reviewer names are hidden.', makePublic:'Make public', makePrivate:'Make private', publicHint:'Only for scenarios without personal data', publicConfirm:'Make this run public? Anyone with the link can read it without signing in. Use it only for scenarios without personal data.', openInApp:'Open in Kaloko', signInLink:'Sign in', replay:'Replay', rpStep:'Step', rpNext:'Next', rpPrev:'Back', rpEnd:'End of the walkthrough.', rpRestart:'Start over', rpChoose:'Which way?', rpOr:'or', rpKeys:'← → step · digits pick a branch · Esc closes', rpDecision:'Decision', rpBranch:'branch', rpAltOnly:'other branch',
|
|
716
|
+
downloadZip:'Download ZIP', downloadZipNote:'Every file in open formats and the canvas, which opens offline from disk', filesWord:'files', publicAccess:'public link', orgAccess:'access after signing in to the organization', projectAll:'All projects', packNote:'Criterion from a check pack (packs in the scenario)', coverage:'Plan coverage', coverageLine:'criteria from ACCEPTANCE.md have a check', coverageExtra:'checks outside the plan', coverageFull:'Every planned criterion has a check.', coverageSteps:'Steps in the plan without a node', flakyTitle:'Flaky criteria', flaky:'flaky', flakyNote:'The verdict differs between runs on the same commit and environment.', people:'People in the flow', peopleHint:'Click a person to highlight their steps; dashed arrows are hand-offs between people.', awaiting:'Awaiting approval from', approvedBy:'approved by', approvedByAll:'Every required approver has approved.', publicRun:'Public run', publicNote:'The canvas is readable without sign-in; reviewer names are hidden.', makePublic:'Make public', makePrivate:'Make private', publicHint:'Only for scenarios without personal data', publicConfirm:'Make this run public? Anyone with the link can read it without signing in. Use it only for scenarios without personal data.', openInApp:'Open in Kaloko', signInLink:'Sign in', replay:'Replay', rpStep:'Step', rpNext:'Next', rpPrev:'Back', rpEnd:'End of the walkthrough.', rpRestart:'Start over', rpChoose:'Which way?', rpOr:'or', rpKeys:'← → step · digits pick a branch · Esc closes', rpDecision:'Decision', rpBranch:'branch', rpAltOnly:'other branch',
|
|
695
717
|
compare:'Compare', 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',
|
|
696
718
|
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' }
|
|
697
719
|
};
|
|
@@ -1821,7 +1843,7 @@ function renderChrome () {
|
|
|
1821
1843
|
const app = window.QAWALK_APP;
|
|
1822
1844
|
$('brand').href = app ? (app.user ? `/${app.org}/` : '/') : '#';
|
|
1823
1845
|
$('appNav').innerHTML = (app && app.user ? `<a href="/${esc(app.org)}/projects">${UI === 'cs' ? 'Projekty' : 'Projects'}</a><a href="/${esc(app.org)}/">${UI === 'cs' ? 'Scénáře' : 'Scenarios'}</a><a href="/${esc(app.org)}/runs" aria-current="page">${UI === 'cs' ? 'Běhy' : 'Runs'}</a>` : app ? (app.embed ? `<a href="${esc(location.pathname)}" target="_blank" rel="noopener">${T('openInApp')} ↗</a>` : `<a href="/login">${T('signInLink')}</a>`) : '')
|
|
1824
|
-
+ `<span class="langsw">${['
|
|
1846
|
+
+ `<span class="langsw">${['en', 'cs'].map((l) => `<button type="button" data-lang="${l}" aria-pressed="${l === UI}">${l.toUpperCase()}</button>`).join('')}</span>`;
|
|
1825
1847
|
$('appNav').querySelectorAll('[data-lang]').forEach((b) => { b.onclick = () => { if (b.dataset.lang !== UI) switchLang(b.dataset.lang); }; });
|
|
1826
1848
|
document.querySelectorAll('[data-t]').forEach((el) => { el.textContent = T(el.dataset.t); });
|
|
1827
1849
|
$('helpBtn').title = `${T('helpTitle')} (?)`; $('helpBtn').setAttribute('aria-label', T('helpTitle'));
|
|
@@ -2083,7 +2105,7 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2083
2105
|
const lb = $('lb');
|
|
2084
2106
|
const html = `<div class="lb__box">
|
|
2085
2107
|
<div class="lb__stage">
|
|
2086
|
-
<div class="lb__tools"><div class="seg" id="lbVp"></div>${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">${S.cmp ? `${T('cmpTitle')} ${esc(cmpLabel())}` : T('hover')}</span></div>
|
|
2108
|
+
<div class="lb__tools"><div class="seg" id="lbVp"></div>${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">${S.cmp ? `${T('cmpTitle')} ${esc(cmpLabel())}` : T('hover')}</span></div>
|
|
2087
2109
|
<div class="lb__scroll" id="lbScroll">${(() => {
|
|
2088
2110
|
const bn0 = S.cmp && cmpNode(n.id), bcap0 = bn0?.captures?.[S.loc]?.[v.name];
|
|
2089
2111
|
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);
|
|
@@ -2146,6 +2168,7 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2146
2168
|
seg($('lbVp'), (n.item ? viewports().filter((x) => n.captures?.[S.loc]?.[x.name]) : viewports()).map((x) => [x.name, vpLabel(x)]), v.name, (nv) => openLb(id, nv));
|
|
2147
2169
|
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); });
|
|
2148
2170
|
if ($('lbLiveTools')) renderLiveTools();
|
|
2171
|
+
if ($('lbProto')) $('lbProto').onclick = () => ptOpen(id, v.name);
|
|
2149
2172
|
wireStepStatus(n); wireCopyCss();
|
|
2150
2173
|
// pins of comments made on elements, drawn over the screenshot too
|
|
2151
2174
|
const pimg = $('lbImg');
|
|
@@ -2233,6 +2256,10 @@ function applyHash () {
|
|
|
2233
2256
|
if (p.get('replay')) { const path = (p.get('path') || p.get('step') || '').split(',').filter((id) => byId(id)); if (!S.rp || S.rp.path.join(',') !== path.join(',')) rpOpen(path); return; }
|
|
2234
2257
|
if (S.rp) rpClose();
|
|
2235
2258
|
if (p.get('comments')) showCommentsFromHash(p.get('comments'));
|
|
2259
|
+
// a shared link straight into the prototype: #present=<step>&loc=…&vp=…
|
|
2260
|
+
const pres = p.get('present');
|
|
2261
|
+
if (pres && byId(pres)) { if (p.get('loc') && RUN.scope.locales.includes(p.get('loc')) && p.get('loc') !== S.loc) { S.loc = p.get('loc'); renderAll(); } ptOpen(pres, p.get('vp')); return; }
|
|
2262
|
+
if (!$('pt').hidden) { $('pt').hidden = true; $('pt').innerHTML = ''; }
|
|
2236
2263
|
const step = p.get('step');
|
|
2237
2264
|
if (step && step.includes('~') && !byId(step) && byId(step.split('~')[0])?.kind === 'stack') { loadStack(byId(step.split('~')[0])).then((d) => { if (d) { S.itemList = d.items.map((it) => ensureItemNode(byId(step.split('~')[0]), d, it).id); applyHash(); } }); return; }
|
|
2238
2265
|
if (!step || !byId(step)) return;
|
|
@@ -2353,7 +2380,7 @@ function renderReport () {
|
|
|
2353
2380
|
${isDesign() ? '' : `<div class="sec"><h4>${T('changes')}</h4>`}${isDesign() ? '' : pr ? (changes.length ? `<ul class="issues">${changes.map((n) => `<li><button data-go="${esc(n.id)}"><span class="badge st-${esc(n.status)}">Δ</span><span><b>${esc(loc(n.title))}</b></span><small>${esc(T(pr.statuses[n.id]))} (${esc(pr.git?.commit || '')}) → ${esc(T(n.status))} (${esc(RUN.git?.commit || '')})</small></button></li>`).join('')}</ul>` : `<p class="note">${T('noChanges')}</p>`) : `<p class="note">${T('noPrev')}</p>`}${isDesign() ? '' : '</div>'}
|
|
2354
2381
|
<div class="sec share"><h4>${ICON.share} ${T('share')}</h4>${APP()
|
|
2355
2382
|
? `<div class="url" id="shareUrl">${esc(location.origin + location.pathname)}</div>
|
|
2356
|
-
<div class="share__row"><button class="btn btn--primary" id="shareCopy">${ICON.copy} ${T('copyLink')}</button>${APP().user && APP().run ? `<a class="btn" href="${esc(`${APP().api}/orgs/${APP().org}/runs/${APP().run}/export.zip`)}" download>${ICON.download} ${T(
|
|
2383
|
+
<div class="share__row"><button class="btn btn--primary" id="shareCopy">${ICON.copy} ${T('copyLink')}</button>${APP().user && APP().run ? `<a class="btn" href="${esc(`${APP().api}/orgs/${APP().org}/runs/${APP().run}/export.zip`)}" download title="${esc(T("downloadZipNote"))}">${ICON.download} ${T("downloadZip")}</a>` : ''}</div>
|
|
2357
2384
|
<p class="note">${[RUN.expires_at && `${T('expires')} ${fmtDate(RUN.expires_at)}`, RUN.usage && `${T('storage')}: ${fmtBytes(RUN.usage.bytes)} · ${RUN.usage.files} ${T('filesWord')}`, RUN.public ? T('publicAccess') : T('orgAccess')].filter(Boolean).map(esc).join(' · ')}</p>`
|
|
2358
2385
|
: `<div class="url">kaloko share</div><p class="note">${T('shareLocal')}</p>`}</div>`;
|
|
2359
2386
|
$('rsClose').onclick = () => { S.right = false; setPanels(); };
|
|
@@ -2440,10 +2467,30 @@ const scenarioApi = (p) => { const a = APP(); return `${a.api}/orgs/${a.org}/sce
|
|
|
2440
2467
|
const branchQ = () => RUN.draft && RUN.draft.branch !== 'main' ? `?branch=${enc(RUN.draft.branch)}` : '';
|
|
2441
2468
|
function liveSrc (cap, bases = RUN.live_bases) {
|
|
2442
2469
|
const l = cap?.live; if (!l) return null;
|
|
2443
|
-
|
|
2470
|
+
// from disk: the merged tree of the run (links between steps resolve there), else the revision's own copy
|
|
2471
|
+
if (!APP()) return RUN.live_site === 'live/site/' ? RUN.live_site + l.entry : `live/${l.revision}/${l.entry}`;
|
|
2444
2472
|
const b = bases?.[l.revision];
|
|
2445
2473
|
return b ? b + l.entry : null;
|
|
2446
2474
|
}
|
|
2475
|
+
/**
|
|
2476
|
+
* A page inside a step's live folder that a prototype link named (frame.js sends `path`, e.g. "step-b/details.html"):
|
|
2477
|
+
* only inside that step's own folder, no "..", no absolute or scheme paths; null otherwise (the step's entry opens).
|
|
2478
|
+
*/
|
|
2479
|
+
function livePath (path, node) {
|
|
2480
|
+
if (typeof path !== 'string' || typeof node !== 'string' || path.length > 240 || !path.startsWith(node + '/')) return null;
|
|
2481
|
+
const parts = path.split('/');
|
|
2482
|
+
if (parts.some((x) => !x || x === '.' || x === '..' || /[\\:\u0000-\u001f]/.test(x))) return null;
|
|
2483
|
+
return parts.map(encodeURIComponent).join('/');
|
|
2484
|
+
}
|
|
2485
|
+
/** liveSrc with the step's entry replaced by `path` (from livePath), within the same revision. */
|
|
2486
|
+
function liveSubSrc (cap, path, bases) {
|
|
2487
|
+
const src = liveSrc(cap, bases); const entry = cap?.live?.entry;
|
|
2488
|
+
if (!src || !path || !entry) return src;
|
|
2489
|
+
const enc = entry.split('/').map(encodeURIComponent).join('/');
|
|
2490
|
+
if (src.endsWith(entry)) return src.slice(0, src.length - entry.length) + path;
|
|
2491
|
+
if (src.endsWith(enc)) return src.slice(0, src.length - enc.length) + path;
|
|
2492
|
+
return src;
|
|
2493
|
+
}
|
|
2447
2494
|
function viewSrc (cap, bases = RUN.live_bases) {
|
|
2448
2495
|
const l = cap?.view; if (!l?.revision) return null;
|
|
2449
2496
|
if (!APP()) return `live/${l.revision}/${l.entry}`;
|
|
@@ -2471,7 +2518,7 @@ async function reloadDraft () {
|
|
|
2471
2518
|
const m = await apiJson('GET', scenarioApi(`/draft/manifest${branchQ()}`)).catch(() => null);
|
|
2472
2519
|
if (!m) return;
|
|
2473
2520
|
swapBase(BASE, m.files_base); BASE = m.files_base || BASE; a.files = m.files_base;
|
|
2474
|
-
RUN = m; applyReviewerVerdicts(); renderAll(); if (S.open) openLb(S.open, S.lbVp, { keepFrame: true });
|
|
2521
|
+
RUN = m; applyReviewerVerdicts(); renderAll(); if (S.open && $('pt').hidden) openLb(S.open, S.lbVp, { keepFrame: true });
|
|
2475
2522
|
}
|
|
2476
2523
|
|
|
2477
2524
|
function renderDesignBar () {
|
|
@@ -2534,7 +2581,7 @@ function liveStage (cap, v, id = 'lbFrame', bases, view = false) {
|
|
|
2534
2581
|
// an HTML view gets no forms and runs only the frame helper (its origin's CSP enforces that too)
|
|
2535
2582
|
return `<div class="lb__live"><iframe id="${id}" title="${view ? T('htmlView') : T('liveMode')}" sandbox="${view ? 'allow-scripts' : 'allow-scripts allow-forms'}" referrerpolicy="no-referrer" src="${esc(src)}" style="width:${v.w}px;height:${Math.min(v.h, 2000)}px"></iframe></div>`;
|
|
2536
2583
|
}
|
|
2537
|
-
const frames = () => [...document.querySelectorAll('#lbFrame, #lbFrameB')];
|
|
2584
|
+
const frames = () => [...document.querySelectorAll('#lbFrame, #lbFrameB, #ptIframe')];
|
|
2538
2585
|
// a live step renders at its real viewport width and is scaled to fit the stage, like the screenshot
|
|
2539
2586
|
function fitLive () {
|
|
2540
2587
|
for (const f of frames()) {
|
|
@@ -2565,8 +2612,20 @@ addEventListener('message', (e) => {
|
|
|
2565
2612
|
if (!frames().some((f) => e.source === f.contentWindow)) return;
|
|
2566
2613
|
const m = e.data || {};
|
|
2567
2614
|
const n = S.open && byId(S.open);
|
|
2615
|
+
// presenting: the frame moves between steps (links, data-kaloko-go) and hands over the keys it does not use
|
|
2616
|
+
if (!$('pt').hidden) {
|
|
2617
|
+
const str = (x) => typeof x === 'string' ? x.slice(0, 300).replace(/[#\s]/g, '') : '';
|
|
2618
|
+
if (m.type === 'qawalk:go' && typeof m.node === 'string') { if (byId(m.node) && byId(m.node).kind !== 'decision') ptOpen(m.node, PT.vp, { query: str(m.query), hash: str(m.hash), path: livePath(m.path, m.node) }); else toast(T('protoNoStep')); }
|
|
2619
|
+
if (m.type === 'qawalk:key') ptKey({ key: String(m.key), preventDefault () {} });
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2568
2622
|
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 }); }
|
|
2569
|
-
if (m.type === 'qawalk:go' && typeof m.node === 'string' && byId(m.node)) {
|
|
2623
|
+
if (m.type === 'qawalk:go' && typeof m.node === 'string' && byId(m.node)) {
|
|
2624
|
+
toast(`${T('prototypeTo')} ${loc(byId(m.node).title)}`); openLb(m.node, S.lbVp);
|
|
2625
|
+
// a link to a page inside the step's folder opens that page in the live frame, not the step's entry
|
|
2626
|
+
const sub = livePath(m.path, m.node), fr = $('lbFrame'), c = byId(m.node).captures?.[S.loc]?.[S.lbVp];
|
|
2627
|
+
if (sub && fr && S.lbMode === 'live') { const u = liveSubSrc(c, sub); if (u) fr.src = u; }
|
|
2628
|
+
}
|
|
2570
2629
|
if (m.type === 'qawalk:hover' && LIVE.mode === 'inspect' && m.el) { LIVE.insp = sanitizeEl(m.el); const sec = $('inspSec'); if (sec) sec.outerHTML = inspectorHtml(); else $('lbSideTop')?.insertAdjacentHTML('afterend', inspectorHtml()); wireCopyCss(); }
|
|
2571
2630
|
if (m.type === 'qawalk:picked' && m.el) {
|
|
2572
2631
|
const el = sanitizeEl(m.el);
|
|
@@ -2626,6 +2685,69 @@ function wireStepStatus (n) {
|
|
|
2626
2685
|
}
|
|
2627
2686
|
|
|
2628
2687
|
|
|
2688
|
+
// ---- prototype presentation (#42): full screen, live pages where the run has them, screenshots otherwise ----
|
|
2689
|
+
const PT = { id: null, vp: null, device: true, query: '', hash: '', path: '', from: null };
|
|
2690
|
+
const ptList = () => flowOrder().filter((n) => n.kind !== 'decision' && n.kind !== 'stack' && !n.item);
|
|
2691
|
+
function ptOpen (id, vpName, extra = {}) {
|
|
2692
|
+
const list = ptList(); if (!list.length) return;
|
|
2693
|
+
const n = list.includes(byId(id)) ? byId(id) : list[0];
|
|
2694
|
+
if ($('pt').hidden) { PT.from = $('lb').hidden ? null : S.open; PT.device = innerWidth > 700; }
|
|
2695
|
+
$('lb').hidden = true; LIVE.mode = null;
|
|
2696
|
+
PT.id = n.id; PT.query = extra.query || ''; PT.hash = extra.hash || ''; PT.path = extra.path || '';
|
|
2697
|
+
PT.vp = viewports().some((v) => v.name === vpName) ? vpName : PT.vp || S.lbVp || (S.vp === 'mobile' ? mVp() : dVp())?.name || viewports()[0].name;
|
|
2698
|
+
renderPt();
|
|
2699
|
+
setHash({ present: n.id, loc: S.loc, vp: PT.vp });
|
|
2700
|
+
}
|
|
2701
|
+
function ptClose () {
|
|
2702
|
+
if (document.fullscreenElement) document.exitFullscreen?.().catch(() => {});
|
|
2703
|
+
$('pt').hidden = true; $('pt').innerHTML = '';
|
|
2704
|
+
if (PT.from && byId(PT.id)) openLb(PT.id, PT.vp); else setHash(null);
|
|
2705
|
+
}
|
|
2706
|
+
function ptKey (e) {
|
|
2707
|
+
const list = ptList(), i = list.indexOf(byId(PT.id));
|
|
2708
|
+
if (e.key === 'Escape') { e.preventDefault(); ptClose(); }
|
|
2709
|
+
else if ((e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') && i < list.length - 1) { e.preventDefault(); ptOpen(list[i + 1].id, PT.vp); }
|
|
2710
|
+
else if ((e.key === 'ArrowLeft' || e.key === 'PageUp') && i > 0) { e.preventDefault(); ptOpen(list[i - 1].id, PT.vp); }
|
|
2711
|
+
else if (e.key === 'Home') { e.preventDefault(); ptOpen(list[0].id, PT.vp); }
|
|
2712
|
+
else if (e.key === 'End') { e.preventDefault(); ptOpen(list[list.length - 1].id, PT.vp); }
|
|
2713
|
+
else if (e.key === 'f' || e.key === 'F') { e.preventDefault(); $('ptFull')?.click(); }
|
|
2714
|
+
else if (e.key === 'd' || e.key === 'D') { e.preventDefault(); $('ptDev')?.click(); }
|
|
2715
|
+
}
|
|
2716
|
+
function renderPt () {
|
|
2717
|
+
const el = $('pt'), list = ptList(), n = byId(PT.id), i = list.indexOf(n);
|
|
2718
|
+
const v = viewports().find((x) => x.name === PT.vp) || viewports()[0];
|
|
2719
|
+
const cap = n.captures?.[S.loc]?.[v.name];
|
|
2720
|
+
const src = PT.path ? liveSubSrc(cap, PT.path) : liveSrc(cap);
|
|
2721
|
+
const url = src ? src + (PT.query ? `?${PT.query}` : '') + (PT.hash ? `#${PT.hash}` : '') : null;
|
|
2722
|
+
const kind = v.mobile ? 'phone' : 'desk';
|
|
2723
|
+
const screen = url ? `<iframe id="ptIframe" title="${esc(loc(n.title))}" sandbox="allow-scripts allow-forms" referrerpolicy="no-referrer" src="${esc(url)}" style="width:${v.w}px;height:${v.h}px"></iframe>`
|
|
2724
|
+
: cap ? `<img src="${esc(asset(cap.image || cap.png))}" alt="${esc(loc(n.title))}">` : `<p class="pt__empty">${T('notReached')}</p>`;
|
|
2725
|
+
const note = !src ? T('protoShot') : APP() ? T('protoHint') : T('protoLocal');
|
|
2726
|
+
el.innerHTML = `<div class="pt__bar"><span class="pt__title" id="ptTitle">${esc(loc(n.title))}</span><span class="note">${i + 1} / ${list.length}</span><span class="spacer"></span><div class="seg" id="ptVp"></div>${RUN.scope.locales.length > 1 ? '<div class="seg" id="ptLoc"></div>' : ''}<button class="btn" id="ptDev" aria-pressed="${PT.device}">${T('protoDevice')}</button>${document.fullscreenEnabled ? `<button class="btn" id="ptFull" aria-pressed="${!!document.fullscreenElement}">${T('protoFull')}</button>` : ''}<button class="btn" id="ptLink">${ICON.external} ${T('copyLink')}</button><button class="icon-btn" id="ptClose" aria-label="${esc(T('protoExit'))}" title="${esc(T('protoExit'))} (Esc)">${ICON.close}</button></div>
|
|
2727
|
+
<div class="pt__stage" id="ptStage"><div class="pt__fit" id="ptFit"><div class="pt__dev pt__dev--${kind} ${PT.device ? '' : 'pt__dev--bare'}" id="ptDevBox"><div class="pt__screen" style="width:${v.w}px;height:${v.h}px">${screen}</div></div></div></div>
|
|
2728
|
+
<div class="pt__bar pt__bar--foot"><button class="icon-btn" id="ptPrev" aria-label="Previous" ${i <= 0 ? 'disabled' : ''}>${ICON.prev}</button><span class="note">${esc(note)}</span><button class="icon-btn" id="ptNext" aria-label="Next" ${i >= list.length - 1 ? 'disabled' : ''}>${ICON.next}</button></div>`;
|
|
2729
|
+
el.hidden = false;
|
|
2730
|
+
seg($('ptVp'), viewports().map((x) => [x.name, vpLabel(x)]), v.name, (nv) => ptOpen(PT.id, nv, PT));
|
|
2731
|
+
if ($('ptLoc')) seg($('ptLoc'), RUN.scope.locales.map((l) => [l, l]), S.loc, (l) => { S.loc = l; renderAll(); ptOpen(PT.id, PT.vp, PT); });
|
|
2732
|
+
$('ptDev').onclick = () => { PT.device = !PT.device; renderPt(); };
|
|
2733
|
+
if ($('ptFull')) $('ptFull').onclick = () => { (document.fullscreenElement ? document.exitFullscreen() : el.requestFullscreen()).catch(() => {}).finally(() => setTimeout(fitPt, 120)); };
|
|
2734
|
+
$('ptLink').onclick = async () => { try { await navigator.clipboard.writeText(location.href); toast(T('linkCopied')); } catch { toast(location.href); } };
|
|
2735
|
+
$('ptClose').onclick = ptClose;
|
|
2736
|
+
$('ptPrev').onclick = () => ptKey({ key: 'ArrowLeft', preventDefault () {} });
|
|
2737
|
+
$('ptNext').onclick = () => ptKey({ key: 'ArrowRight', preventDefault () {} });
|
|
2738
|
+
fitPt(); requestAnimationFrame(fitPt);
|
|
2739
|
+
$('ptClose').focus();
|
|
2740
|
+
}
|
|
2741
|
+
// the device keeps the viewport's real size and is scaled to the stage, like a live step in the detail
|
|
2742
|
+
function fitPt () {
|
|
2743
|
+
const st = $('ptStage'), box = $('ptDevBox'), fitEl = $('ptFit'); if (!st || !box) return;
|
|
2744
|
+
const w = box.offsetWidth, h = box.offsetHeight, pad = innerWidth > 700 ? 32 : 12;
|
|
2745
|
+
const k = Math.min(1, (st.clientWidth - pad) / w, (st.clientHeight - pad) / h);
|
|
2746
|
+
box.style.transform = `scale(${k})`; fitEl.style.width = `${Math.floor(w * k)}px`; fitEl.style.height = `${Math.floor(h * k)}px`;
|
|
2747
|
+
}
|
|
2748
|
+
addEventListener('resize', () => { if (!$('pt').hidden) fitPt(); });
|
|
2749
|
+
document.addEventListener('fullscreenchange', () => { const b = $('ptFull'); if (b) b.setAttribute('aria-pressed', String(!!document.fullscreenElement)); setTimeout(fitPt, 60); });
|
|
2750
|
+
|
|
2629
2751
|
/* ---------- boot ---------- */
|
|
2630
2752
|
let tt; function toast (msg) { const el = $('toast'); el.textContent = msg; el.hidden = false; clearTimeout(tt); tt = setTimeout(() => { el.hidden = true; }, 2600); }
|
|
2631
2753
|
async function load () {
|
|
@@ -2713,6 +2835,7 @@ load().then((data) => {
|
|
|
2713
2835
|
$('togLeft').onclick = () => { S.left = !S.left; setPanels(); savePanels(); };
|
|
2714
2836
|
$('togRight').onclick = () => { S.right = !S.right; setPanels(); savePanels(); };
|
|
2715
2837
|
$('rpBtn').onclick = () => rpOpen(null);
|
|
2838
|
+
$('ptBtn').hidden = !isDesign(); $('ptBtn').onclick = () => ptOpen(null);
|
|
2716
2839
|
wireComments($('lb')); wireComments($('rside')); wireComments($('rp'));
|
|
2717
2840
|
// small menus of the top bar: one open at a time, closed by a click elsewhere or Escape
|
|
2718
2841
|
// wide screens show navigation, language and view options in the bar; narrower ones fold them into ☰
|
|
@@ -2736,6 +2859,7 @@ load().then((data) => {
|
|
|
2736
2859
|
// (the draft stays); the next Escape closes the overlay. Search boxes keep their own arrow and Enter keys.
|
|
2737
2860
|
const field = e.target.closest?.('textarea,select,[contenteditable="true"],input:not([type=checkbox]):not([type=radio]):not([type=button])');
|
|
2738
2861
|
if (field && !['siQ', 'riQ', 'brSearch'].includes(field.id)) { if (e.key === 'Escape') { e.preventDefault(); field.blur(); } return; }
|
|
2862
|
+
if (!$('pt').hidden) { if (!e.metaKey && !e.ctrlKey && !e.altKey) ptKey(e); return; }
|
|
2739
2863
|
if (!$('si').hidden) { if (stackKey(e)) return; if (e.target.closest('input,select,textarea')) return; return; }
|
|
2740
2864
|
if (S.rp && !$('rp').hidden) { if (!e.target.closest('input,select,textarea')) { e.preventDefault(); rpKey(e); } return; }
|
|
2741
2865
|
if (!$('br').hidden) { if (e.key === 'Escape') closeBrowser(); return; }
|
|
@@ -2745,6 +2869,7 @@ load().then((data) => {
|
|
|
2745
2869
|
if (e.key === 'ArrowRight') $('lbNext')?.click();
|
|
2746
2870
|
if (e.key === 'ArrowLeft') $('lbPrev')?.click();
|
|
2747
2871
|
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); focusComposer(); }
|
|
2872
|
+
if (e.key === 'p' || e.key === 'P') { e.preventDefault(); ptOpen(S.open, S.lbVp); }
|
|
2748
2873
|
return;
|
|
2749
2874
|
}
|
|
2750
2875
|
if (S.mode === 'index' && !e.metaKey && !e.ctrlKey && !e.altKey) { if (S.st?.inline ? stackKey(e) : riKey(e)) return; }
|
|
@@ -2754,6 +2879,7 @@ load().then((data) => {
|
|
|
2754
2879
|
if (e.key === 'f' || e.key === 'F') { const any = S.left || S.right; S.left = S.right = !any; setPanels(); }
|
|
2755
2880
|
if (e.key === 'k' || e.key === 'K' || (e.key === '/' && S.mode !== 'index')) { e.preventDefault(); openBrowser(); }
|
|
2756
2881
|
if (e.key === 'r' || e.key === 'R') { e.preventDefault(); rpOpen(null); }
|
|
2882
|
+
if (e.key === 'p' || e.key === 'P') { e.preventDefault(); ptOpen(null); }
|
|
2757
2883
|
if (e.key === '0') fit();
|
|
2758
2884
|
if (e.key === '1') startView();
|
|
2759
2885
|
});
|