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