kaloko 0.8.0 → 0.10.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.
Files changed (43) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/schemas/config.v1.json +2 -0
  4. package/schemas/revision.v1.json +11 -0
  5. package/schemas/run.v1.json +12 -3
  6. package/schemas/scenario.v1.json +14 -0
  7. package/skills/kaloko/reference/design.md +93 -1
  8. package/skills/kaloko/reference/execution.md +6 -0
  9. package/skills/kaloko/reference/portable-html.md +12 -3
  10. package/skills/kaloko/reference/stacks.md +6 -0
  11. package/src/cli.mjs +119 -47
  12. package/src/lib/access.mjs +1 -1
  13. package/src/lib/apps/drivers.mjs +1 -1
  14. package/src/lib/apps/webview.mjs +3 -3
  15. package/src/lib/build.mjs +9 -46
  16. package/src/lib/capture.mjs +1 -1
  17. package/src/lib/changelog.mjs +2 -2
  18. package/src/lib/design-cli.mjs +121 -47
  19. package/src/lib/design-core.mjs +26 -4
  20. package/src/lib/design-export.mjs +28 -8
  21. package/src/lib/design-lint.mjs +45 -11
  22. package/src/lib/design-origins.mjs +86 -0
  23. package/src/lib/design.mjs +112 -8
  24. package/src/lib/dimensions.mjs +66 -12
  25. package/src/lib/doctor.mjs +72 -10
  26. package/src/lib/drift.mjs +1 -1
  27. package/src/lib/evaluate.mjs +7 -3
  28. package/src/lib/figma.mjs +1 -1
  29. package/src/lib/integrations-cli.mjs +3 -3
  30. package/src/lib/login.mjs +1 -1
  31. package/src/lib/mail.mjs +1 -1
  32. package/src/lib/manifest-core.mjs +90 -5
  33. package/src/lib/run.mjs +1 -1
  34. package/src/lib/service.mjs +39 -9
  35. package/src/lib/spec-cli.mjs +7 -4
  36. package/src/lib/stability.mjs +1 -1
  37. package/src/lib/stack.mjs +16 -9
  38. package/src/lib/tokens.mjs +16 -0
  39. package/src/lib/util.mjs +6 -0
  40. package/src/lib/video.mjs +11 -2
  41. package/src/lib/vision.mjs +1 -1
  42. package/src/lib/walk-dimensions.mjs +31 -6
  43. package/viewer/index.html +249 -48
package/README.md CHANGED
@@ -182,7 +182,9 @@ npx kaloko import walkthrough/ --adhoc # a Cursor or Antigravity walkthroug
182
182
  lays screenshots, Playwright reports and traces out as a run: matched to the
183
183
  scenario's steps (by step id in `steps.json`, URL or title), or as an ad-hoc
184
184
  flow when there is no scenario. Build and share it like any run. When something does not work (Chrome,
185
- token, environment URL, evaluator key), `npx kaloko doctor` says what to fix.
185
+ token, environment URL, evaluator key), `npx kaloko doctor` says what to fix, and
186
+ `npx kaloko doctor --fix` installs a missing browser, creates missing folders and fills in `org:` from your token.
187
+ Every error the CLI prints says what to do next; if you are still stuck, write to support@sinfin.cz.
186
188
 
187
189
  ## Review only what changed
188
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaloko",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
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",
@@ -193,7 +193,9 @@
193
193
  "kind": { "enum": ["app", "design"], "default": "app", "description": "app: the application under test (a web app at base_url, or a native app in the app block). design: a folder of live HTML steps served locally by the CLI (serve); its runs are published as design versions (#42)." },
194
194
  "serve": { "type": "string", "description": "kind: design. Folder with one sub-folder per step (<step>/index.html, optional <step>/<locale>/index.html) and shared files in _shared/; relative to the config." },
195
195
  "fixtures": { "type": "string", "description": "kind: design. Folder of JSON fixtures live steps may fetch instead of real APIs; relative to the config." },
196
+ "origins": { "type": "array", "maxItems": 20, "items": { "type": "string", "pattern": "^https://(\\*\\.)?[A-Za-z0-9.-]+(:[0-9]{1,5})?/?$" }, "description": "kind: design. Origins the design loads fonts, stylesheets and images from (https://fonts.googleapis.com, https://*.cdn.example.com). The organization allows them in Settings → Security; kaloko design lint warns about one it does not allow. Scripts never load from them." },
196
197
  "tokens_mode": { "type": "string", "description": "kind: design. Token mode (e.g. light, dark) steps are rendered in; default: the token set's first mode." },
