kaloko 0.4.2 → 0.4.7
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 +2 -1
- package/package.json +2 -2
- package/skills/kaloko/reference/evaluation.md +3 -1
- package/src/cli.mjs +28 -4
- package/src/lib/capture.mjs +2 -1
- package/src/lib/doctor.mjs +5 -3
- package/src/lib/linkcheck.mjs +5 -3
- package/src/lib/login.mjs +70 -0
- package/viewer/index.html +236 -52
package/README.md
CHANGED
|
@@ -122,7 +122,8 @@ captured HTML, page text and console output.
|
|
|
122
122
|
adapter module (`provider: script`, example in `templates/mail/`), or hand a
|
|
123
123
|
message over with `npx kaloko mail --step <id> --file message.eml`.
|
|
124
124
|
- **Service and evaluator**: `KALOKO_TOKEN` for `share`, `feedback`, `inbox` and clean-up (`runs`, `trash`, `prune`, `keep`, `archive`)
|
|
125
|
-
(
|
|
125
|
+
(`npx kaloko login` approves one in the browser and saves it to `~/.config/kaloko/.env`, or create it in your
|
|
126
|
+
organization's settings); `TYPESAFE_API_KEY` for the
|
|
126
127
|
semantic evaluator (optional). Both names can be changed in the config.
|
|
127
128
|
|
|
128
129
|
`npx kaloko doctor` checks that every variable the config names is set and that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kaloko",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "See what your AI agents built: every screen, language and device walked, assessed and approved on one canvas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qa",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "node --test --test-timeout=
|
|
35
|
+
"test": "node --test --test-timeout=900000 test/*.test.mjs worker/test/*.test.mjs"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"ajv": "^8.20.0",
|
|
@@ -39,7 +39,9 @@ cannot be repeated (data created, an e-mail sent once, a state that moved
|
|
|
39
39
|
on), fix the scenario and run `kaloko evaluate --recheck [--steps a,b]`: the
|
|
40
40
|
deterministic checks run again against the stored HTML with page scripts off
|
|
41
41
|
and are marked `rechecked`; console errors, overflow and performance keep
|
|
42
|
-
their live result.
|
|
42
|
+
their live result. The same command asks changed or new semantic questions
|
|
43
|
+
again on the stored captures, so a reworded semantic criterion needs no new
|
|
44
|
+
walk. When the page content itself is in question, capture again.
|
|
43
45
|
|
|
44
46
|
## Quick signals
|
|
45
47
|
|
package/src/cli.mjs
CHANGED
|
@@ -30,7 +30,8 @@ import { DEFAULTS as JEV_DEFAULTS } from './lib/jev.mjs'
|
|
|
30
30
|
import { expandPacks } from './lib/packs.mjs'
|
|
31
31
|
import { upsertPrComment } from './lib/pr.mjs'
|
|
32
32
|
import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
33
|
-
import { service } from './lib/service.mjs'
|
|
33
|
+
import { service, serviceUrl } from './lib/service.mjs'
|
|
34
|
+
import { login } from './lib/login.mjs'
|
|
34
35
|
import { walkDesign, syncDesign, publishVersion, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
35
36
|
import { designDir, readBase, headKey } from './lib/design.mjs'
|
|
36
37
|
import { lintDesign, formatLint } from './lib/design-lint.mjs'
|
|
@@ -51,6 +52,10 @@ Project
|
|
|
51
52
|
pages (title, description, H1) with the seo, a11y, perf and console packs
|
|
52
53
|
init [--agent claude|codex|all] [--org <slug>]
|
|
53
54
|
Create ${CONFIG_FILE} and install the kaloko skill for your agent
|
|
55
|
+
login [--org <slug>] [--url <service>] [--print] [--no-open]
|
|
56
|
+
Sign this machine in through the browser: approve the code on kaloko.app, choose
|
|
57
|
+
the organization and what the token may do; the token (at most 30 days) goes to
|
|
58
|
+
~/.config/kaloko/.env, or is printed with --print
|
|
54
59
|
doctor [--json] [--fix] Check Node, Chrome, config, environments, browser ports, token, evaluator key
|
|
55
60
|
and app tools; --fix asks macOS for the desktop capture permissions
|
|
56
61
|
validate <scenario.yml>... [--coverage [--source ACCEPTANCE.md]]
|
|
@@ -98,7 +103,8 @@ Run
|
|
|
98
103
|
|
|
99
104
|
Results
|
|
100
105
|
evaluate Deterministic checks + semantic evaluator (default JEV) for all captures
|
|
101
|
-
evaluate --recheck [--steps a,b] After a scenario
|
|
106
|
+
evaluate --recheck [--steps a,b] After a scenario change: re-run deterministic checks on the stored HTML and ask
|
|
107
|
+
the semantic questions again on the stored captures (no walk);
|
|
102
108
|
console, overflow and performance keep their live result
|
|
103
109
|
build Write run.json and the local preview (index.html)
|
|
104
110
|
preview Build and print the preview path
|
|
@@ -557,7 +563,7 @@ const commands = {
|
|
|
557
563
|
await cp(join(packageRoot, 'templates', 'steps', 'consent.mjs'), consentStep)
|
|
558
564
|
say('Added qa/flows/steps/consent.mjs (accepts the consent dialog, if the site has one)')
|
|
559
565
|
}
|
|
560
|
-
say(`Next:
|
|
566
|
+
say(`Next: run kaloko login to approve a token in the browser (or create one in the app under Settings → API tokens and put KALOKO_TOKEN into .env).\nScenario template: ${join(packageRoot, 'templates', 'scenario.yml')}`)
|
|
561
567
|
},
|
|
562
568
|
|
|
563
569
|
async validate (args) {
|
|
@@ -936,7 +942,7 @@ const commands = {
|
|
|
936
942
|
for (const x of report.skipped) { const [where, ...why] = x.split(': '); const k = why.join(': '); byReason.set(k, [...(byReason.get(k) || []), where]) }
|
|
937
943
|
say(`Semantic criteria not evaluated:\n - ${[...byReason].slice(0, 8).map(([why, where]) => `${where.length > 1 ? `${where.length} captures (${where.slice(0, 3).join(', ')}${where.length > 3 ? ', …' : ''})` : where[0]}: ${why}`).join('\n - ')}`)
|
|
938
944
|
}
|
|
939
|
-
if (report.outdated.length) say(`Criteria changed after capture of ${report.outdated.join(', ')}: not evaluated. Run "kaloko evaluate --recheck --steps ${report.outdated.join(',')}"
|
|
945
|
+
if (report.outdated.length) say(`Criteria changed after capture of ${report.outdated.join(', ')}: not evaluated. Run "kaloko evaluate --recheck --steps ${report.outdated.join(',')}": it rechecks the stored captures against the new criteria, deterministic and semantic, without walking again.`)
|
|
940
946
|
},
|
|
941
947
|
|
|
942
948
|
async build (args) {
|
|
@@ -1217,6 +1223,24 @@ const commands = {
|
|
|
1217
1223
|
say(args.undo ? `✓ ${args._[0]} is active again` : `✓ ${args._[0]} archived: hidden from the scenario list, its runs stay reachable (kaloko archive ${args._[0]} --undo)`)
|
|
1218
1224
|
},
|
|
1219
1225
|
|
|
1226
|
+
async login (args) {
|
|
1227
|
+
// a project is optional: it only names the service URL, the organization and the token variable
|
|
1228
|
+
let config = {}
|
|
1229
|
+
const shell = { ...process.env } // before the project and user .env files are read
|
|
1230
|
+
try { ({ config } = await context(args)) } catch (e) { if (!(e instanceof UserError)) throw e }
|
|
1231
|
+
const tokenEnv = config.share?.token_env || 'KALOKO_TOKEN'
|
|
1232
|
+
const inShell = shell[tokenEnv]
|
|
1233
|
+
const base = typeof args.url === 'string' ? args.url : serviceUrl(config)
|
|
1234
|
+
const version = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')).version
|
|
1235
|
+
const r = await login(base, { org: typeof args.org === 'string' ? args.org : config.org || null, print: !!args.print, open: !args['no-open'], tokenEnv, version, say })
|
|
1236
|
+
if (args.print) return
|
|
1237
|
+
say(`✓ Signed in to ${r.org_name || r.org} as ${r.email} (${r.scope}; expires ${String(r.expires_at || '').slice(0, 10)}). ${tokenEnv} saved to ${r.file}.`)
|
|
1238
|
+
if (config.org && r.org && config.org !== r.org) say(`! ${CONFIG_FILE} names the organization ${config.org}; the token is for ${r.org}.`)
|
|
1239
|
+
let projectEnv = ''
|
|
1240
|
+
try { projectEnv = await readFile(join(process.cwd(), '.env'), 'utf8') } catch {}
|
|
1241
|
+
if (inShell || new RegExp(`^\\s*(export\\s+)?${tokenEnv}\\s*=`, 'm').test(projectEnv)) say(`! ${tokenEnv} is also set ${inShell ? 'in your shell' : 'in this project\'s .env'}, which wins over ~/.config/kaloko/.env. Remove it there to use the new token.`)
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1220
1244
|
async inbox (args) {
|
|
1221
1245
|
const { config } = await context(args)
|
|
1222
1246
|
const client = service(config)
|
package/src/lib/capture.mjs
CHANGED
|
@@ -252,6 +252,7 @@ export async function networkChecks (page, criteria, url, { run = null, locale =
|
|
|
252
252
|
const doc = await page.evaluate((sels) => ({
|
|
253
253
|
lang: document.documentElement.getAttribute('lang') || '',
|
|
254
254
|
alternates: [...document.querySelectorAll("link[rel~='alternate'][hreflang][href]")].map((l) => ({ hreflang: l.getAttribute('hreflang').toLowerCase(), href: l.href })),
|
|
255
|
+
canonical: document.querySelector("link[rel='canonical'][href]")?.href || null,
|
|
255
256
|
links: Object.fromEntries(sels.map((sel) => [sel, (() => { try { return [...document.querySelectorAll(sel)].map((a) => a.href).filter(Boolean) } catch { return [] } })()]))
|
|
256
257
|
}), [...new Set(todo.filter((c) => c.check.assert === 'links_ok').map((c) => c.check.selector || 'a[href]'))]).catch(() => null)
|
|
257
258
|
for (const c of todo) {
|
|
@@ -259,7 +260,7 @@ export async function networkChecks (page, criteria, url, { run = null, locale =
|
|
|
259
260
|
try {
|
|
260
261
|
out[c.id] = c.check.assert === 'links_ok'
|
|
261
262
|
? await checkLinks({ links: doc.links[c.check.selector || 'a[href]'] || [], pageUrl: url, get, max: c.check.max ?? undefined, exclude: c.check.pattern || null })
|
|
262
|
-
: await checkHreflang({ alternates: doc.alternates, pageUrl: url, lang: doc.lang, get, min: c.check.min ?? 2 })
|
|
263
|
+
: await checkHreflang({ alternates: doc.alternates, pageUrl: url, canonical: doc.canonical, lang: doc.lang, get, min: c.check.min ?? 2 })
|
|
263
264
|
} catch (error) { out[c.id] = { ok: false, evidence: `check error: ${error.message}` } }
|
|
264
265
|
}
|
|
265
266
|
} finally { await request.dispose().catch(() => {}) }
|
package/src/lib/doctor.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export async function doctor ({ config, root, file = null, fetchImpl = fetch, la
|
|
|
107
107
|
const tokenEnv = config.share?.token_env || 'KALOKO_TOKEN'
|
|
108
108
|
const token = env[tokenEnv]
|
|
109
109
|
const service = serviceUrl(config, env)
|
|
110
|
-
if (!token) add('token', false, `${tokenEnv} not set`, `
|
|
110
|
+
if (!token) add('token', false, `${tokenEnv} not set`, `Run kaloko login, or create a tester token in ${service}/${config.org || '<org>'}/settings and put ${tokenEnv}=… into .env (or ~/.config/kaloko/.env).`)
|
|
111
111
|
else {
|
|
112
112
|
try {
|
|
113
113
|
const res = await fetchImpl(`${service}/api/v1/me`, { headers: { Authorization: `Bearer ${token}` } })
|
|
@@ -115,8 +115,10 @@ export async function doctor ({ config, root, file = null, fetchImpl = fetch, la
|
|
|
115
115
|
else {
|
|
116
116
|
const me = await res.json()
|
|
117
117
|
const sameOrg = !config.org || me.org === config.org
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// sharing runs needs the Developer / Tester role (or admin) and, on tokens with scope sets, the upload scope
|
|
119
|
+
const canShare = ['tester', 'admin'].includes(me.role) && (!Array.isArray(me.scopes) || me.scopes.includes('upload'))
|
|
120
|
+
add('token', sameOrg, `${me.email} · ${me.role} · org ${me.org}${canShare ? '' : ' (this token cannot share runs)'}`, sameOrg ? '' : `The token belongs to organization "${me.org}" but the config says "${config.org}".`)
|
|
121
|
+
if (!canShare) add('token role', false, Array.isArray(me.scopes) ? `${me.role} · ${me.scopes.join(', ')}` : me.role, 'kaloko share needs a Developer / Tester token with the upload scope.')
|
|
120
122
|
}
|
|
121
123
|
} catch (e) { add('token', false, `${service} unreachable: ${e.message}`, 'Check your network; KALOKO_URL overrides the service URL.') }
|
|
122
124
|
}
|
package/src/lib/linkcheck.mjs
CHANGED
|
@@ -69,16 +69,18 @@ export async function checkLinks ({ links, pageUrl, get, max = DEFAULT_MAX_LINKS
|
|
|
69
69
|
* Fails when there are fewer than `min` languages, when the page's own language does not point to the page, or
|
|
70
70
|
* when another language version does not answer 200 or does not link back to the page.
|
|
71
71
|
*/
|
|
72
|
-
export async function checkHreflang ({ alternates, pageUrl, lang, get, min = 2 }) {
|
|
72
|
+
export async function checkHreflang ({ alternates, pageUrl, canonical = null, lang, get, min = 2 }) {
|
|
73
73
|
const problems = []
|
|
74
74
|
const langs = alternates.filter((a) => a.hreflang !== 'x-default')
|
|
75
75
|
if (new Set(langs.map((a) => a.hreflang)).size < min) problems.push(`${new Set(langs.map((a) => a.hreflang)).size} hreflang language(s), expected ${min}`)
|
|
76
|
-
|
|
76
|
+
// the page's identity is its canonical address (hreflang must match it); tracking or variant parameters in the
|
|
77
|
+
// walked URL (?v=, utm_*) do not make it another page
|
|
78
|
+
const self = normUrl(canonical || pageUrl)
|
|
77
79
|
const primary = String(lang || '').toLowerCase().split('-')[0]
|
|
78
80
|
const own = langs.find((a) => a.hreflang === String(lang || '').toLowerCase()) || langs.find((a) => a.hreflang.split('-')[0] === primary)
|
|
79
81
|
if (!primary) problems.push('html lang is not set, so the page\'s own hreflang cannot be identified')
|
|
80
82
|
else if (!own) problems.push(`no hreflang for the page's own language "${lang}"`)
|
|
81
|
-
else if (normUrl(own.href) !== self) problems.push(`hreflang ${own.hreflang} points to ${own.href}, not to this page (${pageUrl})`)
|
|
83
|
+
else if (normUrl(own.href) !== self) problems.push(`hreflang ${own.hreflang} points to ${own.href}, not to this page (${canonical || pageUrl})`)
|
|
82
84
|
for (const alt of langs) {
|
|
83
85
|
if (alt === own) continue
|
|
84
86
|
if (actionLink(new URL(alt.href))) { problems.push(`${alt.hreflang} version ${alt.href} looks like an action link; not fetched`); continue }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// kaloko login: signs this machine in through the browser instead of pasting a token. The service hands out a code
|
|
2
|
+
// (POST /cli/device), a signed-in person approves it at /cli/authorize for one organization with the scopes their
|
|
3
|
+
// role allows, and the next poll (POST /cli/token) returns a personal token that expires within 30 days. The token
|
|
4
|
+
// goes to ~/.config/kaloko/.env (or is printed with --print).
|
|
5
|
+
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises'
|
|
6
|
+
import { join, dirname } from 'node:path'
|
|
7
|
+
import { homedir, hostname } from 'node:os'
|
|
8
|
+
import { spawn } from 'node:child_process'
|
|
9
|
+
import { UserError } from './util.mjs'
|
|
10
|
+
|
|
11
|
+
export const userEnvFile = (home = homedir()) => join(home, '.config', 'kaloko', '.env')
|
|
12
|
+
|
|
13
|
+
/** Sets NAME=value in a .env text, replacing an earlier line of the same name; keeps everything else. */
|
|
14
|
+
export function setEnvLine (text, name, value) {
|
|
15
|
+
const lines = String(text || '').split('\n').filter((l, i, all) => !(i === all.length - 1 && l === ''))
|
|
16
|
+
const re = new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=`)
|
|
17
|
+
const at = lines.findIndex((l) => re.test(l))
|
|
18
|
+
if (at >= 0) lines[at] = `${name}=${value}`
|
|
19
|
+
else lines.push(`${name}=${value}`)
|
|
20
|
+
return lines.join('\n') + '\n'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function openBrowser (url) {
|
|
24
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]]
|
|
25
|
+
try { spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref() } catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Runs the device flow against `base` (the service URL). Returns { token, org, email, scope, expires_at, file? }.
|
|
30
|
+
* Options: org (preselected in the browser), print (do not write the file), open (launch the browser), tokenEnv
|
|
31
|
+
* (the variable name, share.token_env), home, version, say, fetchImpl, sleep.
|
|
32
|
+
*/
|
|
33
|
+
export async function login (base, { org = null, print = false, open = true, tokenEnv = 'KALOKO_TOKEN', home = homedir(), version = '', say = console.log, fetchImpl = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
|
|
34
|
+
base = base.replace(/\/$/, '')
|
|
35
|
+
const post = async (path, body) => {
|
|
36
|
+
let res
|
|
37
|
+
try { res = await fetchImpl(`${base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body) }) } catch (e) { throw new UserError(`Cannot reach ${base}: ${e.message}`) }
|
|
38
|
+
return { status: res.status, body: await res.json().catch(() => ({})) }
|
|
39
|
+
}
|
|
40
|
+
const start = await post('/cli/device', { client: `kaloko${version ? ` ${version}` : ''} on ${hostname()}`.slice(0, 60) })
|
|
41
|
+
if (start.status !== 200 || !start.body.device_code) throw new UserError(`${base} did not start the sign-in (HTTP ${start.status}${start.body.error ? `: ${start.body.error}` : ''}). Is this a Kaloko service?`)
|
|
42
|
+
const { device_code: device, user_code: code, verification_uri_complete: complete, expires_in: ttl = 600 } = start.body
|
|
43
|
+
let interval = Math.max(1, Number(start.body.interval) || 5)
|
|
44
|
+
const link = org ? `${complete}&org=${encodeURIComponent(org)}` : complete
|
|
45
|
+
say(`Open this page and approve the sign-in:\n ${link}\nCheck that it shows the code ${code}.`)
|
|
46
|
+
if (open) openBrowser(link)
|
|
47
|
+
const until = Date.now() + ttl * 1000
|
|
48
|
+
while (Date.now() < until) {
|
|
49
|
+
await sleep(interval * 1000)
|
|
50
|
+
const r = await post('/cli/token', { device_code: device })
|
|
51
|
+
if (r.status === 200 && r.body.access_token) {
|
|
52
|
+
const out = { token: r.body.access_token, org: r.body.org, org_name: r.body.org_name, email: r.body.email, scope: r.body.scope, expires_at: r.body.expires_at }
|
|
53
|
+
if (print) { say(`${tokenEnv}=${out.token}`); return out }
|
|
54
|
+
const file = userEnvFile(home)
|
|
55
|
+
await mkdir(dirname(file), { recursive: true })
|
|
56
|
+
let text = ''
|
|
57
|
+
try { text = await readFile(file, 'utf8') } catch {}
|
|
58
|
+
await writeFile(file, setEnvLine(text, tokenEnv, out.token), { mode: 0o600 })
|
|
59
|
+
await chmod(file, 0o600).catch(() => {})
|
|
60
|
+
return { ...out, file }
|
|
61
|
+
}
|
|
62
|
+
const error = r.body.error
|
|
63
|
+
if (error === 'authorization_pending') continue
|
|
64
|
+
if (error === 'slow_down') { interval += 5; continue }
|
|
65
|
+
if (error === 'access_denied') throw new UserError('The sign-in was declined in the browser.')
|
|
66
|
+
if (error === 'expired_token') throw new UserError('The code expired. Run kaloko login again.')
|
|
67
|
+
throw new UserError(`Sign-in failed (HTTP ${r.status}${error ? `: ${error}` : ''}).`)
|
|
68
|
+
}
|
|
69
|
+
throw new UserError('The code expired before it was approved. Run kaloko login again.')
|
|
70
|
+
}
|
package/viewer/index.html
CHANGED
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
<meta name="robots" content="noindex">
|
|
7
7
|
<title>Kaloko</title>
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
10
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
|
10
11
|
<style>
|
|
12
|
+
@font-face{font-family:"Instrument Sans Fallback";src:local("Arial");font-weight:400;size-adjust:99.9%;ascent-override:97%;descent-override:25%;line-gap-override:0%}
|
|
13
|
+
@font-face{font-family:"Instrument Sans Fallback";src:local("Arial Bold"),local("Arial");font-weight:600;size-adjust:96.9%;ascent-override:100%;descent-override:26%;line-gap-override:0%}
|
|
14
|
+
@font-face{font-family:"Instrument Sans Fallback";src:local("Arial Bold"),local("Arial");font-weight:700;size-adjust:98.1%;ascent-override:99%;descent-override:25%;line-gap-override:0%}
|
|
11
15
|
:root{
|
|
12
16
|
--ground:#ecece8; --dots:#c9c9c3; --panel:#ffffff; --panel-2:#f5f5f2;
|
|
13
17
|
--ink:#151514; --muted:#6a6a64; --line:#d7d7d1; --line-strong:#151514;
|
|
14
18
|
--accent:#ffe452; --accent-ink:#151514;
|
|
15
19
|
--pass:#1d8249; --fail:#c8102e; --partial:#b86e00; --skip:#8d8d86;
|
|
16
20
|
--pass-bg:#e3f3e9; --fail-bg:#fbe5e8; --partial-bg:#fbf0dc; --skip-bg:#ececea;
|
|
21
|
+
--pass-ink:#176f3b; --fail-ink:#b40f2a; --partial-ink:#8f5500; --skip-ink:#5f5f59;
|
|
17
22
|
--edge:#55554f; --shadow:0 1px 2px rgba(20,20,18,.08),0 8px 24px rgba(20,20,18,.08);
|
|
18
|
-
--sans:"Instrument Sans","Helvetica Neue",-apple-system,"Segoe UI",Roboto,Arial,sans-serif;
|
|
23
|
+
--sans:"Instrument Sans","Instrument Sans Fallback","Helvetica Neue",-apple-system,"Segoe UI",Roboto,Arial,sans-serif;
|
|
19
24
|
--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
20
25
|
}
|
|
21
26
|
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
|
|
@@ -24,6 +29,7 @@
|
|
|
24
29
|
--ink:#f0f0ec; --muted:#9d9d96; --line:#35352f; --line-strong:#f0f0ec;
|
|
25
30
|
--pass:#52c784; --fail:#ff6070; --partial:#f0a83c; --skip:#8d8d86;
|
|
26
31
|
--pass-bg:#18311f; --fail-bg:#3a1a1f; --partial-bg:#372a14; --skip-bg:#2a2a27;
|
|
32
|
+
--pass-ink:#52c784; --fail-ink:#ff6070; --partial-ink:#f0a83c; --skip-ink:#a9a9a2;
|
|
27
33
|
--edge:#8a8a83; --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.35);
|
|
28
34
|
}}
|
|
29
35
|
:root[data-theme="dark"]{
|
|
@@ -32,6 +38,7 @@
|
|
|
32
38
|
--ink:#f0f0ec; --muted:#9d9d96; --line:#35352f; --line-strong:#f0f0ec;
|
|
33
39
|
--pass:#52c784; --fail:#ff6070; --partial:#f0a83c; --skip:#8d8d86;
|
|
34
40
|
--pass-bg:#18311f; --fail-bg:#3a1a1f; --partial-bg:#372a14; --skip-bg:#2a2a27;
|
|
41
|
+
--pass-ink:#52c784; --fail-ink:#ff6070; --partial-ink:#f0a83c; --skip-ink:#a9a9a2;
|
|
35
42
|
--edge:#8a8a83; --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.35);
|
|
36
43
|
}
|
|
37
44
|
html,body{height:100%}
|
|
@@ -53,7 +60,7 @@ button:focus-visible,a:focus-visible,select:focus-visible{outline:2px solid var(
|
|
|
53
60
|
.pop{left:auto;right:0;min-width:280px;gap:12px;padding:12px}.pop .ctl{display:grid;gap:4px}.pop .seg-label{position:static!important;width:auto!important;height:auto!important;clip:auto!important;overflow:visible!important}.pop select{width:100%}
|
|
54
61
|
.inbox-btn{display:inline-flex;align-items:center;gap:4px;font-size:13px;font-weight:600;color:inherit;text-decoration:none;padding:4px 8px;border-radius:999px}.inbox-btn:hover{background:var(--panel-2)}.inbox-btn b{background:var(--partial);color:#151514;border-radius:999px;padding:0 6px;font-size:11px}
|
|
55
62
|
#viewBtn{position:relative}.dot-on{position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:var(--accent,#ffe452);box-shadow:0 0 0 2px var(--panel)}
|
|
56
|
-
.pmenu{left:auto;right:0}.pmenu button{all:unset;cursor:pointer;padding:6px 8px;border-radius:6px;font-size:13px}.pmenu button:hover{background:var(--panel-2)}.pmenu-btn{all:unset;cursor:pointer;font-size:12px;color:var(--muted);padding:2px 6px;border-radius:999px;border:1px solid var(--line)}
|
|
63
|
+
.pmenu{left:auto;right:0}.pmenu button{all:unset;cursor:pointer;padding:6px 8px;border-radius:6px;font-size:13px}.pmenu button:hover{background:var(--panel-2)}.pmenu-btn{all:unset;cursor:pointer;font-size:12px;color:var(--muted);padding:2px 6px;border-radius:999px;border:1px solid var(--line);min-width:24px;min-height:24px;box-sizing:border-box;text-align:center}
|
|
57
64
|
.helppop{min-width:320px;font-size:13px}.helppop .kbd{margin-right:4px}.helppop ul{margin:0;padding:0 0 0 2px;list-style:none;display:grid;gap:4px}
|
|
58
65
|
.bar{display:flex;flex-wrap:wrap;align-items:center;gap:10px 18px;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--line-strong);position:relative;z-index:5}
|
|
59
66
|
.brand{display:flex;align-items:baseline;gap:10px;font-weight:700;letter-spacing:-.01em;font-size:15px}
|
|
@@ -61,6 +68,8 @@ button:focus-visible,a:focus-visible,select:focus-visible{outline:2px solid var(
|
|
|
61
68
|
.task{display:flex;flex-direction:column;min-width:0}
|
|
62
69
|
.task b{font-size:14px;font-weight:600}
|
|
63
70
|
.task span{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
71
|
+
/* the bar keeps its height while the run loads: empty title lines and switches hold their place (no layout shift) */
|
|
72
|
+
.task b:empty::before,.task span:empty::before{content:"\00a0"}.bar .seg:empty{min-height:29px}
|
|
64
73
|
.spacer{flex:1}
|
|
65
74
|
.seg{display:inline-flex;border:1px solid var(--line);border-radius:999px;padding:2px;background:var(--panel-2)}
|
|
66
75
|
.seg button{padding:3px 11px;border-radius:999px;font-size:12.5px;font-weight:500;color:var(--muted)}
|
|
@@ -116,12 +125,12 @@ svg.i{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.7;stro
|
|
|
116
125
|
.av{width:20px;height:20px;border-radius:50%;display:inline-grid;place-items:center;font-size:9.5px;font-weight:700;background:var(--panel-2);border:1px solid var(--line);flex:none}
|
|
117
126
|
a.pr{color:var(--ink);text-decoration:underline;text-decoration-color:var(--line);text-underline-offset:2px}
|
|
118
127
|
a.pr:hover{text-decoration-color:var(--ink)}
|
|
119
|
-
.prstate{font-size:10.5px;font-weight:600;padding:0 5px;border-radius:4px;background:var(--pass-bg);color:var(--pass);margin-left:4px}
|
|
128
|
+
.prstate{font-size:10.5px;font-weight:600;padding:0 5px;border-radius:4px;background:var(--pass-bg);color:var(--pass-ink);margin-left:4px}
|
|
120
129
|
.prstate--merged{background:var(--skip-bg);color:var(--muted)}
|
|
121
130
|
.run__env{font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:1px 6px;border-radius:4px;background:var(--skip-bg);color:var(--muted);white-space:nowrap}
|
|
122
|
-
.run__env--staging{background:var(--partial-bg);color:var(--partial)}
|
|
123
|
-
.run__env--production{background:var(--fail-bg);color:var(--fail)}
|
|
124
|
-
.run__env--review{background:var(--pass-bg);color:var(--pass)}
|
|
131
|
+
.run__env--staging{background:var(--partial-bg);color:var(--partial-ink)}
|
|
132
|
+
.run__env--production{background:var(--fail-bg);color:var(--fail-ink)}
|
|
133
|
+
.run__env--review{background:var(--pass-bg);color:var(--pass-ink)}
|
|
125
134
|
.bar5{display:flex;height:5px;border-radius:3px;overflow:hidden;background:var(--skip-bg);min-width:60px}
|
|
126
135
|
.bar5 i{display:block}
|
|
127
136
|
.steps{list-style:none;margin:0;padding:0 8px 16px}
|
|
@@ -166,6 +175,8 @@ table.matrix{border-collapse:collapse;width:100%;font-size:12.5px}
|
|
|
166
175
|
select,input[type="search"]{font:inherit;font-size:12.5px;padding:4px 8px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink)}
|
|
167
176
|
input[type="search"]:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-color:var(--ink)}
|
|
168
177
|
.note{font-size:12px;color:var(--muted);margin:0}
|
|
178
|
+
.btn--primary .note{color:#4a4a45}
|
|
179
|
+
.cmpchip{display:inline-flex;align-items:center;gap:4px;font-size:12px;padding:2px 2px 2px 10px;border-radius:999px;background:var(--panel-2);border:1px solid var(--line);white-space:nowrap;max-width:280px;overflow:hidden}.cmpchip>span{overflow:hidden;text-overflow:ellipsis}.cmpchip .icon-btn{width:24px;height:24px}
|
|
169
180
|
.note code{font-family:var(--mono);font-size:11px}
|
|
170
181
|
|
|
171
182
|
/* semantic evaluation on node */
|
|
@@ -180,7 +191,7 @@ input[type="search"]:focus-visible,select:focus-visible{outline:2px solid var(--
|
|
|
180
191
|
.jev__c b{font-family:var(--mono);font-weight:500;font-size:10.5px;color:var(--muted)}
|
|
181
192
|
.jev__c .ok{color:var(--muted)}
|
|
182
193
|
.jev pre{margin:0;border-top:1px solid var(--line);padding:7px 8px;font-family:var(--mono);font-size:10.5px;line-height:1.45;max-height:190px;overflow:auto;white-space:pre;background:var(--panel);cursor:text;width:338px}
|
|
183
|
-
.json .k{color:var(--muted)}.json .s{color:var(--pass)}.json .n{color:var(--partial)}
|
|
194
|
+
.json .k{color:var(--muted)}.json .s{color:var(--pass-ink)}.json .n{color:var(--partial-ink)}
|
|
184
195
|
|
|
185
196
|
/* browser: scenarios and runs */
|
|
186
197
|
.br{position:fixed;inset:0;z-index:25;background:rgba(12,12,11,.5);display:flex;justify-content:center;align-items:flex-start;padding:clamp(0px,6vh,64px) clamp(0px,3vw,32px)}
|
|
@@ -254,6 +265,8 @@ body.embed .helpwrap{display:none}
|
|
|
254
265
|
.lane-head{position:absolute;left:16px;top:-30px;font-size:14px;font-weight:700;color:var(--ink);white-space:nowrap;display:flex;gap:10px;align-items:baseline}
|
|
255
266
|
.lane-head small{font-size:12px;font-weight:500;color:var(--muted)}
|
|
256
267
|
.world.far .jev,.world.far .node__foot{visibility:hidden}
|
|
268
|
+
/* comparing runs: the pixel difference stays readable when the whole flow is in view */
|
|
269
|
+
.world.far .node__foot .pxd{visibility:visible;font-size:calc(10.5px * min(var(--inv,1), 2.4))}
|
|
257
270
|
/* thumbnails stay while they are still recognisable; below that they would only be a few pixels to decode and paint */
|
|
258
271
|
.world.tiny .shots img{visibility:hidden}
|
|
259
272
|
.world.far .node{outline:6px solid transparent}.world.far .node.st-pass{outline-color:color-mix(in srgb,var(--pass) 55%,transparent)}.world.far .node.st-fail{outline-color:color-mix(in srgb,var(--fail) 55%,transparent)}.world.far .node.st-partial{outline-color:color-mix(in srgb,var(--partial) 55%,transparent)}
|
|
@@ -261,7 +274,7 @@ body.embed .helpwrap{display:none}
|
|
|
261
274
|
.world.far .node.st-pass{background:color-mix(in srgb,var(--pass) 22%,var(--panel))}.world.far .node.st-fail{background:color-mix(in srgb,var(--fail) 28%,var(--panel))}.world.far .node.st-partial{background:color-mix(in srgb,var(--partial) 28%,var(--panel))}
|
|
262
275
|
.world.far .node__title{font-size:calc(12px * min(var(--inv,1), 2.4));line-height:1.1}
|
|
263
276
|
.steps h3[data-lane]{cursor:pointer}
|
|
264
|
-
.mail-old{color:var(--fail);font-weight:600;font-size:12px}
|
|
277
|
+
.mail-old{color:var(--fail-ink);font-weight:600;font-size:12px}
|
|
265
278
|
.presence{display:flex;align-items:center;gap:4px;margin-left:6px}
|
|
266
279
|
.presence .pav{width:26px;height:26px;border-radius:50%;border:2px solid var(--panel);background:var(--c);color:#fff;font-size:11px;font-weight:700;display:grid;place-items:center;cursor:pointer;padding:0;margin-left:-6px;box-shadow:0 0 0 1px var(--line)}
|
|
267
280
|
.presence .pav[aria-pressed="true"]{box-shadow:0 0 0 2px var(--ink)}
|
|
@@ -307,7 +320,7 @@ body.embed .helpwrap{display:none}
|
|
|
307
320
|
.si-row:hover,.si-row.is-sel{background:var(--panel-2)}.si-row img,.si-card img,.si-row .ph,.si-card .ph{width:96px;height:58px;object-fit:cover;object-position:top;border:1px solid var(--line);border-radius:3px;background:var(--panel-2)}
|
|
308
321
|
.si-row .t{min-width:0}.si-row .t b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.si-row .t small{display:block;color:var(--muted);font-family:var(--mono,monospace);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
309
322
|
.si-row .f{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}.si-row .f.num{font-family:var(--mono,monospace);text-align:right}
|
|
310
|
-
.si-fails{display:flex;gap:4px;flex-wrap:wrap}.si-fails span{font-size:11px;font-weight:700;border-radius:4px;padding:1px 5px;background:color-mix(in srgb,var(--fail) 16%,transparent);color:var(--fail)}.si-fails i{font-style:normal;font-weight:500;opacity:.75;margin-left:4px}.si-fails span.rv{background:color-mix(in srgb,var(--partial) 16%,transparent);color:var(--partial)}
|
|
323
|
+
.si-fails{display:flex;gap:4px;flex-wrap:wrap}.si-fails span{font-size:11px;font-weight:700;border-radius:4px;padding:1px 5px;background:color-mix(in srgb,var(--fail) 16%,transparent);color:var(--fail-ink)}.si-fails i{font-style:normal;font-weight:500;opacity:.75;margin-left:4px}.si-fails span.rv{background:color-mix(in srgb,var(--partial) 16%,transparent);color:var(--partial-ink)}
|
|
311
324
|
.si-headrow{display:grid;grid-template-columns:104px minmax(140px,2fr) 96px var(--si-cols,repeat(3,minmax(48px,1fr))) minmax(80px,1fr);gap:10px;padding:6px 18px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);border-bottom:1px solid var(--line);background:var(--panel-2)}.si-headrow button{all:unset;cursor:pointer}.si-headrow button.num{text-align:right}.si-headrow button[aria-sort]{color:var(--ink);font-weight:700}
|
|
312
325
|
.si-card{position:absolute;width:220px;height:198px;border:1px solid var(--line);border-radius:6px;background:var(--panel);padding:8px;text-align:left;font:inherit;color:var(--ink);cursor:pointer;display:grid;grid-template-rows:auto auto auto;gap:4px}
|
|
313
326
|
.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}
|
|
@@ -322,10 +335,10 @@ body.embed .helpwrap{display:none}
|
|
|
322
335
|
.http{font-family:var(--mono);font-size:10.5px;font-weight:600;padding:1px 5px;border-radius:4px;background:var(--ink);color:var(--panel);margin-right:5px;vertical-align:1px}.http--5{background:var(--fail)}
|
|
323
336
|
.node__path{font-family:var(--mono);font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
324
337
|
.badge{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;font-weight:600;padding:2px 7px;border-radius:999px;white-space:nowrap;font-variant-numeric:tabular-nums;flex:none}
|
|
325
|
-
.badge.st-pass{background:var(--pass-bg);color:var(--pass)}
|
|
326
|
-
.badge.st-fail{background:var(--fail-bg);color:var(--fail)}
|
|
327
|
-
.badge.st-partial{background:var(--partial-bg);color:var(--partial)}
|
|
328
|
-
.badge.st-skip{background:var(--skip-bg);color:var(--skip)}
|
|
338
|
+
.badge.st-pass{background:var(--pass-bg);color:var(--pass-ink)}
|
|
339
|
+
.badge.st-fail{background:var(--fail-bg);color:var(--fail-ink)}
|
|
340
|
+
.badge.st-partial{background:var(--partial-bg);color:var(--partial-ink)}
|
|
341
|
+
.badge.st-skip{background:var(--skip-bg);color:var(--skip-ink)}
|
|
329
342
|
/* the status stripe is the card's own top border, so it follows the rounded corners */
|
|
330
343
|
.node::before{content:"";position:absolute;inset:-1px;border-radius:inherit;border-top:3px solid var(--skip);pointer-events:none}
|
|
331
344
|
.node.st-pass::before{border-top-color:var(--pass)}.node.st-fail::before{border-top-color:var(--fail)}.node.st-partial::before{border-top-color:var(--partial)}
|
|
@@ -336,15 +349,15 @@ body.embed .helpwrap{display:none}
|
|
|
336
349
|
/* phones only: the phone screenshot is the card, large enough to read and to keep titles on few lines */
|
|
337
350
|
.shots--mbig .shot--m{width:180px;height:390px;border-radius:14px}.node:has(.shots--mbig){min-width:220px}
|
|
338
351
|
.shot__in{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none}
|
|
339
|
-
.shot__tag{position:absolute;right:4px;bottom:4px;font-size:9.5px;font-family:var(--mono);background
|
|
352
|
+
.shot__tag{position:absolute;right:4px;bottom:4px;font-size:9.5px;font-family:var(--mono);background:#151514;color:#fff;padding:0 4px;border-radius:3px}
|
|
340
353
|
.shots:hover .shot{border-color:var(--ink)}
|
|
341
354
|
.node__foot{display:flex;align-items:center;gap:8px;padding:8px 10px 9px;color:var(--muted);font-size:11.5px}
|
|
342
355
|
.att{display:inline-flex;align-items:center;gap:3px}
|
|
343
356
|
.delta{margin-left:auto;font-size:11px;font-weight:600;padding:1px 6px;border-radius:4px;background:var(--accent);color:var(--accent-ink)}
|
|
344
357
|
.loc-dots{display:inline-flex;gap:3px;align-items:center}
|
|
345
358
|
.loc-dots span{font-family:var(--mono);font-size:10px;padding:0 3px;border-radius:3px;border:1px solid var(--line)}
|
|
346
|
-
.loc-dots span.st-fail{border-color:var(--fail);color:var(--fail)}
|
|
347
|
-
.loc-dots span.st-partial{border-color:var(--partial);color:var(--partial)}
|
|
359
|
+
.loc-dots span.st-fail{border-color:var(--fail);color:var(--fail-ink)}
|
|
360
|
+
.loc-dots span.st-partial{border-color:var(--partial);color:var(--partial-ink)}
|
|
348
361
|
|
|
349
362
|
.node--decision{display:flex;align-items:center;gap:10px;padding:12px 14px;width:210px;border-style:dashed;border-color:var(--edge);background:var(--panel-2);box-shadow:none}
|
|
350
363
|
.node--decision::before{display:none}
|
|
@@ -375,13 +388,13 @@ body.embed .helpwrap{display:none}
|
|
|
375
388
|
.lb__nav{display:flex;gap:6px;margin-left:auto}
|
|
376
389
|
.sec{padding:14px 18px;border-bottom:1px solid var(--line)}
|
|
377
390
|
.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}
|
|
378
|
-
.purpose{margin:0;max-width:
|
|
379
|
-
.sigb{font-size:11px;font-weight:700;color:var(--partial);background:var(--partial-bg);border-radius:4px;padding:0 5px}
|
|
380
|
-
.sigs{list-style:none;margin:0;padding:0;display:grid;gap:6px;font-size:12.5px}.sigs li{display:grid;grid-template-columns:1fr 90px 64px;gap:8px;align-items:center}.sigs li.bad{color:var(--partial);font-weight:600}.gauge--sig{background:var(--skip-bg)}
|
|
391
|
+
.purpose{margin:0;max-width:41.3em}
|
|
392
|
+
.sigb{font-size:11px;font-weight:700;color:var(--partial-ink);background:var(--partial-bg);border-radius:4px;padding:0 5px}
|
|
393
|
+
.sigs{list-style:none;margin:0;padding:0;display:grid;gap:6px;font-size:12.5px}.sigs li{display:grid;grid-template-columns:1fr 90px 64px;gap:8px;align-items:center}.sigs li.bad{color:var(--partial-ink);font-weight:600}.gauge--sig{background:var(--skip-bg)}
|
|
381
394
|
.fixhelp{grid-column:2;font-size:12.5px;margin-top:4px;padding:6px 8px;border-radius:6px;background:var(--panel-2)}.fixhelp p{margin:0 0 3px}.fixhelp p:last-child{margin:0}details.fixhelp{background:none;padding:0}details.fixhelp summary{cursor:pointer;color:var(--muted);font-size:11.5px}
|
|
382
395
|
.ev--text{display:block;grid-column:2;font-size:12.5px;padding:4px 8px;border-radius:6px;background:var(--panel-2)}
|
|
383
396
|
.lbtabs{display:flex;gap:2px;padding:6px 16px 0;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--panel);z-index:2}.lbtabs button{padding:8px 10px;font-size:12.5px;font-weight:600;color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px}.lbtabs button[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--ink)}
|
|
384
|
-
.account{font-size:11px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--muted);margin-left:6px}.st-fail-t{color:var(--fail)}
|
|
397
|
+
.account{font-size:11px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--muted);margin-left:6px}.st-fail-t{color:var(--fail-ink)}
|
|
385
398
|
.ac li .acbody{grid-column:2;display:grid;gap:4px}.where--go{justify-self:start;text-decoration:underline;color:var(--ink)}.ac li.st-pass:not(.is-on) .acbody{display:none}.ac li.st-pass{padding:5px 8px}.ac li.st-pass:not(.is-on){cursor:pointer}
|
|
386
399
|
.clog{list-style:none;margin:6px 0 0;padding:0;display:grid;gap:6px;max-height:420px;overflow:auto}.clog li{position:relative;background:#151514;color:#e8e8e2;border-radius:6px}.clog pre{margin:0;padding:8px 34px 8px 10px;font:12px/1.5 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;max-height:220px;overflow:auto}
|
|
387
400
|
.clog__copy{position:absolute;top:4px;right:4px;width:24px;height:24px;display:grid;place-items:center;border-radius:4px;color:#bdbdb6;opacity:.7}.clog__copy:hover{opacity:1;background:rgba(255,255,255,.1)}.clog__all{float:right;font-size:11.5px;padding:2px 8px;text-transform:none;letter-spacing:0}
|
|
@@ -395,10 +408,10 @@ body.embed .helpwrap{display:none}
|
|
|
395
408
|
.ac li{display:grid;grid-template-columns:22px 1fr;gap:2px 8px;padding:8px;border-radius:6px;border:1px solid var(--line)}
|
|
396
409
|
.ac li:hover{border-color:var(--ink)}
|
|
397
410
|
.ac .mark{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:12px;font-weight:700}
|
|
398
|
-
.ac .st-pass .mark,.ac li.st-pass .mark{background:var(--pass-bg);color:var(--pass)}
|
|
399
|
-
.ac li.st-fail .mark{background:var(--fail-bg);color:var(--fail)}
|
|
400
|
-
.ac li.st-partial .mark{background:var(--partial-bg);color:var(--partial)}
|
|
401
|
-
.ac li.st-skip .mark{background:var(--skip-bg);color:var(--skip)}
|
|
411
|
+
.ac .st-pass .mark,.ac li.st-pass .mark{background:var(--pass-bg);color:var(--pass-ink)}
|
|
412
|
+
.ac li.st-fail .mark{background:var(--fail-bg);color:var(--fail-ink)}
|
|
413
|
+
.ac li.st-partial .mark{background:var(--partial-bg);color:var(--partial-ink)}
|
|
414
|
+
.ac li.st-skip .mark{background:var(--skip-bg);color:var(--skip-ink)}
|
|
402
415
|
.ac .id{font-family:var(--mono);font-size:11px;color:var(--muted);margin-right:4px}
|
|
403
416
|
.ac .ev{grid-column:2;font-family:var(--mono);font-size:11px;color:var(--muted);background:var(--panel-2);padding:4px 6px;border-radius:4px;overflow-wrap:anywhere}
|
|
404
417
|
.files{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px}
|
|
@@ -407,7 +420,7 @@ body.embed .helpwrap{display:none}
|
|
|
407
420
|
.file__prev{height:74px;background:var(--panel-2);position:relative;overflow:hidden;display:grid;place-items:center;color:var(--muted)}
|
|
408
421
|
.file__name{font-size:11.5px;padding:5px 7px;display:flex;gap:5px;align-items:center;line-height:1.25}
|
|
409
422
|
pre.json{margin:0;font-family:var(--mono);font-size:11.5px;line-height:1.5;background:var(--panel-2);border-radius:6px;padding:10px;overflow:auto;max-height:280px;white-space:pre}
|
|
410
|
-
.json .k{color:var(--muted)}.json .s{color:var(--pass)}.json .n{color:var(--partial)}
|
|
423
|
+
.json .k{color:var(--muted)}.json .s{color:var(--pass-ink)}.json .n{color:var(--partial-ink)}
|
|
411
424
|
details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
412
425
|
.kv{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:12.5px;margin:0}
|
|
413
426
|
.kv dt{color:var(--muted)}.kv dd{margin:0;font-family:var(--mono);font-size:11.5px;overflow-wrap:anywhere}
|
|
@@ -448,9 +461,9 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
448
461
|
.md tr.flash td{animation:mdflash 1.6s ease-out}
|
|
449
462
|
@keyframes mdflash{0%{background:rgba(255,228,82,.7)}100%{background:transparent}}
|
|
450
463
|
.md tr.st-fail td{background:var(--fail-bg)}
|
|
451
|
-
.md tr.st-pass td:first-child{color:var(--pass)}
|
|
464
|
+
.md tr.st-pass td:first-child{color:var(--pass-ink)}
|
|
452
465
|
.md .acb{display:inline-block;font-family:var(--mono);font-size:10px;padding:0 4px;border-radius:3px;margin-left:6px;vertical-align:1px}
|
|
453
|
-
.md .acb.st-pass{background:var(--pass-bg);color:var(--pass)}.md .acb.st-fail{background:var(--fail-bg);color:var(--fail)}.md .acb.st-partial{background:var(--partial-bg);color:var(--partial)}
|
|
466
|
+
.md .acb.st-pass{background:var(--pass-bg);color:var(--pass-ink)}.md .acb.st-fail{background:var(--fail-bg);color:var(--fail-ink)}.md .acb.st-partial{background:var(--partial-bg);color:var(--partial-ink)}
|
|
454
467
|
.md pre{margin:0;padding:8px 10px;border-top:1px solid var(--line);font-family:var(--mono);font-size:11px;max-height:260px;overflow:auto;background:var(--panel-2)}
|
|
455
468
|
.md .h{display:block;font-size:12px;padding:3px 10px;border-top:1px solid var(--line)}
|
|
456
469
|
.md .h i{font-style:normal;font-family:var(--mono);color:var(--muted);font-size:10.5px;margin-right:6px}
|
|
@@ -463,8 +476,8 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
463
476
|
/* compare mode */
|
|
464
477
|
#cmpSel{max-width:220px}
|
|
465
478
|
.pxd{font-family:var(--mono);font-size:10.5px;font-weight:500;padding:1px 5px;border-radius:4px;background:var(--skip-bg);color:var(--muted);white-space:nowrap}
|
|
466
|
-
.pxd.lv1{background:var(--partial-bg);color:var(--partial)}.pxd.lv2{background:var(--fail-bg);color:var(--fail)}.pxd.lv0{background:var(--pass-bg);color:var(--pass)}
|
|
467
|
-
.cmpd{font-size:11px;font-weight:600;padding:1px 6px;border-radius:4px;background:var(--fail-bg);color:var(--fail)}
|
|
479
|
+
.pxd.lv1{background:var(--partial-bg);color:var(--partial-ink)}.pxd.lv2{background:var(--fail-bg);color:var(--fail-ink)}.pxd.lv0{background:var(--pass-bg);color:var(--pass-ink)}
|
|
480
|
+
.cmpd{font-size:11px;font-weight:600;padding:1px 6px;border-radius:4px;background:var(--fail-bg);color:var(--fail-ink)}
|
|
468
481
|
.lb__cmp{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
|
|
469
482
|
.lb__cmp figure{margin:0;min-width:0}
|
|
470
483
|
.lb__cmp figcaption{font-size:11.5px;color:var(--muted);margin:0 0 6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
@@ -474,7 +487,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
474
487
|
.lb__diff canvas{display:block;max-width:100%;height:auto}
|
|
475
488
|
.ac .ev--cmp{border-left:3px solid var(--partial)}
|
|
476
489
|
.ac li.cmp-changed{border-color:var(--partial)}
|
|
477
|
-
.ac .dl{font-family:var(--mono);font-size:10px;padding:0 4px;border-radius:3px;background:var(--partial-bg);color:var(--partial);margin-left:4px}
|
|
490
|
+
.ac .dl{font-family:var(--mono);font-size:10px;padding:0 4px;border-radius:3px;background:var(--partial-bg);color:var(--partial-ink);margin-left:4px}
|
|
478
491
|
@media (max-width:900px){.lb__cmp{grid-template-columns:1fr}}
|
|
479
492
|
/* sessions: people in the flow */
|
|
480
493
|
.sess{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;padding:1px 7px;border-radius:999px;border:1px solid var(--line);color:var(--ink);white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -497,6 +510,8 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
497
510
|
/* replay: guided walkthrough */
|
|
498
511
|
.rp{position:fixed;inset:0;z-index:22;background:var(--ground);display:grid;grid-template-rows:auto 1fr auto}
|
|
499
512
|
.rp__top{display:flex;align-items:center;gap:10px 14px;flex-wrap:wrap;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--line-strong)}
|
|
513
|
+
/* keyboard hints mean nothing on a touch screen */
|
|
514
|
+
@media (hover:none) and (pointer:coarse){.rp__top .note{display:none}}
|
|
500
515
|
.rp__top .crumbs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;min-width:0;font-size:12.5px}
|
|
501
516
|
.rp__top .crumbs button{padding:2px 8px;border-radius:999px;border:1px solid var(--line);color:var(--muted);background:var(--panel)}
|
|
502
517
|
.rp__top .crumbs button[aria-current="step"]{background:var(--ink);color:var(--panel);border-color:var(--ink)}
|
|
@@ -522,7 +537,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
522
537
|
.dbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-width:0}
|
|
523
538
|
.dbar .vchip{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border:1px solid var(--line);border-radius:999px;font:500 12px/1 var(--mono);color:var(--ink);text-decoration:none;white-space:nowrap;background:var(--panel)}
|
|
524
539
|
.dbar .vchip[aria-current="true"]{background:var(--ink);color:var(--panel);border-color:var(--ink)}
|
|
525
|
-
.
|
|
540
|
+
.vchip .ref{color:var(--partial-ink)}.vchip[aria-current="true"] .ref{color:inherit}
|
|
526
541
|
.dbar .vchip--draft{border-style:dashed}
|
|
527
542
|
#cursors{position:absolute;left:0;top:0;pointer-events:none}
|
|
528
543
|
.node.is-same{opacity:.38}
|
|
@@ -576,6 +591,52 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
576
591
|
/* phones: the lightbox stacks stage over side panel. Placed after the base .lb__box rule, which used to win and
|
|
577
592
|
squeeze the stage to nothing on narrow screens */
|
|
578
593
|
@media (max-width:900px){.lb__box{grid-template-columns:1fr;grid-template-rows:minmax(260px,48%) 1fr}.lb__stage{border-right:0;border-bottom:1px solid var(--line)}}
|
|
594
|
+
/* the secondary tools of the detail: inline on wide screens, behind "⋯" on phones */
|
|
595
|
+
.lb__more{display:contents}.lb__moreBtn,.lb__grab{display:none}
|
|
596
|
+
/* phones: the preview gets the height; the step header and tabs are a bottom sheet that starts collapsed */
|
|
597
|
+
@media (max-width:700px){
|
|
598
|
+
.lb{padding:0}
|
|
599
|
+
.lb__box{--peek:calc(98px + env(safe-area-inset-bottom));display:block;position:relative;border-radius:0;height:100%;overflow:hidden}
|
|
600
|
+
.lb__stage{position:absolute;left:0;right:0;top:0;bottom:var(--peek);border:0}
|
|
601
|
+
.lb__tools{flex-wrap:nowrap;gap:8px;padding:8px 10px;position:relative}
|
|
602
|
+
.lb__tools #lbFocus{padding:5px 10px;flex:none}
|
|
603
|
+
.lb__moreBtn{display:inline-grid;margin-left:auto;flex:none;width:32px;height:32px}
|
|
604
|
+
.lb__more{display:none}
|
|
605
|
+
.lb__more.is-open{display:flex;position:absolute;top:calc(100% + 4px);right:8px;z-index:6;flex-direction:column;align-items:flex-start;gap:10px;padding:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);max-width:calc(100vw - 16px)}
|
|
606
|
+
.lb__more .spacer,.lb__more .lb__hint{display:none}
|
|
607
|
+
.lb__scroll{padding:10px}
|
|
608
|
+
.lb__side{position:absolute;left:0;right:0;bottom:0;height:var(--peek);z-index:3;background:var(--panel);border-radius:14px 14px 0 0;box-shadow:0 -6px 24px rgba(0,0,0,.18);overflow:hidden;padding-bottom:env(safe-area-inset-bottom);overscroll-behavior:contain}
|
|
609
|
+
.lb[data-sheet="half"] .lb__side{height:58%;overflow:auto}
|
|
610
|
+
.lb[data-sheet="full"] .lb__side{height:calc(100% - 48px);overflow:auto}
|
|
611
|
+
.lb__grab{display:block;position:sticky;top:0;z-index:3;height:16px;background:var(--panel);cursor:grab;touch-action:none}
|
|
612
|
+
.lb__grab::before{content:"";display:block;width:36px;height:4px;border-radius:2px;background:var(--line);margin:6px auto 0}
|
|
613
|
+
.lb__head{padding:0 14px 8px;gap:4px;top:16px;touch-action:none;cursor:pointer}
|
|
614
|
+
.lb__head .kind{display:none}.lb__head .lb__linkTxt{display:none}.lb__head #lbLink{padding:5px 8px}
|
|
615
|
+
.lb__head h3{font-size:16px}
|
|
616
|
+
.lb[data-sheet="peek"] .lb__head h3{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
617
|
+
.lb[data-sheet="peek"] .lb__side:not(.is-drag) .lbtabs,.lb[data-sheet="peek"] .lb__side:not(.is-drag) .lbtab{visibility:hidden}
|
|
618
|
+
.lbtabs{top:16px}
|
|
619
|
+
}
|
|
620
|
+
@media (max-width:359px){.lb__tools #lbFocus span{display:none}}
|
|
621
|
+
@media (max-width:700px) and (prefers-reduced-motion:no-preference){.lb__side{transition:height .2s ease}.lb__side.is-drag{transition:none}}
|
|
622
|
+
/* focus: only this step's capture or live page, full viewport, with a small floating bar that hides when idle */
|
|
623
|
+
.lb.is-focus{padding:0;background:#111110}
|
|
624
|
+
.lb.is-focus .lb__box{display:block;position:relative;height:100%;border-radius:0;background:#111110;box-shadow:none}
|
|
625
|
+
.lb.is-focus .lb__side,.lb.is-focus .lb__tools{display:none}
|
|
626
|
+
.lb.is-focus .lb__stage{position:absolute;inset:0;border:0;background:#111110}
|
|
627
|
+
.lb.is-focus .lb__scroll{padding:0;overscroll-behavior:contain}
|
|
628
|
+
.lb.is-focus .lb__scroll > p.note{display:none}
|
|
629
|
+
.lb.is-focus .lb__img{box-shadow:none}
|
|
630
|
+
.lb.is-focus .lb__img--m,.lb.is-focus .lb__img--dev{border:0;border-radius:0;box-shadow:none}
|
|
631
|
+
.lb.is-focus .lb__live{border:0;border-radius:0;box-shadow:none}
|
|
632
|
+
.lb.is-focus .lb__win{border-radius:0}
|
|
633
|
+
.lb__fbar{position:absolute;left:50%;bottom:max(14px,env(safe-area-inset-bottom));transform:translateX(-50%);z-index:8;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:6px 8px;width:max-content;max-width:calc(100% - 16px);padding:6px 8px;border-radius:22px;color-scheme:dark;--panel:#1f1f1d;--panel-2:#262624;--ink:#f0f0ec;--muted:#a3a39c;--line:#3d3d37;background:rgba(22,22,21,.9);color:var(--ink);box-shadow:0 8px 30px rgba(0,0,0,.4);backdrop-filter:blur(8px)}
|
|
634
|
+
.lb__fbar .icon-btn{width:34px;height:34px}
|
|
635
|
+
.lb__fpos{font-size:12.5px;color:var(--muted);min-width:44px;text-align:center;font-variant-numeric:tabular-nums}
|
|
636
|
+
.lb__fshow{display:none;position:absolute;top:max(10px,env(safe-area-inset-top));right:10px;z-index:8;width:38px;height:38px;place-items:center;border-radius:50%;background:rgba(22,22,21,.6);color:#f0f0ec;opacity:.75}
|
|
637
|
+
.lb.is-idle .lb__fbar{opacity:0;pointer-events:none}
|
|
638
|
+
.lb.is-focus.is-idle .lb__fshow{display:grid}
|
|
639
|
+
@media (prefers-reduced-motion:no-preference){.lb__fbar{transition:opacity .2s ease}}
|
|
579
640
|
</style>
|
|
580
641
|
</head>
|
|
581
642
|
<body>
|
|
@@ -593,6 +654,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
593
654
|
<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
655
|
<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>
|
|
595
656
|
<div class="dbar" id="dbar" hidden></div>
|
|
657
|
+
<span class="cmpchip" id="cmpChip" hidden></span>
|
|
596
658
|
<div class="spacer"></div>
|
|
597
659
|
<div class="seg" id="modeSeg" aria-label="Layout"></div>
|
|
598
660
|
<div class="seg" id="locSeg" aria-label="Locale"></div>
|
|
@@ -610,7 +672,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
610
672
|
<a class="inbox-btn" id="inboxBtn" hidden></a>
|
|
611
673
|
<button class="tog" id="togRight" aria-pressed="true" aria-label="Right panel: summary" title="Summary (F hides both)"><svg class="i" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="1.5"/><path d="M15 4v16"/></svg></button>
|
|
612
674
|
</header>
|
|
613
|
-
<
|
|
675
|
+
<main class="main" id="main">
|
|
614
676
|
<aside class="side" id="side">
|
|
615
677
|
<h2><span data-t="shownRun"></span> <button class="btn" id="allRuns" data-t="allRuns"></button></h2>
|
|
616
678
|
<div class="runcard" id="runcard"></div>
|
|
@@ -637,7 +699,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
637
699
|
</section>
|
|
638
700
|
<section class="ri" id="ri" hidden aria-label="Index"></section>
|
|
639
701
|
<aside class="rside" id="rside"></aside>
|
|
640
|
-
</
|
|
702
|
+
</main>
|
|
641
703
|
</div>
|
|
642
704
|
<div class="si" id="si" hidden role="dialog" aria-modal="true" aria-labelledby="siTitle"></div>
|
|
643
705
|
<div class="lb" id="lb" hidden role="dialog" aria-modal="true" aria-labelledby="lbTitle"></div>
|
|
@@ -668,7 +730,7 @@ details summary{cursor:pointer;font-size:12.5px;font-weight:500}
|
|
|
668
730
|
const EMBEDDED = /*__QAWALK_RUN__*/null;
|
|
669
731
|
if (!window.QAWALK_APP) { const el = document.getElementById('qawalk-app'); if (el) { try { window.QAWALK_APP = JSON.parse(el.textContent); } catch {} } }
|
|
670
732
|
const I18N = {
|
|
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ý',
|
|
733
|
+
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ý',
|
|
672
734
|
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',
|
|
673
735
|
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ý',
|
|
674
736
|
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í',
|
|
@@ -690,9 +752,9 @@ const I18N = {
|
|
|
690
752
|
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',
|
|
691
753
|
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',
|
|
692
754
|
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',
|
|
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í',
|
|
755
|
+
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í',
|
|
694
756
|
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' },
|
|
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',
|
|
757
|
+
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',
|
|
696
758
|
pass:'Passed', partial:'Partial', fail:'Failed', skip:'Not checked', crop:'crop', html:'HTML snapshot', email:'e-mail', external:'third party',
|
|
697
759
|
branch:'Branch', commit:'Commit', author:'Author', pr:'PR', issue:'Issue', env:'Environment', duration:'Duration', noPr:'no PR', open:'open', merged:'merged', closed:'closed',
|
|
698
760
|
older:'Older run', newer:'Newer run', purpose:'Screen purpose from the scenario', criteria:'Acceptance criteria', jevTitle:'Semantic evaluation',
|
|
@@ -714,7 +776,7 @@ const I18N = {
|
|
|
714
776
|
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',
|
|
715
777
|
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',
|
|
716
778
|
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',
|
|
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',
|
|
779
|
+
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',
|
|
718
780
|
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' }
|
|
719
781
|
};
|
|
720
782
|
const storedLang = (() => { try { return localStorage.getItem('qawalk.lang'); } catch { return null; } })();
|
|
@@ -741,7 +803,9 @@ const ICON = {
|
|
|
741
803
|
share:'<svg class="i" viewBox="0 0 24 24"><path d="M12 16V4M7 9l5-5 5 5M5 20h14"/></svg>',
|
|
742
804
|
copy:'<svg class="i" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>',
|
|
743
805
|
download:'<svg class="i" viewBox="0 0 24 24"><path d="M12 4v12M7 11l5 5 5-5M5 20h14"/></svg>',
|
|
744
|
-
next:'<svg class="i" viewBox="0 0 24 24"><path d="m9 5 7 7-7 7"/></svg>'
|
|
806
|
+
next:'<svg class="i" viewBox="0 0 24 24"><path d="m9 5 7 7-7 7"/></svg>',
|
|
807
|
+
focus:'<svg class="i" viewBox="0 0 24 24"><path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"/></svg>',
|
|
808
|
+
more:'<svg class="i" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="19" cy="12" r="1.3"/></svg>'
|
|
745
809
|
};
|
|
746
810
|
const MARK = { pass:'✓', partial:'!', fail:'✕', skip:'–' };
|
|
747
811
|
const VSTAT = { pass:'pass', fail:'fail', needs_review:'partial', not_evaluated:'partial', not_reached:'skip' };
|
|
@@ -1014,7 +1078,7 @@ function openThread (nodeId, threadId) {
|
|
|
1014
1078
|
requestAnimationFrame(() => requestAnimationFrame(() => { const el = threadId && document.querySelector(`#lb .c-thread[data-thread="${CSS.escape(threadId)}"]`); (el || $('lb').querySelector('#c-step'))?.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { C.focus = null; }, 1800); }));
|
|
1015
1079
|
}
|
|
1016
1080
|
/** Key C in the lightbox (or its comment button): the composer of the step, in view and focused. */
|
|
1017
|
-
function focusComposer () { const t
|
|
1081
|
+
function focusComposer () { if (S.focus) setFocus(false); if (lbPhone() && S.lbSheet === 'peek') setSheet('half'); const t =$('lb').querySelector('.c-main .c-text'); if (!t) return; t.scrollIntoView({ behavior: 'smooth', block: 'center' }); t.focus({ preventScroll: true }); }
|
|
1018
1082
|
/** #comments=all|run|flow: the right panel on the comments. */
|
|
1019
1083
|
function showCommentsFromHash (which) {
|
|
1020
1084
|
S.cTab = ['all', 'run', 'flow'].includes(which) ? which : 'all';
|
|
@@ -1171,6 +1235,11 @@ function fillPixelBadges () {
|
|
|
1171
1235
|
Promise.all([worker(), worker(), worker()]);
|
|
1172
1236
|
}
|
|
1173
1237
|
function renderCmpControl () {
|
|
1238
|
+
// what the canvas is compared with stays in view, with a way out (the choice itself lives in the menu)
|
|
1239
|
+
const chip = $('cmpChip');
|
|
1240
|
+
chip.hidden = !S.cmp;
|
|
1241
|
+
chip.innerHTML = S.cmp ? `<span>${T('cmpWith')} <b>${esc(cmpLabel())}</b></span><button type="button" class="icon-btn" id="cmpChipOff" aria-label="${esc(T('cmpStop'))}" title="${esc(T('cmpStop'))}">${ICON.close}</button>` : '';
|
|
1242
|
+
if (S.cmp) $('cmpChipOff').onclick = () => loadCmp('');
|
|
1174
1243
|
const list = cmpCandidates();
|
|
1175
1244
|
const ctl = $('cmpCtl');
|
|
1176
1245
|
ctl.hidden = !list.length;
|
|
@@ -1184,7 +1253,11 @@ function renderCmpControl () {
|
|
|
1184
1253
|
const X0 = 80, Y0 = 90;
|
|
1185
1254
|
// phones only: the larger phone cards are narrower and taller than the desktop pair
|
|
1186
1255
|
const COLW = () => S.vp === 'mobile' ? 420 : 580;
|
|
1187
|
-
|
|
1256
|
+
// Lane height grows to the tallest card (a session chip and a three-line title made cards taller than the
|
|
1257
|
+
// fixed pitch, so the next lane's heading sat on them). LANE_EXTRA is measured after the cards render.
|
|
1258
|
+
let LANE_EXTRA = 0;
|
|
1259
|
+
const LANE_GAP = 96;
|
|
1260
|
+
const LANEH = () => (S.jev === 'json' ? 640 : 460) + (S.vp === 'mobile' ? 240 : 0) + LANE_EXTRA;
|
|
1188
1261
|
// long flows wrap: a lane with more than WRAP + 2 columns continues in rows of WRAP steps under itself
|
|
1189
1262
|
const WRAP = 8;
|
|
1190
1263
|
let LANE_ROWS = null;
|
|
@@ -1243,6 +1316,9 @@ function renderNodes () {
|
|
|
1243
1316
|
<div class="node__foot">${attIcons(n)}${cmtBadge(n.id)}${sigBadge(n)}<span class="loc-dots">${RUN.scope.locales.map((l) => `<span class="st-${esc(n.by_locale?.[l] || 'skip')}" title="${esc(l)}: ${esc(T(n.by_locale?.[l] || 'skip'))}">${esc(l)}</span>`).join('')}</span>${S.cmp && cmpNode(n.id) ? `<span class="pxd" data-pxd="${esc(n.id)}" title="${esc(cmpLabel())}">◐ …</span>${cmpCritChanges(n).length ? `<span class="cmpd" title="${esc(T('cmpTitle'))} ${esc(cmpLabel())}">Δ ${cmpCritChanges(n).length}</span>` : ''}` : ''}${changed && !S.cmp ? `<span class="delta" title="${esc(pr.git?.commit || '')}">Δ ${esc(T(before).toLowerCase())} → ${esc(T(n.status).toLowerCase())}</span>` : ''}${reviewState(n.id) ? `<span class="badge st-${reviewState(n.id) === 'approved' ? 'pass' : 'fail'}" style="margin-left:${changed || S.cmp ? '4px' : 'auto'}">${reviewState(n.id) === 'approved' ? '✓ ' + T('approved') : '✕ ' + T('rejected')}</span>` : ''}</div>
|
|
1244
1317
|
</div>`;
|
|
1245
1318
|
}).join('');
|
|
1319
|
+
const tallest = Math.max(0, ...[...$('nodes').querySelectorAll('.node:not(.node--decision)')].map((el) => el.offsetHeight));
|
|
1320
|
+
const need = tallest + LANE_GAP - LANEH();
|
|
1321
|
+
if (need > 0 || (LANE_EXTRA > 0 && need < -LANE_GAP)) { LANE_EXTRA = Math.max(0, LANE_EXTRA + need); return renderNodes(); }
|
|
1246
1322
|
requestAnimationFrame(() => { drawEdges(); renderMini(); });
|
|
1247
1323
|
fillPixelBadges();
|
|
1248
1324
|
}
|
|
@@ -2105,7 +2181,7 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2105
2181
|
const lb = $('lb');
|
|
2106
2182
|
const html = `<div class="lb__box">
|
|
2107
2183
|
<div class="lb__stage">
|
|
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>
|
|
2184
|
+
<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>
|
|
2109
2185
|
<div class="lb__scroll" id="lbScroll">${(() => {
|
|
2110
2186
|
const bn0 = S.cmp && cmpNode(n.id), bcap0 = bn0?.captures?.[S.loc]?.[v.name];
|
|
2111
2187
|
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);
|
|
@@ -2119,10 +2195,11 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2119
2195
|
return `<div class="lb__cmp"><figure><figcaption><b>${T('thisRun')}</b> ${fmtDate(RUN.started_at)}${RUN.git?.commit ? ' · ' + esc(RUN.git.commit) : ''}</figcaption>${img}</figure><figure><figcaption><b>${esc(cmpLabel())}</b> <span id="lbPx"></span></figcaption>${bimg}</figure></div>`;
|
|
2120
2196
|
})()}</div>
|
|
2121
2197
|
</div>
|
|
2122
|
-
<div class="lb__side">
|
|
2198
|
+
<div class="lb__side" id="lbSide">
|
|
2199
|
+
<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>
|
|
2123
2200
|
<div class="lb__head">
|
|
2124
2201
|
<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>` : ''}
|
|
2125
|
-
<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}
|
|
2202
|
+
<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>
|
|
2126
2203
|
<h3 id="lbTitle">${esc(loc(n.title))}</h3>
|
|
2127
2204
|
<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>
|
|
2128
2205
|
${sameAsCmp(n) ? `<p class="note">${T('unchangedSince')} ${esc(cmpLabel())}</p>` : ''}${cap?.live?.stale_capture ? `<p class="note">${T('staleCapture')}</p>` : ''}
|
|
@@ -2156,11 +2233,18 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2156
2233
|
const keep = opts.keepFrame && ['live', 'view'].includes(S.lbMode) && lb.querySelector('#lbFrame')?.getAttribute('src') === (S.lbMode === 'view' ? viewSrc(cap) : liveSrc(cap)) && lb.dataset.step === id && lb.dataset.vp === v.name;
|
|
2157
2234
|
if (keep) { const tmp = document.createElement('div'); tmp.innerHTML = html; lb.querySelector('.lb__side').replaceWith(tmp.querySelector('.lb__side')); toFrame({ type: 'qawalk:pins', pins: pinsFor(n, v.name) }); }
|
|
2158
2235
|
else lb.innerHTML = html;
|
|
2159
|
-
lb.
|
|
2236
|
+
if (lb.hidden) S.lbSheet = 'peek'; // the phone sheet starts collapsed each time the detail opens
|
|
2237
|
+
lb.dataset.step = id; lb.dataset.vp = v.name; lb.dataset.sheet = S.lbSheet || 'peek';
|
|
2238
|
+
wireLbOnce(lb);
|
|
2239
|
+
renderFocusBar(lb, n, v, cap, list, i);
|
|
2160
2240
|
lb.hidden = false;
|
|
2161
2241
|
fitLive(); requestAnimationFrame(fitLive);
|
|
2162
2242
|
setTimeout(() => presenceView(true), 0);
|
|
2163
|
-
setHash({ step: id, loc: S.loc, vp: v.name });
|
|
2243
|
+
setHash({ step: id, loc: S.loc, vp: v.name, focus: S.focus ? '1' : null });
|
|
2244
|
+
$('lbFocus').onclick = () => setFocus(true);
|
|
2245
|
+
$('lbMoreBtn').onclick = (e) => { e.stopPropagation(); const m = $('lbMore'), open = !m.classList.contains('is-open'); m.classList.toggle('is-open', open); e.currentTarget.setAttribute('aria-expanded', String(open)); };
|
|
2246
|
+
// a tool chosen in the "⋯" menu closes it, so the preview is free for the click that follows (Inspect, Comment on element)
|
|
2247
|
+
$('lbMore').onclick = (e) => { if (e.target.closest('button')) setTimeout(closeLbMore, 0); };
|
|
2164
2248
|
if ($('lbBack')) $('lbBack').onclick = () => { closeLb(); openStack(n.stack, { focus: n.itemId }); };
|
|
2165
2249
|
if ($('lbComments')) $('lbComments').onclick = () => focusComposer();
|
|
2166
2250
|
placeCommentPins($('lbImg'), n, cap, v);
|
|
@@ -2215,6 +2299,7 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2215
2299
|
if (!li.dataset.where) toast(n.criteria.some((x) => x.target) || n.metadata?.[S.loc] ? T('noPlaceToast') : T('noMeta'));
|
|
2216
2300
|
if (li.dataset.where === 'meta' && !n.metadata?.[S.loc]) { toast(T('noMeta')); return; }
|
|
2217
2301
|
if (li.dataset.where === 'page' && img) {
|
|
2302
|
+
if (lbPhone()) setSheet('peek'); // on a phone the sheet steps aside so the highlight is in view
|
|
2218
2303
|
const els = [...img.querySelectorAll(`.hlbox[data-ac="${CSS.escape(li.dataset.hl)}"]`)].filter((e) => parseFloat(e.style.width) > 0);
|
|
2219
2304
|
els.forEach((e, k) => { e.hidden = false; e.classList.add('on'); if (!k) e.classList.add('first'); });
|
|
2220
2305
|
if (els[0]) $('lbScroll').scrollTo({ top: Math.max(0, els[0].offsetTop - 80), behavior: 'smooth' }); else toast(T('noBoxToast'));
|
|
@@ -2236,13 +2321,96 @@ function openLb (id, vpName, opts = {}) {
|
|
|
2236
2321
|
li.addEventListener('mouseenter', () => { if (li.dataset.where !== 'page' || !img || lb.querySelector('.ac li.is-on')) return; img.querySelectorAll(`.hlbox[data-ac="${CSS.escape(li.dataset.hl)}"]`).forEach((e) => { e.hidden = false; }); });
|
|
2237
2322
|
li.addEventListener('mouseleave', () => { if (!img || lb.querySelector('.ac li.is-on')) return; img.querySelectorAll('.hlbox').forEach((e) => { e.hidden = true; }); });
|
|
2238
2323
|
});
|
|
2239
|
-
$('lbClose').focus();
|
|
2324
|
+
(S.focus ? $('lbFexit') : $('lbClose')).focus({ preventScroll: true });
|
|
2240
2325
|
if (S.pendingAc) {
|
|
2241
2326
|
const li = lb.querySelector(`[data-hl="${CSS.escape(S.pendingAc)}"]`); S.pendingAc = null;
|
|
2242
2327
|
if (li) { const im = img?.querySelector('img'); const go = () => setTimeout(() => li.click(), 60); im && !im.complete ? im.addEventListener('load', go, { once: true }) : go(); }
|
|
2243
2328
|
}
|
|
2244
2329
|
}
|
|
2245
|
-
function closeLb () { $('lb').hidden = true; S.open = null; LIVE.mode = null; LIVE.insp = null; setHash(null); presenceView(); }
|
|
2330
|
+
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(); }
|
|
2331
|
+
const lbPhone = () => matchMedia('(max-width:700px)').matches;
|
|
2332
|
+
function closeLbMore () { const m = $('lbMore'); if (!m) return; m.classList.remove('is-open'); $('lbMoreBtn')?.setAttribute('aria-expanded', 'false'); }
|
|
2333
|
+
/** The phone bottom sheet of the detail: peek (title and status), half, full. */
|
|
2334
|
+
function setSheet (st) {
|
|
2335
|
+
S.lbSheet = st; $('lb').dataset.sheet = st;
|
|
2336
|
+
$('lbGrab')?.setAttribute('aria-expanded', String(st !== 'peek'));
|
|
2337
|
+
if (st === 'peek' && $('lbSide')) $('lbSide').scrollTop = 0;
|
|
2338
|
+
}
|
|
2339
|
+
/** Focus: only this step's capture or live page, full viewport. Previous / next and the switches keep it on;
|
|
2340
|
+
the prototype (P) is a separate presentation and returns here when it ends. */
|
|
2341
|
+
function setFocus (on) {
|
|
2342
|
+
if (!S.open) return;
|
|
2343
|
+
S.focus = !!on; closeLbMore();
|
|
2344
|
+
if (on) { C.pinMode = false; LIVE.mode = null; toFrame({ type: 'qawalk:mode', mode: null }); }
|
|
2345
|
+
openLb(S.open, S.lbVp, { keepFrame: true });
|
|
2346
|
+
}
|
|
2347
|
+
const FX = { timer: null, over: false };
|
|
2348
|
+
// the floating bar shows on activity and hides after a while; a tap on the preview brings it back
|
|
2349
|
+
function fxWake () {
|
|
2350
|
+
const lb = $('lb'); lb.classList.remove('is-idle'); clearTimeout(FX.timer);
|
|
2351
|
+
if (!S.focus) return;
|
|
2352
|
+
FX.timer = setTimeout(function idle () { if (!S.focus || lb.hidden) return; if (FX.over) { FX.timer = setTimeout(idle, 1500); return; } lb.classList.add('is-idle'); }, 2800);
|
|
2353
|
+
}
|
|
2354
|
+
function fxHide () { clearTimeout(FX.timer); $('lb').classList.add('is-idle'); }
|
|
2355
|
+
function renderFocusBar (lb, n, v, cap, list, i) {
|
|
2356
|
+
lb.querySelector('#lbFbar')?.remove(); lb.querySelector('#lbFshow')?.remove();
|
|
2357
|
+
lb.classList.toggle('is-focus', !!S.focus);
|
|
2358
|
+
if (!S.focus) { lb.classList.remove('is-idle'); clearTimeout(FX.timer); FX.over = false; return; }
|
|
2359
|
+
const modes = [...(liveSrc(cap) ? [['live', T('liveMode')]] : []), ...(viewSrc(cap) ? [['view', T('htmlView')]] : [])];
|
|
2360
|
+
if (modes.length && !S.cmp) modes.push(['shot', T('shotMode')]); else modes.length = 0;
|
|
2361
|
+
const vps = n.item ? viewports().filter((x) => n.captures?.[S.loc]?.[x.name]) : viewports();
|
|
2362
|
+
lb.insertAdjacentHTML('beforeend', `<div class="lb__fbar" id="lbFbar" role="toolbar" aria-label="${esc(T('focusBar'))}"><button class="icon-btn" id="lbFexit" aria-label="${esc(T('focusExit'))}" title="${esc(T('focusExit'))} (Esc)">${ICON.close}</button><button class="icon-btn" id="lbFprev" aria-label="Previous" ${i <= 0 ? 'disabled' : ''}>${ICON.prev}</button><span class="lb__fpos" title="${esc(loc(n.title))}">${i + 1} / ${list.length}</span><button class="icon-btn" id="lbFnext" aria-label="Next" ${i >= list.length - 1 ? 'disabled' : ''}>${ICON.next}</button>${vps.length > 1 ? '<div class="seg" id="lbFvp"></div>' : ''}${modes.length ? '<div class="seg" id="lbFmode"></div>' : ''}</div><button class="lb__fshow" id="lbFshow" aria-label="${esc(T('focusShow'))}" title="${esc(T('focusShow'))}">${ICON.more}</button>`);
|
|
2363
|
+
$('lbFexit').onclick = () => setFocus(false);
|
|
2364
|
+
$('lbFprev').onclick = () => $('lbPrev')?.click();
|
|
2365
|
+
$('lbFnext').onclick = () => $('lbNext')?.click();
|
|
2366
|
+
if ($('lbFvp')) seg($('lbFvp'), vps.map((x) => [x.name, vpLabel(x)]), v.name, (nv) => openLb(n.id, nv));
|
|
2367
|
+
if ($('lbFmode')) seg($('lbFmode'), modes, S.lbMode, (m) => { S.lbMode = m; LIVE.mode = null; openLb(n.id, v.name); });
|
|
2368
|
+
$('lbFshow').onclick = (e) => { e.stopPropagation(); fxWake(); };
|
|
2369
|
+
FX.over = false; fxWake();
|
|
2370
|
+
}
|
|
2371
|
+
/** Listeners that live as long as the detail element: it re-renders its content, these stay. */
|
|
2372
|
+
function wireLbOnce (lb) {
|
|
2373
|
+
if (lb.dataset.wired) return; lb.dataset.wired = '1';
|
|
2374
|
+
let lastPointer = 'mouse', swipe = null;
|
|
2375
|
+
lb.addEventListener('pointerdown', (e) => { lastPointer = e.pointerType; }, true);
|
|
2376
|
+
lb.addEventListener('pointermove', (e) => { if (S.focus && e.pointerType === 'mouse') { FX.over = !!e.target.closest('#lbFbar'); if (lb.classList.contains('is-idle') || !FX.over) fxWake(); } });
|
|
2377
|
+
lb.addEventListener('click', (e) => {
|
|
2378
|
+
if (!e.target.closest('#lbMore, #lbMoreBtn')) closeLbMore();
|
|
2379
|
+
// focus: a tap on the preview shows or hides the bar (a mouse shows it just by moving)
|
|
2380
|
+
if (!S.focus || !e.target.closest('#lbScroll') || e.target.closest('button,a,input,select,textarea')) return;
|
|
2381
|
+
if (lb.classList.contains('is-idle')) fxWake(); else if (lastPointer !== 'mouse') fxHide();
|
|
2382
|
+
});
|
|
2383
|
+
// focus on a touch screen: a swipe sideways moves to the previous or next step, unless the page is pinch-zoomed
|
|
2384
|
+
lb.addEventListener('touchstart', (e) => { swipe = S.focus && e.touches.length === 1 && e.target.closest('#lbScroll') ? { x: e.touches[0].clientX, y: e.touches[0].clientY, t: Date.now() } : null; }, { passive: true });
|
|
2385
|
+
lb.addEventListener('touchmove', (e) => { if (e.touches.length > 1) swipe = null; }, { passive: true });
|
|
2386
|
+
lb.addEventListener('touchend', (e) => {
|
|
2387
|
+
const sw = swipe; swipe = null; if (!sw || !S.focus) return;
|
|
2388
|
+
const t = e.changedTouches[0], dx = t.clientX - sw.x, dy = t.clientY - sw.y, sc = $('lbScroll');
|
|
2389
|
+
if ((window.visualViewport?.scale || 1) > 1.05 || Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5 || Date.now() - sw.t > 900) return;
|
|
2390
|
+
if (sc && sc.scrollWidth > sc.clientWidth + 2) return; // a capture wider than the screen pans sideways instead
|
|
2391
|
+
$(dx < 0 ? 'lbNext' : 'lbPrev')?.click();
|
|
2392
|
+
}, { passive: true });
|
|
2393
|
+
// the phone sheet: drag the handle or the header to resize it, tap to open or collapse it
|
|
2394
|
+
lb.addEventListener('pointerdown', (e) => {
|
|
2395
|
+
if (!lbPhone() || S.focus || e.button > 0) return;
|
|
2396
|
+
const h = e.target.closest('#lbGrab, .lb__head'); if (!h || e.target.closest('button,a,input,select,textarea,label')) return;
|
|
2397
|
+
const side = $('lbSide'), box = side.parentElement.getBoundingClientRect(), stage = lb.querySelector('.lb__stage').getBoundingClientRect();
|
|
2398
|
+
const peekH = box.bottom - stage.bottom, fullH = box.height - 48, halfH = box.height * 0.58;
|
|
2399
|
+
const y0 = e.clientY, h0 = side.getBoundingClientRect().height; let moved = false;
|
|
2400
|
+
const move = (ev) => { const dy = ev.clientY - y0; if (!moved && Math.abs(dy) < 6) return; moved = true; side.classList.add('is-drag'); side.style.overflow = 'hidden'; side.style.height = `${Math.max(peekH, Math.min(fullH, h0 - dy))}px`; };
|
|
2401
|
+
const up = (ev) => {
|
|
2402
|
+
removeEventListener('pointermove', move); removeEventListener('pointerup', up); removeEventListener('pointercancel', up);
|
|
2403
|
+
side.classList.remove('is-drag');
|
|
2404
|
+
if (!moved) { if (ev.type === 'pointerup') setSheet(S.lbSheet === 'peek' ? 'half' : 'peek'); return; }
|
|
2405
|
+
const hh = parseFloat(side.style.height); side.style.height = ''; side.style.overflow = '';
|
|
2406
|
+
setSheet([['peek', peekH], ['half', halfH], ['full', fullH]].sort((a, b) => Math.abs(a[1] - hh) - Math.abs(b[1] - hh))[0][0]);
|
|
2407
|
+
};
|
|
2408
|
+
addEventListener('pointermove', move); addEventListener('pointerup', up); addEventListener('pointercancel', up);
|
|
2409
|
+
});
|
|
2410
|
+
lb.addEventListener('keydown', (e) => { if (e.target.id === 'lbGrab' && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); setSheet(S.lbSheet === 'peek' ? 'half' : 'peek'); } });
|
|
2411
|
+
// tabs and the comment composer need the sheet open
|
|
2412
|
+
lb.addEventListener('focusin', (e) => { if (lbPhone() && !S.focus && S.lbSheet === 'peek' && e.target.closest('.lbtabs, .lbtab')) setSheet('half'); });
|
|
2413
|
+
}
|
|
2246
2414
|
function setHash (p) {
|
|
2247
2415
|
const all = Object.fromEntries(Object.entries({ ...(p || {}), cmp: S.cmp?.id, mode: S.mode === 'index' ? 'index' : null }).filter(([, x]) => x));
|
|
2248
2416
|
const h = Object.keys(all).length ? '#' + new URLSearchParams(all).toString() : ' ';
|
|
@@ -2265,6 +2433,7 @@ function applyHash () {
|
|
|
2265
2433
|
if (!step || !byId(step)) return;
|
|
2266
2434
|
if (p.get('loc') && RUN.scope.locales.includes(p.get('loc')) && p.get('loc') !== S.loc) { S.loc = p.get('loc'); renderAll(); }
|
|
2267
2435
|
S.pendingAc = p.get('ac');
|
|
2436
|
+
S.focus = p.get('focus') === '1'; // #step=<id>&focus=1 opens the step's preview alone
|
|
2268
2437
|
openLb(step, p.get('vp'));
|
|
2269
2438
|
}
|
|
2270
2439
|
|
|
@@ -2586,9 +2755,14 @@ const frames = () => [...document.querySelectorAll('#lbFrame, #lbFrameB, #ptIfra
|
|
|
2586
2755
|
function fitLive () {
|
|
2587
2756
|
for (const f of frames()) {
|
|
2588
2757
|
const box = f.parentElement, host = box.parentElement;
|
|
2589
|
-
|
|
2758
|
+
f.dataset.h ||= f.style.height; // the frame's own height; focus stretches it to the screen and gives it back
|
|
2759
|
+
const w = parseInt(f.style.width, 10);
|
|
2590
2760
|
const cs = getComputedStyle(host);
|
|
2591
|
-
const
|
|
2761
|
+
const focus = S.focus && host.id === 'lbScroll' && !$('lb').hidden;
|
|
2762
|
+
const k = Math.min(1, (host.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) - (focus ? 0 : 2)) / w);
|
|
2763
|
+
// in focus the live page fills the screen: its real width, scaled down to fit, and as tall as the screen allows
|
|
2764
|
+
const h = focus ? Math.max(1, Math.floor(host.clientHeight / k)) : parseInt(f.dataset.h, 10);
|
|
2765
|
+
f.style.height = `${h}px`;
|
|
2592
2766
|
f.style.transformOrigin = '0 0'; f.style.transform = `scale(${k})`;
|
|
2593
2767
|
box.style.width = `${Math.floor(w * k)}px`; box.style.height = `${Math.floor(h * k)}px`;
|
|
2594
2768
|
}
|
|
@@ -2604,7 +2778,7 @@ function inspectorHtml () {
|
|
|
2604
2778
|
const canEdit = isDraft() && canWrite() && APP().user?.role !== 'viewer' && LIVE.mode === null;
|
|
2605
2779
|
const kindOf = (prop) => /color/.test(prop) ? 'color' : prop === 'font-family' ? 'font' : 'dim';
|
|
2606
2780
|
const EDITABLE = ['color', 'background-color', 'border-color', 'font-family', 'font-size', 'border-radius', 'padding', 'margin', 'gap'];
|
|
2607
|
-
const picker = (s) => { const opts = LIVE.tokens.filter((t) => t.kind === kindOf(s.prop)); return EDITABLE.includes(s.prop) && opts.length ? `<select data-edit-prop="${esc(s.prop)}"><option value="">${T('editKeep')}</option>${opts.map((t) => `<option value="${esc(`var(--${t.name.replace(/\./g, '-')}, ${t.value})`)}">${esc(t.name)} · ${esc(t.value)}</option>`).join('')}</select>` : ''; };
|
|
2781
|
+
const picker = (s) => { const opts = LIVE.tokens.filter((t) => t.kind === kindOf(s.prop)); return EDITABLE.includes(s.prop) && opts.length ? `<select data-edit-prop="${esc(s.prop)}" aria-label="${esc(s.prop)}"><option value="">${T('editKeep')}</option>${opts.map((t) => `<option value="${esc(`var(--${t.name.replace(/\./g, '-')}, ${t.value})`)}">${esc(t.name)} · ${esc(t.value)}</option>`).join('')}</select>` : ''; };
|
|
2608
2782
|
return `<div class="sec insp" id="inspSec"><h4>${T('inspect')} <span class="strategy">${esc(el.tag)} · ${el.size.join(' × ')}</span></h4><div class="anchor-note">${esc(el.selector || '')}</div><table>${el.styles.map((s) => `<tr><td>${esc(s.prop)}</td><td>${esc(s.value)}${s.token ? ` <span class="tok">${esc(s.token)}</span>` : ` <span class="raw">${T('noToken')}</span>`}${canEdit ? `<div>${picker(s)}</div>` : ''}</td></tr>`).join('')}</table>
|
|
2609
2783
|
${canEdit ? `<h4 style="margin-top:12px">${T('editTitle')}</h4>${el.editable ? `<label class="note" for="editText">${T('editText')}</label><input id="editText" value="${esc(el.text)}" style="width:100%;font:inherit;padding:6px;border:1px solid var(--line);border-radius:6px;margin:4px 0 8px">` : ''}<button class="btn btn--primary" id="editSave">${T('editSave')}</button> ` : ''}<button class="btn" id="copyCss" data-css="${esc(css)}" style="margin-top:8px">${T('copyCss')}</button></div>`;
|
|
2610
2784
|
}
|
|
@@ -2637,6 +2811,7 @@ addEventListener('message', (e) => {
|
|
|
2637
2811
|
} else {
|
|
2638
2812
|
// a click in the inspector pins the selection: hover stops, and the element can be edited
|
|
2639
2813
|
LIVE.insp = el; LIVE.mode = null; toFrame({ type: 'qawalk:mode', mode: null }); toFrame({ type: 'qawalk:highlight', selector: el.selector, box: el.box });
|
|
2814
|
+
if (lbPhone() && S.lbSheet === 'peek') setSheet('half'); // the inspector's table lives in the sheet
|
|
2640
2815
|
const sec = $('inspSec'); if (sec) sec.outerHTML = inspectorHtml(); else $('lbSideTop')?.insertAdjacentHTML('afterend', inspectorHtml());
|
|
2641
2816
|
renderLiveTools(); wireCopyCss();
|
|
2642
2817
|
}
|
|
@@ -2677,7 +2852,7 @@ function stepStatusHtml (n) {
|
|
|
2677
2852
|
const h = headOf(n); if (!h) return '';
|
|
2678
2853
|
const opts = ['in_progress', 'ready_for_review', 'ready_for_dev'];
|
|
2679
2854
|
const by = h.agent ? `${h.actor} ${T('viaAgent')} ${h.agent}` : h.actor;
|
|
2680
|
-
return `<div class="sec"><h4>${T('stepStatus')}</h4>${canWrite() && APP().user?.role !== 'viewer' ? `<select id="stepStatus">${opts.map((o) => `<option value="${o}" ${h.status === o ? 'selected' : ''}>${T('st_' + o)}</option>`).join('')}</select>` : `<span class="stchip stchip--${esc(h.status)}">${T('st_' + h.status)}</span>`}<p class="note" style="margin-top:6px">${T('revisionBy')} <span class="mono">${esc(h.revision.slice(0, 8))}</span> · ${esc(by)} · ${fmtDate(h.updated_at)}</p></div>`;
|
|
2855
|
+
return `<div class="sec"><h4>${T('stepStatus')}</h4>${canWrite() && APP().user?.role !== 'viewer' ? `<select id="stepStatus" aria-label="${esc(T('stepStatus'))}">${opts.map((o) => `<option value="${o}" ${h.status === o ? 'selected' : ''}>${T('st_' + o)}</option>`).join('')}</select>` : `<span class="stchip stchip--${esc(h.status)}">${T('st_' + h.status)}</span>`}<p class="note" style="margin-top:6px">${T('revisionBy')} <span class="mono">${esc(h.revision.slice(0, 8))}</span> · ${esc(by)} · ${fmtDate(h.updated_at)}</p></div>`;
|
|
2681
2856
|
}
|
|
2682
2857
|
function wireStepStatus (n) {
|
|
2683
2858
|
const sel = $('stepStatus'); if (!sel) return;
|
|
@@ -2816,6 +2991,13 @@ async function postJson (path, body) {
|
|
|
2816
2991
|
return res.json();
|
|
2817
2992
|
}
|
|
2818
2993
|
async function reloadRun () { const a = APP(); const res = await fetch(`${a.api}/orgs/${a.org}/runs/${a.run}`, { credentials: 'same-origin' }); if (res.ok) { RUN = await res.json(); applyReviewerVerdicts(); renderAll(); if (S.open) openLb(S.open, S.lbVp); } }
|
|
2994
|
+
// the panels take their size defaults before the run loads, so the canvas does not jump once it arrives
|
|
2995
|
+
{ // the same rule as after loading: phones and embeds without panels, the left one from 1600 px, a saved choice wins
|
|
2996
|
+
let left = innerWidth >= 1600, right = true;
|
|
2997
|
+
if (innerWidth <= 1100 || window.QAWALK_APP?.embed) left = right = false;
|
|
2998
|
+
else { try { const p = JSON.parse(localStorage.getItem(`qawalk-panels-${innerWidth >= 1600 ? 'wide' : 'narrow'}`) || 'null'); if (p) { left = !!p.left; right = !!p.right; } } catch {} }
|
|
2999
|
+
$('main').classList.toggle('no-left', !left); $('main').classList.toggle('no-right', !right);
|
|
3000
|
+
}
|
|
2819
3001
|
load().then((data) => {
|
|
2820
3002
|
RUN = data;
|
|
2821
3003
|
requestAnimationFrame(() => setTimeout(loadDetails, 0)); // after the first paint
|
|
@@ -2865,7 +3047,9 @@ load().then((data) => {
|
|
|
2865
3047
|
if (!$('br').hidden) { if (e.key === 'Escape') closeBrowser(); return; }
|
|
2866
3048
|
if (!$('lb').hidden) {
|
|
2867
3049
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
2868
|
-
if (
|
|
3050
|
+
if (S.focus) fxWake();
|
|
3051
|
+
if (e.key === 'Escape') { if ($('lbMore')?.classList.contains('is-open')) closeLbMore(); else if (S.focus) setFocus(false); else closeLb(); }
|
|
3052
|
+
if (e.key === 'f' || e.key === 'F') { e.preventDefault(); setFocus(!S.focus); }
|
|
2869
3053
|
if (e.key === 'ArrowRight') $('lbNext')?.click();
|
|
2870
3054
|
if (e.key === 'ArrowLeft') $('lbPrev')?.click();
|
|
2871
3055
|
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); focusComposer(); }
|