kaloko 0.17.0 → 0.18.0
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 +7 -0
- package/package.json +1 -1
- package/schemas/config.v1.json +15 -0
- package/schemas/run.v1.json +17 -0
- package/skills/kaloko/SKILL.md +4 -0
- package/skills/kaloko/reference/ci.md +83 -0
- package/src/cli.mjs +29 -1
- package/src/lib/changelog-core.mjs +19 -1
- package/src/lib/ci.mjs +4 -2
- package/src/lib/git.mjs +14 -1
- package/viewer/index.html +9 -2
package/README.md
CHANGED
|
@@ -162,6 +162,13 @@ jobs:
|
|
|
162
162
|
Uploads go by content hash: captures that did not change since an earlier run
|
|
163
163
|
are not sent again, and an interrupted `share` picks up where it stopped.
|
|
164
164
|
|
|
165
|
+
With the Kaloko GitHub App (Team and up), each pull request also gets one
|
|
166
|
+
`Kaloko` check on its newest commit. It turns green when the runs shared for
|
|
167
|
+
that commit pass, whether they came from this workflow, a coding agent in the
|
|
168
|
+
cloud or a laptop. Branch protection can require it, and labels and
|
|
169
|
+
`/kaloko skip <reason>` control a single pull request. Details:
|
|
170
|
+
`skills/kaloko/reference/ci.md`.
|
|
171
|
+
|
|
165
172
|
## When a screenshot is not enough
|
|
166
173
|
|
|
167
174
|
```bash
|
package/package.json
CHANGED
package/schemas/config.v1.json
CHANGED
|
@@ -136,6 +136,21 @@
|
|
|
136
136
|
"site_contacts": { "type": "boolean", "default": true, "description": "kaloko share takes the site's own contacts for not personal data: e-mails on the environment's domain, e-mails and phone numbers in the page footer, and those of an Organization or ContactPoint in JSON-LD. false checks them like any other value." }
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
|
+
"github": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"additionalProperties": false,
|
|
142
|
+
"description": "The Kaloko check on pull requests (#141). The service reads it from the base branch; it may only make the check stricter than Settings on Kaloko.",
|
|
143
|
+
"properties": {
|
|
144
|
+
"checks": { "enum": ["off", "manual", "auto", "strict"], "description": "manual: only on request (label kaloko:run, /kaloko walk); auto: every pull request, the scenarios its change reaches; strict: every pull request, all scenarios, review required." },
|
|
145
|
+
"scope": { "enum": ["affected", "all"] },
|
|
146
|
+
"require_review": { "type": "boolean", "description": "Also post Kaloko review, which turns green when the runs are accepted." },
|
|
147
|
+
"drafts": { "enum": ["wait", "run"], "description": "wait: no check on a draft pull request until it is ready for review." },
|
|
148
|
+
"skip_authors": { "type": "array", "items": { "type": "string" }, "description": "Only in Settings: skipping authors would loosen the check, so the file's list is ignored." },
|
|
149
|
+
"skip_by": { "enum": ["write", "maintain", "admin"], "description": "The access a person needs to skip a pull request." },
|
|
150
|
+
"author_may_skip": { "type": "boolean" },
|
|
151
|
+
"wait_hours": { "type": "integer", "minimum": 1, "maximum": 336, "description": "strict: hours to wait for a run before the check times out." }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
139
154
|
"share": {
|
|
140
155
|
"type": "object",
|
|
141
156
|
"additionalProperties": false,
|
package/schemas/run.v1.json
CHANGED
|
@@ -116,6 +116,23 @@
|
|
|
116
116
|
"sha": {
|
|
117
117
|
"type": "string",
|
|
118
118
|
"description": "Full commit hash (GitHub check runs need it)."
|
|
119
|
+
},
|
|
120
|
+
"repo": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"description": "owner/name of the origin remote (kaloko 0.18+): the service finds the pull request by commit when pr is missing."
|
|
123
|
+
},
|
|
124
|
+
"host": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"enum": ["github", "gitlab", "other"],
|
|
127
|
+
"description": "Where the origin remote lives."
|
|
128
|
+
},
|
|
129
|
+
"prefix": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "The project's folder inside the repository (monorepos), without a trailing slash."
|
|
132
|
+
},
|
|
133
|
+
"dirty": {
|
|
134
|
+
"type": "boolean",
|
|
135
|
+
"description": "Tracked files differed from the commit when the run started: the run does not count for the commit's GitHub check."
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
},
|
package/skills/kaloko/SKILL.md
CHANGED
|
@@ -168,6 +168,10 @@ only when a person says that value may be uploaded.
|
|
|
168
168
|
[reference/recording.md](reference/recording.md)
|
|
169
169
|
- CI: a workflow that walks each pull request's preview deployment and
|
|
170
170
|
comments on it (`kaloko ci github|gitlab`): [reference/ci.md](reference/ci.md)
|
|
171
|
+
- The `Kaloko` check on a pull request, what makes it pass, `kaloko ci plan`,
|
|
172
|
+
labels and `/kaloko` commands, and walking from a coding agent in the cloud
|
|
173
|
+
(commit first, a dirty tree does not count; never skip the check yourself):
|
|
174
|
+
[reference/ci.md#the-kaloko-check-on-pull-requests](reference/ci.md#the-kaloko-check-on-pull-requests)
|
|
171
175
|
- Environments, access (gates, accounts, sign-in), e-mails, readonly and
|
|
172
176
|
production rules, and what is yours to arrange for the project's stack:
|
|
173
177
|
[reference/environments.md](reference/environments.md)
|
|
@@ -30,6 +30,10 @@ the deployment):
|
|
|
30
30
|
`walk --ephemeral --affected` and `share --pr` (one comment per scenario,
|
|
31
31
|
updated on later pushes).
|
|
32
32
|
|
|
33
|
+
Before the loop, the workflow runs `kaloko ci plan` (see
|
|
34
|
+
[The Kaloko check on pull requests](#the-kaloko-check-on-pull-requests)), so
|
|
35
|
+
the check on the pull request knows which scenarios to wait for.
|
|
36
|
+
|
|
33
37
|
`kaloko ci` also adds the `preview` environment to `kaloko.config.yml` with
|
|
34
38
|
`base_url: ${KALOKO_PREVIEW_URL}`. Scenarios that list `environments:` must
|
|
35
39
|
include `preview`, or `start` refuses them; the command says which.
|
|
@@ -67,6 +71,85 @@ in the newest shared run of the scenario count too, so a fresh CI checkout
|
|
|
67
71
|
maps routes as well as a laptop with earlier runs. See
|
|
68
72
|
[execution.md](execution.md#only-what-changed).
|
|
69
73
|
|
|
74
|
+
## The Kaloko check on pull requests
|
|
75
|
+
|
|
76
|
+
With the Kaloko GitHub App installed (an organization admin: Settings →
|
|
77
|
+
Integrations → GitHub), every pull request in a connected repository gets one
|
|
78
|
+
check named `Kaloko` on its newest commit. It does not walk anything itself:
|
|
79
|
+
it waits for the runs shared for that commit, from a coding agent in the
|
|
80
|
+
cloud, a person's machine or this workflow, and decides from them.
|
|
81
|
+
|
|
82
|
+
| The check says | When |
|
|
83
|
+
|---|---|
|
|
84
|
+
| success | every scenario the change reaches has a run on this commit without failing steps, or nothing reaches a scenario |
|
|
85
|
+
| failure | a run on this commit has a failing step (in strict mode a partial one too) |
|
|
86
|
+
| in progress | a reached scenario has no run on this commit yet; the summary says how to get one |
|
|
87
|
+
| neutral | the repository has no scenario yet, or only code Kaloko cannot map without the repository changed (auto mode) |
|
|
88
|
+
| skipped | someone with the right access skipped it; the summary names them and their reason |
|
|
89
|
+
|
|
90
|
+
Runs of a dirty working tree (tracked files differ from the commit) are
|
|
91
|
+
listed but do not count: commit first, then walk. A new push supersedes the
|
|
92
|
+
old commit's check. The optional `Kaloko review` check follows the people's
|
|
93
|
+
decision (accepted, returned) on those runs.
|
|
94
|
+
|
|
95
|
+
Which scenarios a change reaches: the service maps the pull request's files
|
|
96
|
+
onto the steps' paths and captured URLs, like `kaloko affected` without the
|
|
97
|
+
repository. `kaloko ci plan` sends what `kaloko affected` found (it follows
|
|
98
|
+
imports and `affects:` globs), which wins:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
kaloko ci plan # every scenario of the config, for HEAD
|
|
102
|
+
kaloko ci plan --scenario qa/flows/checkout.yml --none # this commit reaches nothing there
|
|
103
|
+
kaloko ci plan --dry-run --json # show it, send nothing
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Modes (Settings, per repository): `off`, `manual` (only after the label
|
|
107
|
+
`kaloko:run` or the comment `/kaloko walk`), `auto` (the default) and
|
|
108
|
+
`strict` (all scenarios, review required, unknown impact waits, the check
|
|
109
|
+
times out after `wait_hours`). The `github:` section of `kaloko.config.yml`
|
|
110
|
+
on the base branch may make it stricter, never looser:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
github:
|
|
114
|
+
checks: strict # off | manual | auto | strict
|
|
115
|
+
scope: all # affected | all
|
|
116
|
+
require_review: true
|
|
117
|
+
drafts: run # wait (default) | run
|
|
118
|
+
skip_by: maintain # write | maintain | admin
|
|
119
|
+
author_may_skip: false
|
|
120
|
+
wait_hours: 12
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
One pull request: the labels `kaloko:skip` (whole pull request), `kaloko:all`,
|
|
124
|
+
`kaloko:review`, `kaloko:run`; the comments `/kaloko skip <reason>` (this
|
|
125
|
+
commit only), `/kaloko walk`, `/kaloko all`; the buttons on the check. Only
|
|
126
|
+
people with the access set in Settings skip, never apps or bots, and every
|
|
127
|
+
skip is on the check and in the audit log. Do not skip a check yourself as an
|
|
128
|
+
agent; walk, or tell the person why it fails.
|
|
129
|
+
|
|
130
|
+
To make it block merging, the repository's branch protection (or ruleset)
|
|
131
|
+
requires the status check `Kaloko`. Kaloko never changes branch protection.
|
|
132
|
+
|
|
133
|
+
### Coding agents in the cloud
|
|
134
|
+
|
|
135
|
+
An agent in a sandbox (Claude Code on the web, Codex, Cursor background
|
|
136
|
+
agents, Copilot) runs the same commands as on a laptop:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git commit -am "…" # walk what is committed
|
|
140
|
+
npx kaloko start --scenario qa/flows/checkout.yml --env preview
|
|
141
|
+
npx kaloko walk --affected
|
|
142
|
+
npx kaloko share # --pr also comments, if gh is signed in
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `KALOKO_TOKEN` goes into the sandbox's secrets or environment variables
|
|
146
|
+
(tester role, upload scope; a token per agent shows who shared what).
|
|
147
|
+
- `npx kaloko browser install --with-deps` belongs in the environment's setup
|
|
148
|
+
script. When the sandbox cannot run a browser or reach the preview, let the
|
|
149
|
+
workflow above walk instead.
|
|
150
|
+
- `gh` is not needed: the run carries the repository and the commit, and
|
|
151
|
+
Kaloko finds the pull request.
|
|
152
|
+
|
|
70
153
|
## Exit codes
|
|
71
154
|
|
|
72
155
|
| Code | Meaning |
|
package/src/cli.mjs
CHANGED
|
@@ -127,6 +127,10 @@ Project
|
|
|
127
127
|
In CI: the preview URL (KALOKO_PREVIEW_URL, the deployment event, provider
|
|
128
128
|
variables or the commit's GitHub deployments), the pull request and its base;
|
|
129
129
|
--wait polls that long for the deployment (seconds, or 10m)
|
|
130
|
+
ci plan [--scenario a.yml,b.yml] [--base ref] [--none] [--repo owner/name] [--sha commit] [--dry-run] [--json]
|
|
131
|
+
Tell the GitHub check which scenarios this commit reaches (kaloko affected, which
|
|
132
|
+
follows imports): it then waits for runs of those and passes for the rest;
|
|
133
|
+
--none marks the scenarios as not reached
|
|
130
134
|
login [--org <slug>] [--url <service>] [--print] [--no-open]
|
|
131
135
|
Sign this machine in through the browser: approve the code on kaloko.app, choose
|
|
132
136
|
the organization and what the token may do; the token (at most 30 days) goes to
|
|
@@ -1776,7 +1780,31 @@ const commands = {
|
|
|
1776
1780
|
console.log(preview.url)
|
|
1777
1781
|
return
|
|
1778
1782
|
}
|
|
1779
|
-
if (
|
|
1783
|
+
if (action === 'plan') {
|
|
1784
|
+
// #141: what kaloko affected finds for this commit (it follows imports and affects: globs) decides the GitHub
|
|
1785
|
+
// check over the service's guess from the file list; a scenario nothing reaches lets the check pass
|
|
1786
|
+
const { config, root } = await context(args)
|
|
1787
|
+
const g = await gitMeta(root)
|
|
1788
|
+
const repo = typeof args.repo === 'string' ? args.repo : (g?.host === 'github' ? g.repo : null) || process.env.GITHUB_REPOSITORY || null
|
|
1789
|
+
const sha = typeof args.sha === 'string' ? args.sha.toLowerCase() : g?.sha
|
|
1790
|
+
if (!repo || !/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new UserError('kaloko ci plan needs the GitHub repository: run it in a clone whose origin is on GitHub, or pass --repo owner/name.')
|
|
1791
|
+
if (!sha || !/^[0-9a-f]{40}$/.test(sha)) throw new UserError('kaloko ci plan needs the full commit: run it inside the repository, or pass --sha.')
|
|
1792
|
+
const targets = list(args.scenario)?.map((f) => resolve(f)) || await scenarioFiles(config, root)
|
|
1793
|
+
if (!targets.length) throw new UserError('No scenario: pass --scenario <file> or list scenarios in the config.')
|
|
1794
|
+
const scenarios = []
|
|
1795
|
+
for (const file of targets) {
|
|
1796
|
+
const scenario = await loadScenario(file)
|
|
1797
|
+
if (args.none) { scenarios.push({ id: scenario.id, affected: false }); continue }
|
|
1798
|
+
const r = await affectedSelection(args, { config, root, scenario, scenarioFile: file })
|
|
1799
|
+
scenarios.push({ id: scenario.id, affected: !r.sel.none, steps: r.sel.none ? [] : selectedIds(scenario, r.sel) })
|
|
1800
|
+
}
|
|
1801
|
+
if (!args['dry-run']) await service(config).request('POST', `/orgs/${config.org}/github/plan`, { repo, sha, scenarios })
|
|
1802
|
+
if (args.json) { say(JSON.stringify({ repo, sha, scenarios, sent: !args['dry-run'] }, null, 2)); return }
|
|
1803
|
+
for (const x of scenarios) say(`${x.affected ? '→' : '·'} ${x.id}: ${x.affected ? `walk ${x.steps.length} step(s)` : 'nothing this change reaches'}`)
|
|
1804
|
+
say(args['dry-run'] ? 'Not sent (--dry-run).' : `Sent to Kaloko for ${repo}@${sha.slice(0, 7)}: the GitHub check expects runs of the scenarios marked →.`)
|
|
1805
|
+
return
|
|
1806
|
+
}
|
|
1807
|
+
if (!['github', 'gitlab'].includes(action)) throw new UsageError('Usage: kaloko ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected] [--out file] [--print] [--force]\n kaloko ci github --release [--snippet] [--print] [--force]\n kaloko ci preview-url [--wait 600] [--github-env] [--dotenv file]\n kaloko ci plan [--scenario a.yml,b.yml] [--base ref] [--none] [--repo owner/name] [--sha commit] [--dry-run] [--json]')
|
|
1780
1808
|
const { config, root, file } = await context(args)
|
|
1781
1809
|
if (args.release) {
|
|
1782
1810
|
// release-tool integrations: record each published release on Kaloko (docs/DOCS-CHANGELOG.md)
|
|
@@ -60,11 +60,29 @@ export function formatStepRef ({ scenario = null, step, locale = null, viewport
|
|
|
60
60
|
export function findStepRefs (markdown) {
|
|
61
61
|
const out = []
|
|
62
62
|
const re = /!\[([^\]]*)\]\((kaloko:[^)\s]+)\)/g
|
|
63
|
+
const text = withoutCode(String(markdown || ''))
|
|
63
64
|
let m
|
|
64
|
-
while ((m = re.exec(
|
|
65
|
+
while ((m = re.exec(text))) out.push({ alt: m[1], ref: m[2], parsed: parseStepRef(m[2]), index: m.index })
|
|
65
66
|
return out
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** The Markdown with fenced code blocks and inline code blanked out (same length, newlines kept, so indexes still
|
|
70
|
+
* point into the original): a reference shown as an example in a guide is text, not a picture to resolve. */
|
|
71
|
+
export function withoutCode (md) {
|
|
72
|
+
const blank = (s) => s.replace(/[^\n]/g, ' ')
|
|
73
|
+
const lines = md.split('\n')
|
|
74
|
+
let fence = null
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const f = lines[i].match(/^ {0,3}(?:[-*+]\s+|\d+[.)]\s+|>\s*)*(`{3,}|~{3,})/) || lines[i].match(/^\s*(`{3,}|~{3,})/)
|
|
77
|
+
if (fence) {
|
|
78
|
+
if (f && f[1][0] === fence[0] && f[1].length >= fence.length && !lines[i].trim().slice(f[1].length).trim()) fence = null
|
|
79
|
+
lines[i] = blank(lines[i])
|
|
80
|
+
} else if (f) { fence = f[1]; lines[i] = blank(lines[i]) }
|
|
81
|
+
}
|
|
82
|
+
// a code span stays inside its paragraph: a stray backtick never pairs across a blank line
|
|
83
|
+
return lines.join('\n').replace(/(?<!`)(`+)(?!`)(?:(?!\n[ \t]*\n)[\s\S])+?(?<!`)\1(?!`)/g, blank)
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
// ---------- versions ----------
|
|
69
87
|
|
|
70
88
|
/** "v2.4.0" → "2.4.0"; anything else stays as it is (trimmed). */
|
package/src/lib/ci.mjs
CHANGED
|
@@ -99,9 +99,11 @@ export function runner ({ pm = 'npm', dependency = true, version = 'latest' }) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** The loop over scenarios both workflows run: walk what the change reaches, share, comment on the PR. */
|
|
102
|
-
function walkScript ({ scenarios, env, affected, k }) {
|
|
102
|
+
function walkScript ({ scenarios, env, affected, k, plan = false }) {
|
|
103
103
|
return [
|
|
104
104
|
'status=0',
|
|
105
|
+
// the Kaloko check on the pull request (GitHub App): which scenarios this commit reaches, followed through imports
|
|
106
|
+
...(plan && affected ? [`${k} ci plan --scenario ${scenarios.map(q).join(',')} || echo "kaloko ci plan did not reach Kaloko; the GitHub check guesses from the changed files."`] : []),
|
|
105
107
|
`for s in ${scenarios.map(q).join(' ')}; do`,
|
|
106
108
|
...(affected ? [` if ! ${k} affected --scenario "$s" --fail-if-none; then echo "Nothing in $s is affected; skipped."; continue; fi`] : []),
|
|
107
109
|
` ${k} start --scenario "$s" --env ${env}`,
|
|
@@ -159,7 +161,7 @@ ${install ? `${['pnpm', 'yarn'].includes(pm) ? ' - run: corepack enable\n'
|
|
|
159
161
|
run: ${k} ci preview-url --wait 600 --github-env
|
|
160
162
|
- name: Walk, share, comment
|
|
161
163
|
run: |
|
|
162
|
-
${indent(walkScript({ scenarios, env, affected, k }), 10)}
|
|
164
|
+
${indent(walkScript({ scenarios, env, affected, k, plan: true }), 10)}
|
|
163
165
|
`
|
|
164
166
|
}
|
|
165
167
|
|
package/src/lib/git.mjs
CHANGED
|
@@ -90,5 +90,18 @@ export async function gitMeta (cwd) {
|
|
|
90
90
|
pr = { number: p.number, url: p.url, state: String(p.state).toLowerCase(), ...(p.title ? { title: String(p.title).slice(0, 300) } : {}) }
|
|
91
91
|
} catch {}
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
// #141: the repository from the origin remote (a sandbox without gh has no PR; the service finds it by commit), the
|
|
94
|
+
// project's folder inside it (monorepos) and whether tracked files differ from the commit (such a walk does not
|
|
95
|
+
// count for the commit's GitHub check)
|
|
96
|
+
const [origin, prefix, changes] = await Promise.all([
|
|
97
|
+
repoSlug(cwd),
|
|
98
|
+
out('git', ['rev-parse', '--show-prefix'], cwd),
|
|
99
|
+
out('git', ['status', '--porcelain', '--untracked-files=no'], cwd)
|
|
100
|
+
])
|
|
101
|
+
return {
|
|
102
|
+
branch: branch === 'HEAD' && ciBranch ? ciBranch : branch, commit, sha, message, author, pr,
|
|
103
|
+
...(origin ? { repo: origin.slug, host: origin.host } : {}),
|
|
104
|
+
...(prefix ? { prefix: prefix.replace(/\/$/, '') } : {}),
|
|
105
|
+
dirty: changes === null ? false : changes.length > 0
|
|
106
|
+
}
|
|
94
107
|
}
|
package/viewer/index.html
CHANGED
|
@@ -3973,7 +3973,13 @@ function applyHash () {
|
|
|
3973
3973
|
}
|
|
3974
3974
|
|
|
3975
3975
|
/* ---------- replay: guided walkthrough with branch choice ---------- */
|
|
3976
|
-
|
|
3976
|
+
// the ways on from a step in the order of the map: the main path before alternatives, forward before back, the same
|
|
3977
|
+
// row before another, then the nearest column and row, so → in replay follows the diagram, not the order of the file
|
|
3978
|
+
const outEdges = (id) => {
|
|
3979
|
+
const from = byId(id); const c = (n) => n?.col || 0, l = (n) => n?.lane || 0;
|
|
3980
|
+
const k = (e) => { const t = byId(e.to); return [e.kind === 'alt' ? 1 : 0, c(t) > c(from) ? 0 : 1, l(t) === l(from) ? 0 : 1, Math.abs(c(t) - c(from)), Math.abs(l(t) - l(from))]; };
|
|
3981
|
+
return RUN.edges.filter((e) => e.from === id && byId(e.to)).map((e, i) => [e, k(e), i]).sort(([, a, i], [, b, j]) => { for (let x = 0; x < a.length; x++) if (a[x] !== b[x]) return a[x] - b[x]; return i - j; }).map(([e]) => e);
|
|
3982
|
+
};
|
|
3977
3983
|
function startNode () { const order = RUN.nodes.slice().sort((a, b) => a.col - b.col || a.lane - b.lane); return order.find((n) => !RUN.edges.some((e) => e.to === n.id)) || order[0]; }
|
|
3978
3984
|
function rpOpen (path) {
|
|
3979
3985
|
const ids = (path || []).filter((id) => byId(id));
|
|
@@ -4035,7 +4041,8 @@ function renderReplay () {
|
|
|
4035
4041
|
$('rpClose').onclick = rpClose; $('rpPrev').onclick = rpBack;
|
|
4036
4042
|
$('rp').querySelectorAll('[data-go]').forEach((b) => { b.onclick = () => rpGo(b.dataset.go); });
|
|
4037
4043
|
$('rp').querySelectorAll('[data-jump]').forEach((b) => { b.onclick = () => rpJump(Number(b.dataset.jump)); });
|
|
4038
|
-
|
|
4044
|
+
// the stack's index opens over the canvas, not under the replay (which would hide it and take its keys)
|
|
4045
|
+
$('rp').querySelectorAll('[data-stack-open]').forEach((b) => { b.onclick = () => { const id = b.dataset.stackOpen; rpClose(); openStack(id); }; });
|
|
4039
4046
|
$('rp').querySelector('[data-restart]')?.addEventListener('click', () => rpOpen(null));
|
|
4040
4047
|
if ($('rpVid')) $('rpVid').onclick = () => { S.rpVideo = !S.rpVideo; renderReplay(); };
|
|
4041
4048
|
if (S.rpVideo) {
|