198
+ "token_modes": { "oneOf": [{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$" } }, { "const": false }], "description": "kind: design. Token modes of the token set to render every step in, one capture per mode (keys <viewport>.mode-<name>); tokens_mode is the default and has the plain key. Not set: every mode of the set when it has two or more, except a set of only light and dark (left to color_schemes). false: only tokens_mode." },
197
199
  "html_view": { "type": "boolean", "default": false, "description": "Experimental (#42): keep a static HTML view of every capture (assets rehosted, scripts removed, secrets scrubbed). Refused on production unless the environment is readonly." },
198
200
  "base_url": {
199
201
  "oneOf": [
@@ -12,6 +12,12 @@
12
12
  "state": { "type": "string", "default": "default", "description": "default, a design state (empty, error, …) or view:<viewport> for an HTML view." },
13
13
  "kind": { "enum": ["design", "html_view"], "default": "design" },
14
14
  "entry": { "type": "string" },
15
+ "items": {
16
+ "type": ["array", "null"],
17
+ "description": "Stack steps (#61): the variants of the step, each a folder of the step with its own page (<step>/<id>/index.html). The revision covers the whole step folder; its entry is the overview page or the first variant's.",
18
+ "maxItems": 200,
19
+ "items": { "type": "object", "required": ["id", "entry"], "properties": { "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$" }, "entry": { "type": "string" } } }
20
+ },
15
21
  "files": { "$ref": "bundle.v1.json#/properties/files" },
16
22
  "tokens": { "type": ["object", "null"], "properties": { "project": { "type": ["string", "null"] }, "version": { "type": ["integer", "null"] }, "hash": { "type": ["string", "null"] } } },
17
23
  "based_on": { "type": ["string", "null"], "description": "The revision this one changed (a canvas edit, a sync on top of the draft)." },
@@ -31,6 +37,11 @@
31
37
  "mode": { "type": "string" }
32
38
  }
33
39
  }
40
+ },
41
+ "item_captures": {
42
+ "type": "object",
43
+ "description": "Stack steps (#61): item id → viewport → rendered capture of that variant, shaped like captures.",
44
+ "additionalProperties": { "type": "object", "additionalProperties": { "$ref": "#/properties/captures/additionalProperties" } }
34
45
  }
35
46
  }
36
47
  }
@@ -156,11 +156,11 @@
156
156
  },
157
157
  "viewports": {
158
158
  "type": "object",
159
- "description": "Capture key -> [width, height]. A key is the viewport name plus the dimension values that differ from the default, joined by dots (mobile, mobile.webkit, iphone-15.webkit.dark.reduce)."
159
+ "description": "Capture key -> [width, height]. A key is the viewport name plus the dimension values that differ from the default, joined by dots (mobile, mobile.webkit, iphone-15.webkit.dark.reduce). Token modes of a design add mode-<name>, step states state-<name> as the last part (mobile.mode-brand-b, mobile.dark.state-empty)."
160
160
  },
161
161
  "variants": {
162
162
  "type": "object",
163
- "description": "Capture key -> { viewport, browser, color_scheme?, reduced_motion?, forced_colors?, display_mode?, device? }. Absent in runs without dimensions: every key is then a plain viewport in Chromium.",
163
+ "description": "Capture key -> { viewport, browser, color_scheme?, reduced_motion?, forced_colors?, display_mode?, token_mode?, step_state?, device? }. Absent in runs without dimensions: every key is then a plain viewport in Chromium.",
164
164
  "additionalProperties": {
165
165
  "type": "object",
166
166
  "properties": {
@@ -170,13 +170,15 @@
170
170
  "reduced_motion": { "enum": ["no-preference", "reduce"] },
171
171
  "forced_colors": { "enum": ["none", "active"] },
172
172
  "display_mode": { "enum": ["browser", "standalone"] },
173
+ "token_mode": { "type": "string", "description": "Token mode of the design's token set the key was rendered in." },
174
+ "step_state": { "type": "string", "description": "Step state (default, empty, error, loading…) of the key." },
173
175
  "device": { "type": "string" }
174
176
  }
175
177
  }
176
178
  },
