tosijs-ui 1.14.3 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +737 -0
  2. package/NOTICE +26 -0
  3. package/README.md +7 -1
  4. package/bin/tunnel.ts +17 -4
  5. package/dist/crud.js +14 -0
  6. package/dist/data-table.d.ts +2 -0
  7. package/dist/data-table.js +127 -23
  8. package/dist/doc-browser.js +46 -1
  9. package/dist/doc-system/book-html.d.ts +1 -1
  10. package/dist/doc-system/book-html.js +29 -0
  11. package/dist/doc-system/doc-system-styles.d.ts +11 -0
  12. package/dist/doc-system/doc-system-styles.js +48 -3
  13. package/dist/doc-system/doc-system.js +28 -1
  14. package/dist/doc-system/example-policy.d.ts +42 -0
  15. package/dist/doc-system/example-policy.js +71 -0
  16. package/dist/doc-system/highlight-styles.d.ts +4 -0
  17. package/dist/doc-system/highlight-styles.js +125 -0
  18. package/dist/doc-system/highlight.d.ts +73 -0
  19. package/dist/doc-system/highlight.js +559 -0
  20. package/dist/doc-system/print-window.d.ts +30 -0
  21. package/dist/doc-system/print-window.js +50 -0
  22. package/dist/doc-system/render.js +7 -4
  23. package/dist/doc-system/site/asset-stamp.d.ts +29 -0
  24. package/dist/doc-system/site/asset-stamp.js +67 -0
  25. package/dist/doc-system/site/audit-guard.js +3 -2
  26. package/dist/doc-system/site/check-examples-cli.js +12 -1
  27. package/dist/doc-system/site/check-examples.d.ts +9 -0
  28. package/dist/doc-system/site/check-examples.js +83 -3
  29. package/dist/doc-system/site/dev-auth.d.ts +65 -2
  30. package/dist/doc-system/site/dev-auth.js +81 -8
  31. package/dist/doc-system/site/dev-server.js +40 -5
  32. package/dist/doc-system/site/docs.d.ts +11 -0
  33. package/dist/doc-system/site/docs.js +102 -7
  34. package/dist/doc-system/site/epub.js +70 -13
  35. package/dist/doc-system/site/generate-site.d.ts +6 -0
  36. package/dist/doc-system/site/generate-site.js +7 -3
  37. package/dist/doc-system/site/host-preset.d.ts +45 -4
  38. package/dist/doc-system/site/host-preset.js +61 -6
  39. package/dist/doc-system/site/index.d.ts +1 -0
  40. package/dist/doc-system/site/index.js +12 -0
  41. package/dist/doc-system/site/orchestrator.js +169 -23
  42. package/dist/doc-system/site/site-config.d.ts +54 -1
  43. package/dist/highlight-block.d.ts +26 -0
  44. package/dist/highlight-block.js +154 -0
  45. package/dist/icons.js +11 -0
  46. package/dist/iife.js +163 -131
  47. package/dist/iife.js.map +55 -22
  48. package/dist/index-iife.js +9 -0
  49. package/dist/index.js +11 -1
  50. package/dist/live-example/code-transform.d.ts +14 -0
  51. package/dist/live-example/code-transform.js +22 -0
  52. package/dist/live-example/component.d.ts +20 -0
  53. package/dist/live-example/component.js +49 -4
  54. package/dist/live-example/error-location.d.ts +4 -2
  55. package/dist/live-example/error-location.js +51 -12
  56. package/dist/live-example/execution.js +2 -1
  57. package/dist/live-example/insert-examples.d.ts +13 -0
  58. package/dist/live-example/insert-examples.js +41 -4
  59. package/dist/live-example/save-to-source.d.ts +5 -2
  60. package/dist/live-example/save-to-source.js +31 -11
  61. package/dist/live-example/styles.d.ts +11 -0
  62. package/dist/live-example/styles.js +21 -0
  63. package/dist/live-example/test-harness.d.ts +4 -0
  64. package/dist/live-example/test-harness.js +18 -1
  65. package/dist/schema-form.js +36 -0
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/llms.txt +5 -1
  70. package/package.json +10 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,742 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.15.1
4
+
5
+ ### Doc-test failures reported the wrong line — the same wrong line, every time (#142)
6
+
7
+ Reported by an adopter converting a WebGL project to test fences: two failures in two
8
+ *different* blocks both named `(line 114)`. Reproduced in Chromium — blocks failing at lines 2
9
+ and 10 both reported **131**.
10
+
11
+ Nothing was being recognised as the user's frame, so the first bundle frame won and every
12
+ failure named one constant wrong line. The cause is narrow and unlucky: the only mechanism was
13
+ a list of bundle filenames to skip, it matched a bare `/iife.js`, and a doc site serves
14
+ `/iife.js?v=<hash>` — the cache-busting stamp — which the `$` anchor rejects. It also never
15
+ listed `hydrate.js`, which is the bundle an *adopter's* site actually loads. So on a real doc
16
+ site nothing was skipped at all.
17
+
18
+ The user's frame is now identified **positively**, by the `sourceURL` the code was tagged with,
19
+ which is immune to query strings, to bundle names we did not anticipate, and to whatever an
20
+ adopter calls theirs. The exclusion list survives as a fallback for untagged stacks, now
21
+ matching on the path with any query or fragment stripped. When a tag is expected and no tagged
22
+ frame exists — WebKit routinely produces none — it reports **nothing** rather than guessing.
23
+
24
+ The reporter's framing was right and is worth keeping: *a wrong line is worse than none,
25
+ because it sends you confidently to the wrong place.*
26
+
27
+ ### `toBeCloseTo`, `toBeGreaterThanOrEqual`, `toBeLessThanOrEqual` in doc tests (#142)
28
+
29
+ Also from that report. Everything renderer-shaped is floats — intensities, densities, bounding
30
+ boxes — and without `toBeCloseTo` they were writing `Math.round(x * 1e6) / 1e6` at every
31
+ assertion. Jest's semantics exactly, so the habit transfers: `digits` is decimal places and the
32
+ tolerance is half a unit in the last one. All three compose with `.not`.
33
+
34
+ ## 1.15.0
35
+
36
+ ### `save-to-source` wrote edits into the wrong fence, silently
37
+
38
+ Found by this release's own pre-release review, and it is caused by a feature this release
39
+ introduces — so it could not have shipped ahead of `:static`, and `:static` must not ship
40
+ ahead of it.
41
+
42
+ `example-policy.ts` was added this cycle as **the one rule** for "does this fence become a
43
+ live example", and `insert-examples` was converted to it. Its structural twin was not:
44
+ `save-to-source` kept a flat six-language set *and* its own fence-info regex that captured the
45
+ language as `[\w-]*` — which stops at the colon, so `js:static` read as plain `js` and the
46
+ mode was invisible there while being load-bearing everywhere else.
47
+
48
+ Example ordinals are a **shared coordinate system** between those two modules. They agree only
49
+ if both ask the same question, and after `:static` they did not: every ordinal following a
50
+ display-only fence pointed one group too far, so editing an example saved over a *different*
51
+ block. Silently — the "couldn't locate this example" guard only fires on an out-of-range
52
+ ordinal, and here a group existed at that index, just the wrong one. Under `liveExamples:
53
+ 'opt-in'`, which is the prose and book setting, every ordinal on the page shifts.
54
+
55
+ Fixed at the class rather than the instance: `parseFenceInfo` now joins `isLiveFence` in
56
+ `example-policy.ts`, `render.ts` uses it instead of its own inline copy, and `save-to-source`
57
+ groups with `isLiveFence` and takes the policy from the caller. There is one parser and one
58
+ predicate; a third divergence has nowhere to live.
59
+
60
+ Mutation-verified — restoring either half of the old behaviour turns the regression tests red.
61
+
62
+ ### `<tosi-table>` was silently capped at 10,000 rows in Firefox
63
+
64
+ Found by the release gate, and pre-existing — it fails identically at v1.14.3, verified in a
65
+ worktree rather than assumed.
66
+
67
+ `probeMaxElementHeight` asks for a 1e9-pixel element and measures what the engine gives back,
68
+ on the stated reasoning that *"every engine tried clamps it"*. Firefox does not: above its
69
+ ceiling it returns **0**, which is indistinguishable here from "no layout to probe". So
70
+ `derivedMaxVisibleRows` fell back to the flat 10,000 that #82 exists to replace — on every
71
+ Firefox page, silently, while the feature reported working everywhere else. Measured:
72
+
73
+ ```
74
+ 1e9 → 0 1e8 → 0 1e7 → 10000000 1e6 → 1000000
75
+ ```
76
+
77
+ The probe now halves until the element lays out, which converges on the real ceiling within a
78
+ factor of two on any engine, clamping or not, in at most a dozen forced layouts — once per
79
+ page and cached. Result:
80
+
81
+ | engine | before | after |
82
+ | --- | ---: | ---: |
83
+ | Firefox | 10,000 rows | **520,833** |
84
+ | Chromium | 1,118,480 | 1,118,480 |
85
+ | WebKit | 1,118,480 | 1,118,480 |
86
+
87
+ The test had reimplemented the probe inline — the same single ask, carrying the same wrong
88
+ assumption — so it was testing a copy rather than the component. It now calls the shipped
89
+ function, which is why it caught this at all once the real one was fixed.
90
+
91
+ **Why it hid for so long:** CI runs Chromium only, and the fallback is graceful. Nothing was
92
+ broken, just quietly 52× smaller.
93
+
94
+ ### Guards that could not fail, and a scope that was never stated
95
+
96
+ - **`theme.codeBg` was unguarded.** The WCAG contrast test hardcoded `#fdfdfd`, so it could
97
+ not see the public knob at all — with `codeBg: '#1e1e1e'` the worst light-mode token measures
98
+ **1.71:1** against AA's 4.5, all eleven light rules fail and all eleven dark ones too, and
99
+ the test stayed green throughout. It now reads the background out of the spec, so it asserts
100
+ against what ships. The limit is also stated rather than implied: the palette is eleven fixed,
101
+ contrast-checked literals and **does not track `codeBg`** — override it and you own the
102
+ contrast (retheme the tokens via the `--token-*` variables). There is now a test asserting
103
+ that limit, so a change in scope has to be a deliberate edit rather than a silent one.
104
+ - **The `checkExamples` process boundary had no test.** The example policy crosses it through
105
+ `TOSI_LIVE_EXAMPLES`, and every build we run sends `'auto'` — so `'opt-in'` had never crossed
106
+ a process boundary anywhere, which is precisely where the earlier M2 defect lived. Now covered
107
+ by an integration test that spawns the CLI: the env key, the clean-JSON stdout contract, and
108
+ the default. Mutation-verified against a child that ignores the env value.
109
+ - **The Print path changed twice in one cycle with no test at any tier.** Extracted as
110
+ `printWhenReady` and covered: an already-`complete` window prints without waiting for a `load`
111
+ that will never fire again, a loading one waits, a failing highlight still prints (plain code
112
+ beats a dialog that never opens), and highlighting happens *before* print rather than racing
113
+ it.
114
+
115
+ ### The cache-busting stamp no longer skips an asset in silence
116
+
117
+ `?v=` is computed by hashing the assets it guards, and the loop `continue`d past a missing
118
+ input without a word. That is how `doc-system.css` came to be **named in the inputs and never
119
+ hashed** for the life of the feature: the stylesheet was generated ~80 lines later in a
120
+ ~1270-line function. Measured at the time — the `?v=` on every page equalled
121
+ sha256(hydrate.js + iife.js) exactly. A `theme`-only change, which reaches the CSS generator as
122
+ argv and never enters a bundle, therefore deployed new CSS under an **unchanged URL** and
123
+ returning visitors kept the old stylesheet.
124
+
125
+ The previous fix was to move one statement earlier, with nothing asserting the ordering — so
126
+ the same reorder could undo it and every lane would stay green. `computeAssetStamp` is now its
127
+ own module: a named-but-missing input is reported with the file and the consequence, and the
128
+ properties are unit-tested (changing any input moves the stamp; an unchanged rebuild does not;
129
+ the fallback is used only when nothing exists). Verified end to end — the `?v=` in the built
130
+ site equals sha256 of all three assets.
131
+
132
+ ### One `language-*` pattern, not five
133
+
134
+ The class pattern was written `[A-Za-z0-9_+#-]+` four times in `highlight.ts` and `[\w-]+` once
135
+ in `epub.ts`, so a `c++` or `c#` fence was a language to one pass over a document and plain `c`
136
+ to another. Hoisted into `langOfClass`, with `+` and `#` in the character class deliberately —
137
+ they are real language names. Mutation-verified against the narrower spelling.
138
+
139
+ ### Corrections to the `registerGrammar` surface, before anyone builds on it
140
+
141
+ tjs-lang is writing a TJS Prism grammar against this API (#155), so the companion pieces
142
+ mattered more than their severity suggested.
143
+
144
+ - **`grammarFor` contradicted `highlight()` on registered languages.** It consulted the alias
145
+ table only, so after `registerGrammar('tjs', …)` — the one thing a language author does — it
146
+ still answered `javascript` while `highlight()` correctly used the supplied grammar. It is
147
+ now registration-aware, and documented as returning a grammar *name*.
148
+ - **A false guarantee about print.** The docs said a registered grammar reaches "the
149
+ pre-rendered page, the ePub **and print**". The first two are true; print is not. The
150
+ registry is module state in the *build* process and only `__TOSI_EXAMPLE_POLICY` is stamped
151
+ into the page — so a hard-loaded page is correct (the tokens are already in the HTML) while
152
+ client-side navigation and the in-app Print path re-highlight with an empty registry and fall
153
+ back to `javascript`. Now stated plainly, with the workaround (an ESM consumer can register
154
+ in their `bundleEntry` too) and the remaining gap (a CDN `<script>` consumer cannot).
155
+ - **Two exported types named `ExamplePolicy`** with different value sets — `'auto' | 'opt-in'`
156
+ and `'auto' | 'opt-in' | 'none'` — both reachable through the `./*` wildcard, so which one
157
+ you got depended on the import path. One type now, with `SiteExamplePolicy` expressing the
158
+ site-valid subset as `Exclude<ExamplePolicy, 'none'>` rather than as a second declaration
159
+ that can drift.
160
+
161
+ ### The doc-system registration guard reported green on an inert bundle (#159)
162
+
163
+ `bundleRegistrations` tested `bundleSource.includes('tosi-doc-system')` and its own
164
+ documentation called that *"the one case where grepping a bundle is sound"*. It is not. The tag
165
+ being present is **necessary and not sufficient**, and our own documented API supplies the
166
+ counter-example: the context-map loop `document.querySelectorAll('tosi-doc-system')` puts the
167
+ string into a bundle that registers nothing. The reporter's build reported healthy with exactly
168
+ one `customElements.define` in it, none of it the doc system — and the caller then fell through
169
+ to warn about a *different* component, pointing them away from the defect.
170
+
171
+ The obvious repair is wrong in the other direction, and is worth recording because it looks
172
+ right: matching `define(` beside the quoted tag **false-negatives on our own bundle**.
173
+ `elementCreator()` registers with a variable, so the minified output reads
174
+ `customElements.define(i,this,r)` while the literal sits in a `closest()` call and some CSS
175
+ selectors, nowhere near a `define`.
176
+
177
+ It now looks for evidence of a registration by either route — `preferredTagName="<tag>"` (the
178
+ tosijs path, verified to survive minification in both `dist/iife.js` and a built `hydrate.js`)
179
+ or `define("<tag>"` (a direct call). Checked in both directions against real bundles and
180
+ against the #159 reproduction. The false soundness claim is gone from the shipped `.d.ts`; what
181
+ replaces it says plainly that this is a heuristic which now fails closed on the reported shape,
182
+ rather than claiming to be sound.
183
+
184
+ ### tosijs-ui no longer steals a `Prism` the page already owns
185
+
186
+ Prism is a free-global library — its grammar files register against `window.Prism` at load
187
+ time — so load order decides who owns it and neither side is told. We were importing our copy
188
+ over the top of a host's. Reproduced rather than reasoned about, with a host Prism carrying one
189
+ custom language and one plugin:
190
+
191
+ ```
192
+ ensureGrammar(rust) = true
193
+ globalThis.Prism is host? = false
194
+ host custom lang reachable? = false
195
+ host plugins reachable? = false
196
+ host object itself intact? = true ← their captured reference points at an orphan
197
+ we leaked manual onto global? = true
198
+ ```
199
+
200
+ Nothing errored. Their languages and plugins were simply unreachable, and anything they had
201
+ captured addressed an orphan. The `manual` leak is the worse half: Prism reads that flag at
202
+ load, so a host Prism core loading *after* us computed `manual === true` and **silently never
203
+ ran `highlightAll()`** — us breaking their page, not merely losing a global.
204
+
205
+ Now: we **adopt** a host instance when one exists, never write `manual` onto an instance we did
206
+ not create, and restore the prior global if our own import fails. Every line of that
207
+ reproduction flips. Mutation-verified — deleting the adopt branch or leaking `manual` each
208
+ turns the tests red.
209
+
210
+ This is the #131 hazard class (`@codemirror/state` identity) in a worse container. That one got
211
+ a real remedy — `tosijs-ui/codemirror`, a re-export so only one copy exists — and this one still
212
+ has no equivalent: `registerGrammar` supplies grammars, not instance access. The harm is fixed;
213
+ the asymmetry is recorded in `UPSTREAM.md` and `TODO.md`.
214
+
215
+ The adopter reference states that a page which brought no Prism of its own ends up with ours,
216
+ and that loading your own afterwards wins and stops our highlighting — that is Prism's model,
217
+ not something we can fix from here. Load yours first, or use `registerGrammar` to add a
218
+ language to ours instead of bringing a second copy.
219
+
220
+ ### Published-surface corrections
221
+
222
+ - **`bundleEntry` lost its documentation in the emitted `.d.ts`.** `liveExamples` was added
223
+ between that field's JSDoc and the field itself, and TypeScript attaches a doc comment to
224
+ whatever declaration follows it — so `bundleEntry?: string;` shipped with no comment at all,
225
+ taking the #145 warning with it (the one explaining why omitting the doc-system import gives
226
+ you a site that serves 200s and shows nothing). Reordered, and guarded by a test that reads
227
+ the **emitted** `.d.ts` rather than the source, because the source looked fine throughout.
228
+ - **`<tosi-table>`'s selection docs described pre-1.15 behaviour**, calling `selectRow` /
229
+ `selectRows` internals "documented because they may as well be". They are now a supported
230
+ surface that enforces cardinality and fires `selectionChanged` — and the published page said
231
+ neither. It now also carries the feedback-loop guard `<tosi-crud>` needed, since any consumer
232
+ that both drives and observes selection will hit the same thing.
233
+ - **The README understated the iife by 21%** — "~385KB gzip" against a measured 468KB — and
234
+ named CodeMirror as the only contributor. Prism and its 27 grammars (+31.8KB) are the second,
235
+ with the same cause and the same remedy.
236
+
237
+ ### `<tosi-highlight>` renders in colour anywhere, not only inside a doc site
238
+
239
+ The component shipped without the palette it needs. This package contains **no CSS files at
240
+ all** — every style is a `StyleSheet()` call — and the `.token.*` rules lived inside the
241
+ doc-system stylesheet. So a `<tosi-highlight>` on an ordinary page produced perfectly correct
242
+ `<span class="token …">` markup and rendered it in flat black: 17 token spans, every one
243
+ computing to `rgb(0,0,0)`. That is the single thing the component exists to do.
244
+
245
+ The doc block compounded it by telling you to "import the doc-system CSS" — naming an import
246
+ that did not exist.
247
+
248
+ The palette now lives in `doc-system/highlight-styles.ts` as an exported `highlightStyleSpec`,
249
+ the doc-system spreads that one copy rather than holding its own, and the element **injects it
250
+ on first use** — so the standalone case needs no import and no configuration. Every colour is
251
+ still a `varDefault`, so `:root { --token-keyword: rebeccapurple }` retunes one token type
252
+ without touching the rest.
253
+
254
+ > Writing the test for this found a second defect before it shipped: `StyleSheet()` does not
255
+ > dedupe by id on its own, and `render()` runs many times per element, so the first version
256
+ > injected a `<style>` per render — nine in a single test file. It now carries the same
257
+ > module-level guard as `ensureTooltipStyles`.
258
+
259
+ ### Every "Run this example live" link in an `opt-in` book was dead
260
+
261
+ The ePub generator carried a **third** verbatim copy of the six-language set and
262
+ re-implemented example grouping without consulting `isLiveFence` or `config.liveExamples`.
263
+
264
+ The consequence landed hardest on exactly the people who build ePubs. Under
265
+ `liveExamples: 'opt-in'` — the setting the config docs recommend for book and prose sites —
266
+ the page produces no live examples at all, while the book went on injecting "▶ Run this example
267
+ live ↗" links to every one of them. Every link in the shipped book pointed at an anchor that
268
+ does not exist. With a single `:static` fence the ordinals also slid, so `#example-3` addressed
269
+ a block the page never anchored.
270
+
271
+ A book is the medium where a broken link cannot be corrected after publication, which is what
272
+ moved this from a note to a fix.
273
+
274
+ The ePub now uses the same predicate and the site's own policy, and reads the `:mode` off the
275
+ `<pre>` exactly where `insertExamples` reads it. Its language-class pattern also matched
276
+ `[\w-]+` while the highlighter matched `[A-Za-z0-9_+#-]+`, so a `c++` or `c#` fence was read
277
+ differently by the two; both now use the wider one.
278
+
279
+ The existing test asserted only that the link class was present. It now builds a chapter with a
280
+ `:static` fence ahead of a live one and asserts there is exactly **one** link and no
281
+ `#example-2`. Mutation-verified.
282
+
283
+ ### A gitignored file shipped to npm, and no repo-reading check could have seen it
284
+
285
+ 1.14.2's tarball contained `dist/.metadata_never_index` — a zero-byte macOS Spotlight marker
286
+ that is gitignored and untracked. Harmless in itself. The mechanism is not.
287
+
288
+ `files` includes `/dist` wholesale, and **when `files` is present npm consults neither
289
+ `.gitignore` nor `.npmignore`** for the paths it selects. So anything sitting in `dist/` ships,
290
+ and being gitignored makes it *less* visible rather than more: every guard that reads the repo
291
+ is blind to it by construction. A stray debug dump or a scratch `.env` would ship exactly as
292
+ readily.
293
+
294
+ Verified the hard way — an `.npmignore` naming the file was added first and changed nothing,
295
+ so there is no declarative fix and the config line was removed again. `bin/smoke-consumer.ts`
296
+ now asserts on the **packed listing**, the only place the truth exists, and fails on
297
+ `.DS_Store`, Spotlight markers, `.env*` and debug logs.
298
+
299
+ Found by diffing the published tarball against the tag — the post-publish check that reads the
300
+ artifact instead of the repo. All 871 built files were byte-identical; this was the only delta.
301
+
302
+ Also removed: **`bin/dist/`**, 1.6MB of tracked build output last rebuilt in August, not in
303
+ `files`, and referenced by nothing. `reviews-1.11-pass2.md` flagged it — and the identical
304
+ Spotlight marker inside it — two releases ago and left the box unticked. Every clone was paying
305
+ for it, and anyone grepping for `iife.js` found a stale copy.
306
+
307
+ ### tjs-lang 0.13.13 (#135) — **already shipped in 1.14.3**
308
+
309
+ Carried here so a 1.13.x upgrader gets it too. Full notes under 1.14.3 below: on tjs-lang
310
+ <=0.13.11 a `test '…' { … }` inside a quoted string executed at transpile time, in the host
311
+ page, outside the iframe sandbox. If you are on 1.14.3 you already have this.
312
+
313
+ ### Share a link with a PERSON: `tosijs-tunnel --link --share`
314
+
315
+ The magic link was tuned for **device** handoff — you read a seven-character code off one
316
+ screen and type it into the headset in your hands, seconds later, and you want to edit. Five
317
+ minutes is right for that, and the short window is exactly what pays for a token short enough
318
+ to type.
319
+
320
+ **Human** handoff is a different shape and had nothing. You text a URL to a colleague, she
321
+ opens it after her next meeting, and she gets a dead link — which reads as *your feature is
322
+ broken*, not *your link expired*. The alternative, in one real case, was building a Tauri app
323
+ and shipping her an installer so she could look at something.
324
+
325
+ ```bash
326
+ tosijs-tunnel --link --share # read-only, 24 hours
327
+ tosijs-tunnel --link --share --share-ttl=120 # …or two
328
+ ```
329
+
330
+ A share link is **read-only** — it browses and cannot save source — and that capability is
331
+ what makes the long life safe. It is deliberately **one flag rather than `--ttl` plus
332
+ `--read-only`**: two orthogonal knobs would let you construct a week-long *write-capable*
333
+ token sitting in a chat log, and nothing would stop you. For the same reason, raising
334
+ `linkTtlMinutes` is not the answer; that produces a long link that can still write.
335
+
336
+ The session it mints is narrowed to match and expires after **7 days** instead of the usual 30
337
+ — it belongs to someone else, so its horizon should be the demo rather than your dev server's
338
+ uptime. `--share-ttl` is capped there, because past it the link would outlive the credential it
339
+ can hand over. Share links also ignore `linkPolicy: 'single-use'`: a texted link gets opened
340
+ when the recipient gets to it, sometimes twice, sometimes after being forwarded — and spending
341
+ it on first redemption is the friction that led an adopter to replace the mechanism with a
342
+ permanent token of their own.
343
+
344
+ **New in the security surface, since this is a dev-tooling path people audit.** Read-only
345
+ sessions are the mechanism: `AuthState` gains a `readOnly` set marking a token, `redeemLink`
346
+ carries the capability from link to session, and `sessionMayWrite` / `cookieMayWrite` answer
347
+ it. `mayWriteSource` gains an optional `sessionMayWrite` — **optional, defaulting to
348
+ permissive**, so every existing three-field caller behaves exactly as before; a narrowing that
349
+ defaulted the other way would have silently disabled editing for everyone. Also exported:
350
+ `resolveShareTtlMs`, `READONLY_SESSION_TTL_MS`, `SHARE_LINK_TTL_MS`. The loopback-listener rule
351
+ that decides what counts as remote is untouched.
352
+
353
+ Configurable as `tunnel.shareTtlMinutes` (default 1440). Nothing about the existing edit link
354
+ changes: still 5 minutes, still write-capable, and `mayWriteSource` treats an omitted
355
+ capability as permissive so no existing caller behaves differently.
356
+
357
+ It still fronts the running dev server, so it needs your machine on. For a demo that has to
358
+ survive your laptop sleeping, use the static preview.
359
+
360
+ ### `import 'tosijs-ui/doc-browser'` defines `<tosi-doc-system>` (#158, #159) — **already shipped in 1.14.2**
361
+
362
+ Carried here so a 1.13.x upgrader gets it too. Full notes under 1.14.2 below. If you are on
363
+ 1.14.2 or later you already have this.
364
+
365
+ ### Client-side navigation lost syntax highlighting on an `opt-in` site
366
+
367
+ The doc-browser called `highlightBlocks` without a policy, so the new `isLiveFence` filter
368
+ defaulted to `'auto'` and skipped all six executable languages — leaving them for a live
369
+ example that an `opt-in` site never creates. Measured on client-side navigation: **531
370
+ highlighted tokens down to 23.**
371
+
372
+ The symptom lands on exactly the audience `liveExamples: 'opt-in'` exists for — prose and book
373
+ sites, where nearly every fence is illustration. It now passes `examplePolicy()`, the same
374
+ global `insertExamples` reads, so the two cannot disagree.
375
+
376
+ ### Print waited on a fixed timer instead of `load`
377
+
378
+ `autoPrint: true` used to inject a `load` wait; switching it off so the page could be
379
+ highlighted first dropped the wait with it, and printing raced the render. Restored.
380
+
381
+ ### `<tosi-schema-form>`: non-conforming data is documented, not silently absorbed (#162)
382
+
383
+ The form is an editor, not a gate — a value that violates the schema is rendered, validated on
384
+ load and marked, never coerced, stripped or refused. That is deliberate, and it is now written
385
+ down, along with why there is no built-in save gate: with no validator registered `validate()`
386
+ returns `true`, so a gate here would open silently for every consumer who never installed one,
387
+ which reads as enforcement while enforcing nothing.
388
+
389
+ Three known limitations are documented with it, because the value is preserved but **cannot be
390
+ seen**: a value the typed control cannot hold renders empty (`age: "abc"` in a `number` input),
391
+ a union value matching no branch renders no fields at all, and an error whose path has no
392
+ rendered field is invisible while `validate()` returns `false`. Tracked as #162; the fix is a
393
+ panel listing what could not be displayed.
394
+
395
+ ### `bun format` was red on two lint errors no gate ever ran
396
+
397
+ CI runs `format-check`, which is **prettier only** — so the ESLint half of `bun format`, the
398
+ command the docs tell you to run before committing, had gone red without failing anything.
399
+ Two errors in `audit-guard.ts` (a dead initializer and a `require()`), both fixed. Worth
400
+ knowing the shape: a documented command and the gate that checks it were not the same command.
401
+
402
+ The annotation gate had the mirror-image problem, and it is worth recording for anyone using
403
+ `tosijs-release-notes`: the commit that wrote up the last batch left `release-check` missing
404
+ three more, because a write-up is itself an annotation. Two were real user-visible fixes,
405
+ written up above. The third was internal and should have carried `[note]` — which is exactly
406
+ what CLAUDE.md means by "the last commit before `git tag` must carry only `[note]` bullets, or
407
+ the loop does not terminate."
408
+
409
+ ### ePub NCX `playOrder` was duplicated and non-monotonic
410
+
411
+ `playOrder` is the NCX's **linear reading position**, and the spec requires it unique and
412
+ increasing — readers use it for "next chapter" and progress. It was read inside a template
413
+ literal *after* the recursive child call had already advanced the shared counter, so every
414
+ parent inherited its deepest descendant's number.
415
+
416
+ Our own ePub shipped **4 duplicate values across 72 navPoints**, with a sequence that went
417
+ backwards. A four-level section collapsed all four onto one value.
418
+
419
+ `id` was always correct, because it captured `++counter.n` into a const. `playOrder` now does
420
+ the same, which is the whole fix.
421
+
422
+ Found by checking whether the nav system supports a parent with a parent — it does, at every
423
+ level (model, static HTML, hydrated nav, EPUB3 `nav.xhtml`, NCX, and the CSS indentation),
424
+ and refreshing a deeply nested page discloses every ancestor correctly. A flat corpus cannot
425
+ reproduce this defect, which is why it shipped.
426
+
427
+ ### `<tosi-table>`'s selection API agrees with clicking, and notifies (#157)
428
+
429
+ `selectRow`/`selectRows` stamped the selection key with no reference to the table's mode,
430
+ while the click path enforces it three separate ways. So `selectRows([a, b, c], true)` on a
431
+ single-select table left **three rows selected** — a state no amount of clicking can produce —
432
+ and neither method fired `selectionChanged`, so a consumer keeping its own UI in step silently
433
+ missed every programmatic selection. Including "restore selection after a data refresh", which
434
+ is what these methods are documented for.
435
+
436
+ Both now enforce cardinality and notify exactly once. Selecting in single-select mode
437
+ **replaces**, exactly as a plain click does, and warns if handed more than one row rather than
438
+ silently dropping the rest.
439
+
440
+ **Narrower than the issue proposed, and deliberately.** `multiple` is a data-model invariant
441
+ and is enforced; `select` is not, because it governs what the *user* may do rather than what
442
+ the *program* may do. Gating on it broke legitimate headless use — including this component's
443
+ own pinned-row example, which selects a totals row on a table that never enables clicking.
444
+ Cardinality is the invariant the report is really about, and `multiple` covers it alone.
445
+
446
+ Deselection is never gated: it can only reduce a selection, and refusing it would strand rows
447
+ selected before the mode changed with no way to clear them.
448
+
449
+ The click path keeps notifying once per interaction by composing a private non-notifying
450
+ primitive — a plain click deselects everything and then selects one, so a notifying public API
451
+ called from inside would fire several times per click.
452
+
453
+ **`<tosi-crud>` needed a guard for the new notification**, and the shape is worth knowing if
454
+ you drive a table programmatically: it both drives and observes the selection, so its own
455
+ `deSelect()`/`selectRow()` sync now called back into its handler — the deselect half reporting
456
+ an empty selection, which cleared the current record mid-edit. A component that writes and
457
+ watches the same state has to be able to tell its own writes from the user's.
458
+
459
+ ### `registerGrammar` — a language can supply its own Prism definition (#155)
460
+
461
+ Our alias table maps fence names onto Prism's built-in grammars, which is the wrong seam for a
462
+ language that ships its own. tjs-lang is generating a TJS definition from the same source that
463
+ already emits their TextMate grammars; hardcoding it here would put their grammar behind our
464
+ release cadence.
465
+
466
+ ```js
467
+ import { registerGrammar } from 'tosijs-ui/site'
468
+ import { tjsGrammar } from 'tjs-lang/prism'
469
+ registerGrammar('tjs', tjsGrammar)
470
+ ```
471
+
472
+ Registered grammars win over the alias table, install under the **fence** name (so the class
473
+ stays `language-tjs` for a theme to target), and are used by the **build-time** pass — so they
474
+ reach the pre-rendered page, the ePub and print, not just a hydrated browser tab.
475
+
476
+ That last part is the whole point, and it is their argument rather than mine: a wrong token
477
+ colour is cosmetic on the web and permanent in a printed book. TJS's colon examples
478
+ (`greet(name: 'Alice')`) are *values*; rendered with TypeScript's type colour, the page
479
+ visually asserts the exact misreading the document exists to correct.
480
+
481
+ ### `popstate` no longer re-renders on a hash-only change (#152)
482
+
483
+ Setting `location.hash` is a fragment navigation, and per spec it fires `popstate` before
484
+ `hashchange` — so any component keeping state in the hash re-mounted the entire article on
485
+ every write, destroying and re-creating every live example on the page. An editor lost focus
486
+ and its buffer mid-keystroke. The handler now compares the resolved filename and only scrolls
487
+ when the document is unchanged.
488
+
489
+ ### No rule redefines a global palette token (#150)
490
+
491
+ `button, select, .clickable` and `pre, code` each set
492
+ `--text-color: var(--brand-color)` — redefining the token the whole palette derives from
493
+ (`--tosi-text: var(--text-color)`) on an *element* selector. From any button downward it
494
+ stopped meaning "the theme's text colour".
495
+
496
+ That reaches into anything embedded: an editor deriving its chrome from the page resolved
497
+ correctly in the document and to the brand colour on its own slotted toolbar buttons — oklab
498
+ L≈0.27 against an L 0.16 bar in dark mode, unreadable, with correct body text beside it. The
499
+ `pre, code` instance also put every syntax-highlighting token span in a poisoned palette.
500
+
501
+ Both now set `color`. A test encodes the distinction rather than an allowlist: a rule may
502
+ redefine the token **only if it owns a background** — `header` is brand-coloured, so its
503
+ contents genuinely need the matching text colour and should inherit it. A rule that only wants
504
+ to colour itself sets `color`.
505
+
506
+ ### Packaging and API corrections
507
+
508
+ **`tosijs-ui/highlight-block` resolves under every condition.** It shipped as the only named
509
+ subpath without a `default` export condition — its ten siblings all had one. An exact key beats
510
+ the `./*` wildcard and *errors* (`ERR_PACKAGE_PATH_NOT_EXPORTED`) rather than falling through,
511
+ so a resolver under `['require','default']` — Jest's default, a CJS-targeting webpack/rollup
512
+ build, Vite SSR externalisation — hard-failed on that one subpath while `tosijs-ui/diff` beside
513
+ it resolved. `bun run test-consumer` now asserts **structurally** that every named subpath
514
+ carries `types` + `import` + `default`.
515
+
516
+ **`prismjs` is a real dependency, not an optional peer.** It first shipped declared *only* as
517
+ a devDependency while `dist/doc-system/highlight.js` imported it, so an adopter's build
518
+ resolved it by hoisting luck — and without it their doc site, ePub and print output had no
519
+ syntax highlighting at all, silently. Making it an optional peer then broke the build outright:
520
+ the grammar loaders are static imports a bundler must resolve, so an adopter without the
521
+ package could not build. This
522
+ is the fork already recorded for `tosijs-schema`: a literal import fails a consumer's build
523
+ when the package is absent, and a variable specifier cannot resolve in a browser. It costs one
524
+ lockfile entry and one audit surface; it costs **no bytes** unless something highlights, since
525
+ `<tosi-highlight>` is excluded from the root barrel and the grammars are lazy chunks. The iife
526
+ is the exception and pays **+31.8kb gzip** — see below.
527
+
528
+ **`ExamplePolicy` gained `'none'`** — "nothing here is live" — for targets that cannot run an
529
+ example at all. The ePub and the Print path pass it.
530
+
531
+ **`transformAvailable(dialect)`** is exported: the question a diagnostic must ask before it
532
+ accuses a document. `checkExamples` uses it to **skip** dialects whose transpiler could not be
533
+ resolved and report them as *unchecked*, instead of parsing them as JavaScript and blaming the
534
+ author (#154). `ExampleCheck.skipped` now survives the child-process boundary, so that field is
535
+ true on the path the build actually uses rather than always reading "nothing was skipped".
536
+
537
+ **The "writes outside `outputDir`" warning fires under `bun start`.** It was gated on
538
+ `!opts.skipAudit`, which means *interactive* — so it never appeared in the dev server, the
539
+ exact workflow the report came from.
540
+
541
+ ### The iife is +31.8kb gzip — and that is above our own gate
542
+
543
+ `dist/iife.js` went **435.7kb → 467.5kb gzip (+7.3%)** between 1.14.1 and 1.15.0. An iife
544
+ cannot code-split, so it inlines Prism core and all 27 grammar modules. This lands on every CDN
545
+ `<script>` user and every `tosijs-ui/site` adopter without `bundleEntry`, on every page load,
546
+ and it compounds #120 where CodeMirror is already most of that bundle.
547
+
548
+ `CLAUDE.md` sets the gate for a new runtime dependency as the printed gzip delta, and this
549
+ exceeds it. It ships anyway because the alternative was a highlighter that did not work in a
550
+ browser — but the number is stated here rather than left in a commit body, and the unexplored
551
+ remedy (capping the grammar map, or marking prism external for the iife specifically) is
552
+ tracked in `TODO.md` so tagging cannot evaporate it.
553
+
554
+ **The ESM path is unaffected**: an app importing a button ships no prism at all.
555
+
556
+ ### Need more icons?
557
+
558
+ The icons page now points at [lucide](https://lucide.dev) — a maintained fork of Feather in the
559
+ same style with far more options, ISC licensed. It emits the same geometry Feather does
560
+ (`24×24`, `fill="none"`, `stroke="currentColor"`, width 2, round caps), so a downloaded SVG
561
+ drops into `defineIcons()` and inherits colour and sizing like a built-in.
562
+
563
+ ### Smaller corrections from the pre-release review
564
+
565
+ - **`checkExamples` no longer hard-fails on a fence that will never run.** It reduced
566
+ ` ```js:static ` to `js`, so a block marked illustrative — or any block under
567
+ `liveExamples: 'opt-in'` that never asked to run — failed the build over its syntax, advising
568
+ the author to retag it `typescript`: exactly the mislabelling this release abolishes. It now
569
+ keeps the fence mode and gates on the same predicate the highlighter and `insertExamples` use.
570
+ - **`highlightBlocks` asks that predicate too**, rather than relying on `insertExamples` having
571
+ already wrapped live blocks. That held only by ordering, which nothing enforced, and
572
+ tokenizing live-example source is what cost seven doc tests mid-cycle.
573
+ - **`ensureGrammar` distinguishes "no such grammar" from "failed to load".** The first is a
574
+ plain code block; the second is a real problem and now says so. Conflating them is why a
575
+ browser-side failure shipped silently.
576
+ - **The `docPaths` escape from the default `reviews/` exclusion actually works** — the basename
577
+ test ran on the roots too, so naming the directory produced nothing while three shipped
578
+ places said otherwise.
579
+ - **The `-test-only` class** lets a stylesheet present a test-only example's results as its
580
+ body rather than as an annotation under an empty preview.
581
+ - **The barrel guard checks two properties**: a plain component build ships zero prism, and
582
+ every grammar import lives inside a thunk.
583
+
584
+ ### A test-only example shows its results instead of an empty box
585
+
586
+ A ` ```test ` fence with no `js`/`html`/`css` beside it has nothing to render. The preview came
587
+ up empty and the results panel stayed hidden unless a test **failed** — so a passing suite
588
+ looked like a broken example, and hid the one thing the block existed to show.
589
+
590
+ Now the results are the example's body: always visible, in flow, with the empty preview taking
591
+ no space. Ordinary examples are unchanged — their results stay a floating panel, shown on the
592
+ test tab or on failure.
593
+
594
+ Useful for documenting a pure function, an invariant, or any behaviour with no visual output.
595
+ There is a worked example in the `example` docs, which is also what the Playwright test asserts
596
+ against — the corpus had none when this was written.
597
+
598
+ ### Four adoption defects from tjs-lang (#151, #153, #154, #156)
599
+
600
+ All four are silent — the build succeeds and something is quietly wrong, which is #61's thesis
601
+ in four more instances.
602
+
603
+ **`SiteConfig` gained `ignoreDocPaths`, and `reviews/` is excluded by default (#153).** A
604
+ `docPaths: ['docs']` takes the directory wholesale, and one adopter's first successful build
605
+ **published 13 internal pre-release review reports as public pages** — including BLOCK
606
+ verdicts naming an adopter. `extractDocs` already had `ignore`; it simply was not reachable
607
+ from the config. The practices doc that tells you to write review reports is the same one
608
+ warning that publishing them is the bad outcome, so the safe thing is now the default rather
609
+ than the informed choice. An explicit `docPaths` entry still wins — naming a directory skips the basename exclusion,
610
+ which applies only to directories found while walking. And when a default exclusion withholds
611
+ files it now prints the directory, the count and how to publish them, rather than dropping
612
+ pages with no receipt.
613
+
614
+ > **If you built a site on 1.14.1 or earlier, upgrading is not enough.** The default only
615
+ > protects the *next* build. Scraped review pages carry no `noindex` and are listed in
616
+ > `sitemap.xml` and `llms.txt`, so anything already deployed stays public and indexable until
617
+ > you replace it. Check your **deployed** output — not your working tree — for pages derived
618
+ > from a `reviews/` directory, then rebuild on 1.15.0 and redeploy. If they were indexed, the
619
+ > rebuild removes the pages but you may also want a removal request.
620
+
621
+ **The build says which files outside `outputDir` it will overwrite (#154).** `outputDir` reads
622
+ as a box the build stays inside and is not one: `docsJson` defaults to `demo/docs.json` and
623
+ `llms.txt` is written at the project root. An adopter who set `outputDir` to a scratch
624
+ directory *specifically to evaluate without touching their repo* lost both — including the
625
+ `docs.json` their playground reads at runtime. Warned only when `outputDir` has been moved off
626
+ its default, which is the "I am containing this build" gesture; a project on the default has an
627
+ ordinary setup where these writes are the point.
628
+
629
+ **`checkExamples` no longer blames the document for a transpiler it could not load (#154).**
630
+ `loadTransform` degrades to identity when tjs-lang is unresolvable — right at runtime, wrong
631
+ for a build-time check, because the block is then parsed as raw JavaScript. The reporter got
632
+ ~30 syntax errors in **valid TJS**, each advising them to fix correct code, with the one line
633
+ explaining the degradation printed far away. Unparseable dialects are now **skipped and
634
+ reported as unchecked** — "we did not look" and "we looked and it is fine" must not produce the
635
+ same output, and neither may masquerade as "your document is broken".
636
+
637
+ **Dev rebuilds bust the browser cache (#151).** Assets were stamped with the package version,
638
+ which does not change while you work — so `hydrate.js?v=0.2.0` was byte-identical across every
639
+ rebuild and the browser kept executing the bundle it cached hours ago, through edits and
640
+ reloads. It looks exactly like "my change had no effect": the reporter spent a long session
641
+ editing CSS that was already correct and already being served. Dev builds now stamp with a
642
+ **content hash** of the bundle, the iife and the stylesheet, so it moves when the output moves
643
+ and *only* then. **Every** build does — there is no separate release mode.
644
+
645
+ Two defects in the first cut, both caught by the 1.15.0 review. `doc-system.css` was named in
646
+ the hash inputs but generated ~80 lines after the stamp was consumed, and the hash loop skips a
647
+ missing file silently — so a `theme`-only change deployed new CSS under an unchanged URL, a
648
+ regression against the version stamp it replaced. And the "release builds keep the version"
649
+ branch keyed on `opts.lock`, which no shipped caller passes and which means *acquire the build
650
+ lock*: it never ran, and an adopter using `lock` for its documented purpose would have silently
651
+ changed stamping mode. The stylesheet is now generated before the stamp, and the branch is
652
+ gone.
653
+
654
+ **A doc that documents the metadata format is no longer classified by its own example
655
+ (#156).** `<!--{…}-->` matched anywhere in the file, so a page teaching the convention was
656
+ published with the illustration's `title` and `order`. Fenced code is now stripped before
657
+ scanning and the block must start a line — the same rule `/*#` doc comments already follow.
658
+
659
+ ### Static code is syntax-highlighted — on the page, in the ePub, and in print
660
+
661
+ Display-only code was highlighted **nowhere**. The pre-rendered page emitted a bare
662
+ `<pre><code class="language-ts">`, the client never touched it, and the ePub stripped the
663
+ language class entirely — so a doc system that publishes books had no highlighting in the
664
+ books.
665
+
666
+ Highlighting now happens at **build time**, which is the only thing that reaches an ePub
667
+ (readers may run no JavaScript), print, a no-JS reader, or a crawler. Token markup is in the
668
+ HTML; all it needs is CSS.
669
+
670
+ - **Site**: **two palettes**, light (the default) and dark under `.darkmode`, each
671
+ contrast-checked against the `--code-bg` it sits on — worst case 5.32:1 light, 6.01:1 dark,
672
+ against WCAG AA's 4.5:1. Every colour is a `varDefault`, so a single token type can be
673
+ overridden in one mode without replacing the set.
674
+ - **Book and print**: a separate LIGHT palette, contrast-checked against the `#f6f8fa` code
675
+ background (the weakest is ~4.6:1, above WCAG AA).
676
+ - Both the ePub and the **Print** path pass a `'none'` example policy, because neither can run
677
+ a live example — so an executable fence is highlighted there rather than skipped. Without it
678
+ the ePub left **237 of 284 code blocks plain** (every `js`, `html`, `css` and `test` block,
679
+ which is most of the corpus) while this entry claimed otherwise, and the print path never
680
+ highlighted at all, so the book stylesheet's token rules matched nothing.
681
+ - **Client**: the same pass runs after client-side navigation, and skips anything already
682
+ highlighted, which is what keeps the pre-rendered page and its hydrated self identical.
683
+
684
+ **Executable fences are never highlighted** — they are live-example *source*, and tokenizing
685
+ them hands the example markup where it expected code. The build and the client now ask that
686
+ question through one shared predicate (`doc-system/example-policy.ts`), because for one build
687
+ they disagreed and seven doc tests failed as "Expected 0 to be 4": an example that rendered
688
+ nothing, reported as a broken component rather than a broken pipeline.
689
+
690
+ ### New: `<tosi-highlight>` — by subpath, not from the root barrel
691
+
692
+ **Import it as `tosijs-ui/highlight-block`.** It is deliberately *not* re-exported from the
693
+ root barrel, for the same reason the doc-system cluster is not (#133): its import chain
694
+ reaches the static Prism grammar map, and a bundler emitted **15 grammar chunks / 357kb** for
695
+ an app that imported a rating and a select. Measured before and after — 30 chunks / 373kb → 1
696
+ chunk / 287kb.
697
+
698
+ The iife pulls it explicitly, so CDN `<script>` and doc-site users are unaffected. Bundling
699
+ grammars is right there and wrong in the barrel: an iife cannot code-split, and its consumers
700
+ want highlighting.
701
+
702
+ A tree-shakeable component for a fetched snippet, a generated example, an API response — the
703
+ cases the build cannot know about. Renders readable plain text synchronously and upgrades when
704
+ the grammar arrives, so a reader never sees an empty box and a missing grammar degrades to
705
+ plain code rather than failing.
706
+
707
+ Prism is a **lazy chunk**: an app that imports a button ships none of it. `src/index.test.ts`
708
+ asserts that against a real split bundle, since the property is easy to lose to a stray static
709
+ import.
710
+
711
+ Prism rather than reusing the CodeMirror we already ship, because #120 is an open issue about
712
+ CodeMirror being in every IIFE at 94% of the bundle — and routing static highlighting through
713
+ it would deepen exactly the dependency that issue wants to escape. It also covers the languages
714
+ a prose corpus uses and the editor never bundled: shell, python, rust, yaml, diff.
715
+
716
+ ### Live examples can be opt-in, and a single fence can opt out (#140, #146)
717
+
718
+ Six fence languages execute — `js`, `ts`, `tjs`, `html`, `css`, `test` — and until now a
719
+ **static sample in one of them was impossible.** The only workaround was to mislabel the fence
720
+ (`xml` for HTML, `scss` for CSS), which changes the syntax highlighting to a language it isn't.
721
+
722
+ Two escapes. **The default does not move**, so no existing corpus changes:
723
+
724
+ ```
725
+ ```js:static one fence, opted out — works under either policy
726
+ liveExamples: 'opt-in' nothing runs unless its fence asks (`:inline` / `:iframe` / `:ide`)
727
+ ```
728
+
729
+ `'opt-in'` is the setting for a **prose or book site**, where code is overwhelmingly
730
+ illustration. `html` and `css` are the ones that bite, because neither looks like something
731
+ you are asking to run: a fence showing how to style the component *in your app* is injected as
732
+ a page-wide stylesheet — a white slab in dark mode — and a fence showing the markup something
733
+ compiles to renders as an unstyled demo. Neither fails a build, so a site can ship both and
734
+ nobody finds out.
735
+
736
+ The policy reaches the client the same way `__TJS_LOCAL_BASE` does, and is **only emitted when
737
+ it differs from the default**, so an `auto` site's HTML is byte-identical to before.
738
+
739
+ `:static` is honoured under both policies, so one corpus can be written to work either way.
3
740
  ## 1.14.3
4
741
 
5
742
  A single-fix patch cut from `v1.14.2`. Carries no 1.15.0 work.