177
179
  "dimensions": {
178
180
  "type": "object",
179
- "description": "Values of each dimension the run walked (browsers, color_schemes, reduced_motion, forced_colors, display_modes) for the canvas switchers."
181
+ "description": "Values of each dimension the run walked (browsers, color_schemes, reduced_motion, forced_colors, display_modes, token_modes with the default mode first, states with default first) for the canvas switchers. A step that has no such state has no captures under its keys."
180
182
  },
181
183
  "devices": {
182
184
  "type": "object",
@@ -463,6 +465,8 @@
463
465
  "reduced_motion": { "type": "string" },
464
466
  "forced_colors": { "type": "string" },
465
467
  "display_mode": { "type": "string" },
468
+ "token_mode": { "type": "string", "description": "Token mode a design capture was rendered in." },
469
+ "step_state": { "type": "string", "description": "Step state of the capture (default, empty, error…)." },
466
470
  "device_preset": { "type": "string", "description": "Viewport name of the device preset the capture emulated." },
467
471
  "device": { "type": ["string", "null"] },
468
472
  "tree": { "type": ["string", "null"], "description": "UI tree of an app capture (qawalk.tree.v1)." },
@@ -562,6 +566,11 @@
562
566
  "type": "object",
563
567
  "description": "Page metadata per locale (meta, links, JSON-LD, microdata, headings)."
564
568
  },
569
+ "states": {
570
+ "type": "array",
571
+ "items": { "type": "string" },
572
+ "description": "Step states the step is captured in (default first), when its scenario step lists states."
573
+ },
565
574
  "session": {
566
575
  "type": [
567
576
  "string",
@@ -68,6 +68,7 @@
68
68
  "reduced_motion": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["no-preference", "reduce"] }, "description": "prefers-reduced-motion values (keys <viewport>.reduce)." },
69
69
  "forced_colors": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["none", "active"] }, "description": "forced-colors (Windows high contrast) values (keys <viewport>.forced)." },
70
70
  "display_modes": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["browser", "standalone"] }, "description": "PWA display modes: standalone captures the page as an installed app (display-mode media rules and matchMedia; the whole screen of a device preset)." },
71
+ "token_modes": { "oneOf": [{ "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$" } }, { "const": false }], "description": "Design environments: token modes of the token set to render every step in (keys <viewport>.mode-<name>). Not set: every mode of the set (see the environment's token_modes). false: only the environment's tokens_mode." },
71
72
  "devices": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" }, "description": "App scenarios: devices to walk on, e.g. pixel-8-api-34, iphone-15-ios-18 (emulators, simulators or real devices named in the environment's app.<platform>.devices). One capture key per device." },
72
73
  "environments": {
73
74
  "type": "array",
@@ -311,6 +312,13 @@
311
312
  ],
312
313
  "description": "For `kaloko walk --affected`: globs of repository files (relative to the config) that shape this step, next to the routes and imports Kaloko finds itself; `always` walks the step on every affected walk (sign-in, consent)."
313
314
  },
315
+ "states": {
316
+ "type": "array",
317
+ "minItems": 1,
318
+ "uniqueItems": true,
319
+ "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$", "not": { "enum": ["index"] } },
320
+ "description": "States this step is captured in besides the default one, e.g. [empty, error, loading]; any name works. Each state is one more capture per viewport (keys <viewport>.state-<name>) and a value of the State switcher on the canvas. Design steps render <step>/<state>.html (or <step>/<locale>/<state>.html), else <step>/index.html?state=<name>; walks of web and app steps run the step script once more per state with the argument state. `default` may be listed; it is always captured."
321
+ },
314
322
  "page": {
315
323
  "enum": ["landing", "work", "list", "settings", "form", "doc"],
316
324
  "description": "What kind of page this step is (see the scenario's page); the usability pack asks per type, e.g. one primary call to action only on landing pages."
@@ -542,6 +550,12 @@
542
550
  "minItems": 1,
543
551
  "items": { "type": "string" },
544
552
  "description": "Environments where this criterion applies, e.g. [production] for hreflang to the live domain, the live demo or the sitemap. On any other environment the walk reports it as not applicable (not_evaluated with na: environment) and it does not make the step partial."
553
+ },
554
+ "states": {
555
+ "type": "array",
556
+ "minItems": 1,
557
+ "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$" },
558
+ "description": "Step states this criterion applies to, e.g. [empty] for \"says the cart is empty\". Captures of other states report it as not applicable (na: state). Not set: every state of the step."
545
559
  }
546
560
  }
547
561
  },
@@ -37,6 +37,11 @@ design/
37
37
  images, fonts, module imports, `fetch('./fixtures/x.json')`). Links to other steps are not files it needs.
38
38
  - Live steps run sandboxed on their own origin with `connect-src 'self'`: no real APIs. Use JSON fixtures in
39
39
  the bundle.
40
+ - Web fonts and images from a service (Google Fonts, Adobe Fonts, the company CDN) load only from origins an
41
+ organization admin allows in Settings → Security. List the ones the design uses on the environment
42
+ (`design: { kind: design, serve: design, origins: [https://fonts.googleapis.com, https://fonts.gstatic.com] }`);
43
+ `kaloko design lint` warns about each the organization does not allow yet, so tell the person to ask an admin.
44
+ Scripts never load from those origins.
40
45
  - Links between steps are plain relative links to the page: `<a href="../payment/index.html">`. The canvas
41
46
  follows them; `data-kaloko-go="payment"` (or `data-qawalk-go`) moves to that step whatever the `href` says.
42
47
  - Reference token values as CSS custom properties named after the token path (`color.accent` →
@@ -48,6 +53,40 @@ design/
48
53
  `kaloko design lint` checks the folder; `kaloko sync` runs it first and stops on errors (things that break on
49
54
  the live step), warnings are printed.
50
55
 
56
+ ## Variants of one template (stack steps)
57
+
58
+ A step that comes in variants of one template (an e-mail per reason support can pick, a landing per city, a card per
59
+ plan) is one stack step ([stacks.md](stacks.md)), not one step per variant in a lane of its own:
60
+
61
+ ```yaml
62
+ - id: credit
63
+ kind: stack
64
+ title: Credit from support
65
+ items: { urls: [refund, late, damaged] } # folders of the step; without folder names: every subfolder with a page
66
+ fields: [{ id: reason, label: Reason, from: .reason }]
67
+ criteria: [...] # every variant meets them
68
+ ```
69
+
70
+ ```
71
+ design/credit/index.html optional overview (the step's entry; links into the variants)
72
+ design/credit/refund/index.html one folder per variant: its page and the files next to it
73
+ design/credit/refund/cs/index.html a locale override of one variant
74
+ design/credit/late/index.html
75
+ ```
76
+
77
+ - `items.urls` that name folders (`refund`, `./refund/`, `refund/index.html`) list the variants in that order. App
78
+ paths (`/credit?reason=refund`), other sources or no list: every subfolder with an `index.html` is a variant,
79
+ sorted by name. Locale folders, `dark/` and folders starting with `_` are never variants.
80
+ - One revision covers the whole step folder; changing a variant is a new revision of the step. The walk captures
81
+ every variant in every viewport (`items.viewports` narrows it) with the index fields and criteria, and reuses
82
+ captures per revision and variant.
83
+ - `kaloko sync` sends the variants' captures with the step; the published version shows the stack index with every
84
+ variant and its verdict, and each variant opens live in the lightbox.
85
+ - `kaloko design lint` reports a variant folder without its `index.html` as an error.
86
+ - `kaloko export --live` and `export --design` write the variant folders; the design export adds
87
+ `screenshots/<step>/<variant>/<locale>/<viewport>.webp`.
88
+ - Not for steps whose variants are different screens of a flow: those stay separate steps.
89
+
51
90
  ## Devices, dark mode and app screens
52
91
 
53
92
  Design steps can target device presets and colour schemes like any run ([devices.md](devices.md)):
@@ -68,6 +107,30 @@ color_schemes: [light, dark]
68
107
  step spec and the token check of a dark capture name the dark tokens, and drift compares dark with dark.
69
108
  - Browsers: design steps render in Chromium. Check the implementation in WebKit and Firefox instead.
70
109
 
110
+ ## Step states and token modes
111
+
112
+ A step lists its other states in the scenario (`states: [empty, error, loading]`; any name of letters, digits, `-`
113
+ and `_` works). Draw each one so it can be reviewed and built:
114
+
115
+ - A state is a page next to the step's page: `<step>/<state>.html`, per locale `<step>/<locale>/<state>.html`. It
116
+ travels with the step's revision like the dark page; give it its own fixtures (`<step>/empty.json`, or a literal
117
+ `fetch('./fixtures/cart-empty.json')`) so the closure finds them.
118
+ - Without that page the walk opens `<step>/index.html?state=<name>` and says so. Use it when one page switches its
119
+ states in JS (`new URLSearchParams(location.search).get('state')`); the fixture must still be next to the page or
120
+ fetched by a literal path.
121
+ - Captures of a state are keyed `<viewport>.state-<name>` (`mobile.dark.state-empty`). Criteria with
122
+ `states: [empty]` are judged on that state only; other criteria on every state.
123
+ - The canvas shows a State switcher next to the language; review mode and the grid follow it. A step without the
124
+ state shows its default capture with a note.
125
+
126
+ Token modes: when the token set has two or more modes (resolver contexts or mode groups), every step renders in each
127
+ mode. The walk sets the mode's tokens as custom properties on `:root` after the page's own styles and
128
+ `data-kaloko-mode="<mode>"` on `<html>`; a page that switches themes by class can read the attribute. Captures are
129
+ keyed `<viewport>.mode-<name>`; the environment's `tokens_mode` (else the first mode) is the default with the plain
130
+ key, and the canvas switches modes like colour schemes. A set with only `light`/`dark` modes follows the colour
131
+ scheme instead (`color_schemes: [light, dark]`), unless `token_modes` names them. `token_modes: [brand-a, brand-b]` (environment, scenario or `kaloko start
132
+ --token-modes`) picks modes; `false` turns it off. Live view shows the default mode; other modes are screenshots.
133
+
71
134
  **Native app screens as portable HTML.** A mockup of an iOS or Android screen is a full-screen page:
72
135
 
73
136
  - Size it to the screen, not to a document: `html, body { height: 100% }` and the main area `min-height: 100dvh`.
@@ -121,7 +184,36 @@ opens straight into the presentation of that step: share it with the person who
121
184
  without it, a step never rendered live) the presentation shows the screenshots.
122
185
  - From the local preview and an offline export, the steps open from the run's merged folder (`live/site/`), so plain
123
186
  relative links click through there too.
124
- - A link's query and hash travel to the next step (`../payment/index.html?plan=pro#card`): keep prototype state there.
187
+ - A link's query and hash travel to the next step (`../payment/index.html?plan=pro#card`): enough for one value.
188
+ For more, use the prototype state below.
189
+
190
+ ### Prototype state
191
+
192
+ Steps of a prototype share a small store, so a cart filled on one step shows on the next. The frame helper of the
193
+ live step provides `window.kaloko.state`; the canvas keeps it for the viewer in their browser tab, per draft or
194
+ version, and never sends it to the service. *Start over* in the presentation clears it.
195
+
196
+ ```html
197
+ <!-- payment/index.html: the seeds are what the step shows when nothing set it yet -->
198
+ <script type="application/json" id="seed" data-kaloko-state>{ "cart": [{ "name": "Tea", "price": 120 }] }</script>
199
+ <script>
200
+ const render = (s) => { /* draw s.cart */ }
201
+ // first the step's own seeds, so it looks right alone (from disk, in a capture, in an export)
202
+ render(JSON.parse(document.getElementById('seed').textContent))
203
+ // then what the prototype remembers: the helper loads after the page's scripts and the state comes a moment later
204
+ addEventListener('kaloko:state', (e) => render(e.detail.state))
205
+ </script>
206
+ ```
207
+
208
+ - `kaloko.state.get(key)`, `set(key, value)` (JSON, up to 64 kB; `null` removes the key), `update(key, fn)`,
209
+ `subscribe(fn)` (called with the whole state now and on every change; returns an unsubscribe function), `reset()`,
210
+ `ready` (a promise of the first state). The `kaloko:state` event on `window` carries the same state; scripts that
211
+ run before the helper use it.
212
+ - The seeds in `<script type="application/json" data-kaloko-state>` fill keys that nothing has set yet. Give every
213
+ step that reads state seeds that make it look right on its own: reviewers open steps directly, captures render
214
+ each step alone, and the local preview and exports have no helper, so `window.kaloko` is missing there.
215
+ - Write with `set` or `update` on the step where the person acts (`update('cart', (c) => [...(c || []), item])`),
216
+ read on the next. Keep it small: ids, counts and form values, not page markup.
125
217
 
126
218
  ## What changed in a version
127
219
 
@@ -34,6 +34,12 @@ call `kaloko session reset` themselves when the plan starts as a new visitor.
34
34
  to the node's person (`session.key`, `title`, `mailbox`, `account`, and
35
35
  `credentials` for that account in the run's environment; see
36
36
  [environments.md](environments.md#accounts-and-sign-in)).
37
+ - Step states: a step with `states: [empty, error]` runs its script once
38
+ more per state with `state` (`'default'` first, then `'empty'`, …); bring
39
+ the page into that state (route a mocked API response with `page.route`,
40
+ empty the cart) before the capture. Each run captures that state's keys
41
+ (`mobile.state-empty`). For a step an agent walks:
42
+ `kaloko capture --step <id> --state empty`. App scripts get `state` too.
37
43
  - `agent` — steps that need judgement or change often. Follow
38
44
  `execution.instructions`, then `kaloko capture --step <id>`.
39
45
  - `manual` — a person does it; you capture afterwards.
@@ -19,7 +19,14 @@ warnings, `--json` gives every finding with file, line, rule and fix.
19
19
  `https://`, no protocol-relative `//cdn…`, no paths starting with `/` (on the live origin `/` is the origin root, from
20
20
  disk it is the disk root). Errors: `external-url`, `root-relative`, `outside-folder`, `missing-file`.
21
21
  - **No CDNs.** Download libraries into `_shared/` and link them relatively. Fonts are bundled as **woff2**, subset to
22
- the characters used, with `@font-face` in the shared CSS; Google Fonts links do not load.
22
+ the characters used, with `@font-face` in the shared CSS.
23
+ - **Font and asset origins** are the one exception, for stylesheets, fonts and images only: an organization admin
24
+ allows origins such as `https://fonts.googleapis.com`, `https://fonts.gstatic.com`, `https://use.typekit.net` or
25
+ the company CDN in Settings → Security, and live steps load them. Declare the ones the design uses on the design
26
+ environment (`origins: [https://fonts.googleapis.com, https://fonts.gstatic.com]`); `kaloko design lint` then
27
+ takes them instead of an `external-url` error and warns (`origin-not-allowed`) about each the organization does
28
+ not allow yet. Scripts, module imports, `fetch` and video never load from them. A bundled font still works from
29
+ disk and from an export; prefer it when you can.
23
30
  - Only bundle file types (html, css, js, mjs, json, svg, woff2, woff, png, jpg, webp, avif, gif, mp4, webm, txt).
24
31
  Convert TTF/OTF fonts and other formats (`file-type`).
25
32
  - **Size budget:** a file over 1 MB or a step over 5 MB (with the shared files) warns (`large-file`); 10 MB per file
@@ -40,8 +47,10 @@ warnings, `--json` gives every finding with file, line, rule and fix.
40
47
 
41
48
  ## State and data
42
49
 
43
- - **State lives in the URL**, not in storage: `localStorage`, `sessionStorage`, IndexedDB and cookies throw in the
44
- sandboxed frame and are empty for every other viewer (`storage`). Use `location.hash` or the query
50
+ - **State between steps lives in `window.kaloko.state` or the URL**, not in storage: `localStorage`,
51
+ `sessionStorage`, IndexedDB and cookies throw in the sandboxed frame and are empty for every other viewer
52
+ (`storage`). For a cart, a chosen plan or details filled in on an earlier step use the prototype state
53
+ ([design.md → Prototype state](design.md#prototype-state)); for a single value the query and hash do
45
54
  (`?plan=pro#card`): a link's query and hash travel to the next step in the presentation, and work from disk.
46
55
  Wrap any storage access you keep in `try/catch` with a fallback.
47
56
  - **No network:** real APIs, `XMLHttpRequest`, `WebSocket`, `EventSource` do not connect (`websocket`). Put data in
@@ -126,5 +126,11 @@ two or three example paths), whether the limit cut the list, and that
126
126
  semantic criteria covered only the sample. Put the full list in the CSV or
127
127
  the MCP result rather than in the issue.
128
128
 
129
+ ## Design environments
130
+
131
+ On a design environment a stack step is a template with its variants: each variant is a folder of the step,
132
+ `design/<step>/<variant>/index.html`, listed by `items.urls` or discovered from subfolders, and every variant is
133
+ captured in every viewport. See [design.md](design.md#variants-of-one-template-stack-steps).
134
+
129
135
  Not yet supported for stacks: comparison with a baseline run per page and
130
136
  the documentation layer (latest captures per step).