tosijs-ui 1.13.0 → 1.14.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 (68) hide show
  1. package/CHANGELOG.md +643 -0
  2. package/bin/caddy-install.ts +17 -32
  3. package/bin/deploy-preview.ts +14 -7
  4. package/bin/release-notes.ts +15 -4
  5. package/bin/resolve-site-config.ts +77 -0
  6. package/bin/tunnel.ts +15 -7
  7. package/dist/.metadata_never_index +0 -0
  8. package/dist/babylon-3d.d.ts +14 -0
  9. package/dist/babylon-3d.js +52 -0
  10. package/dist/code-editor.d.ts +1 -0
  11. package/dist/code-editor.js +50 -2
  12. package/dist/codemirror.d.ts +2 -0
  13. package/dist/codemirror.js +43 -0
  14. package/dist/data-table.d.ts +32 -1
  15. package/dist/data-table.js +177 -2
  16. package/dist/doc-browser.d.ts +1 -0
  17. package/dist/doc-browser.js +75 -10
  18. package/dist/doc-system/doc-system-styles.js +14 -6
  19. package/dist/doc-system/doc-system.js +15 -0
  20. package/dist/doc-system/site/audit-guard.d.ts +22 -0
  21. package/dist/doc-system/site/audit-guard.js +69 -0
  22. package/dist/doc-system/site/audit-reach.d.ts +16 -0
  23. package/dist/doc-system/site/audit-reach.js +56 -0
  24. package/dist/doc-system/site/build-lock.d.ts +11 -2
  25. package/dist/doc-system/site/build-lock.js +12 -1
  26. package/dist/doc-system/site/build-stamp.d.ts +18 -0
  27. package/dist/doc-system/site/build-stamp.js +77 -0
  28. package/dist/doc-system/site/dev-auth.d.ts +9 -3
  29. package/dist/doc-system/site/dev-auth.js +54 -24
  30. package/dist/doc-system/site/dev-server.js +93 -29
  31. package/dist/doc-system/site/editable-sources.d.ts +14 -0
  32. package/dist/doc-system/site/editable-sources.js +86 -0
  33. package/dist/doc-system/site/generate-site.d.ts +2 -0
  34. package/dist/doc-system/site/generate-site.js +10 -2
  35. package/dist/doc-system/site/host-preset.d.ts +13 -0
  36. package/dist/doc-system/site/host-preset.js +56 -0
  37. package/dist/doc-system/site/invite-page.d.ts +2 -2
  38. package/dist/doc-system/site/invite-page.js +10 -3
  39. package/dist/doc-system/site/orchestrator.d.ts +12 -0
  40. package/dist/doc-system/site/orchestrator.js +177 -7
  41. package/dist/doc-system/site/preflight.d.ts +26 -2
  42. package/dist/doc-system/site/preflight.js +97 -14
  43. package/dist/doc-system/site/site-config.d.ts +6 -2
  44. package/dist/filter-builder.js +35 -1
  45. package/dist/icon-types.d.ts +16 -1
  46. package/dist/iife.js +111 -92
  47. package/dist/iife.js.map +40 -40
  48. package/dist/index-iife.js +20 -1
  49. package/dist/index.d.ts +0 -4
  50. package/dist/index.js +40 -4
  51. package/dist/live-example/code-transform.d.ts +3 -0
  52. package/dist/live-example/code-transform.js +58 -12
  53. package/dist/live-example/component.d.ts +15 -0
  54. package/dist/live-example/component.js +19 -0
  55. package/dist/live-example/execution.js +9 -2
  56. package/dist/live-example/index.d.ts +1 -1
  57. package/dist/live-example/index.js +1 -1
  58. package/dist/live-example/insert-examples.js +26 -0
  59. package/dist/menu.d.ts +13 -1
  60. package/dist/rich-text.d.ts +2 -1
  61. package/dist/segmented.d.ts +2 -1
  62. package/dist/segmented.js +1 -1
  63. package/dist/sizer.d.ts +1 -1
  64. package/dist/tsconfig.tsbuildinfo +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/llms.txt +1 -1
  68. package/package.json +9 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,648 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.14.0
4
+
5
+ ### Quote style no longer decides whether a live example runs (#141)
6
+
7
+ The live-example import rewriter accepted `'single-quoted'` specifiers and rejected
8
+ `"double-quoted"` ones — the same import, the same `{ named }` form, differing only in quotes.
9
+
10
+ On its own that is a papercut. What made it serious is **Prettier**: it formats fenced code
11
+ inside markdown and normalises quotes to double, so on any project that formats its `.md`,
12
+ every live example was silently converted into a non-running one. The only symptom was a
13
+ build-time warning that reads as advisory.
14
+
15
+ Reported from tosijs-3d-ensemble, where the README's headline example — the one showing what a
16
+ game imports — had never run for the life of the repo. Because it never ran, nothing noticed it
17
+ also named a function the package has never exported. Two defects in the most-read code block
18
+ there, both invisible for the same reason. Found by the doc-test corpus, not by reading.
19
+
20
+ All three context forms (`{ named }`, `* as ns`, default) now accept either quote style, as
21
+ does the `.elements` accessor.
22
+
23
+ **And the error message named the wrong thing.** It listed the supported forms and the context
24
+ packages — so a failing `import { x } from "pkg"`, a listed package in a listed form, was
25
+ answered with a sentence that ruled out its own cause. The reporter spent ~40 minutes rewriting
26
+ a multiline import that was never the problem. The message now separates the two real causes: a
27
+ specifier the context does not carry, versus a clause shape the rewriter cannot handle.
28
+
29
+ ### `<tosi-code>`'s own diff overlay is readable by default (#143)
30
+
31
+ `<tosi-diff>` resolves its surface as `--tosi-diff-bg || --background`. Inside `<tosi-code>`,
32
+ `--background` is still the page's white while `--text-color` is deliberately the light code
33
+ colour — so the diff `showDiff()` mounts rendered near-white text on white, and its unchanged
34
+ lines were invisible. The one place the component mounts a diff itself was the one place the
35
+ default did not work.
36
+
37
+ `--background` is now redefined for that subtree to follow `--code-bg`, so the nested diff
38
+ inherits the editor's palette. Deliberately **not** by setting `--tosi-diff-*` here: an inner
39
+ definition beats an inherited one, so that would have overridden the very custom properties
40
+ adopters are already using as an escape hatch — a silent break of the thing the issue was
41
+ about. A consumer's `--tosi-diff-bg` is still checked first and still wins.
42
+
43
+ ### Live `ts` examples load a pinned TypeScript compiler
44
+
45
+ tjs-lang's `fromTS` lazy-loads the TypeScript compiler from `DEFAULT_TYPESCRIPT_URL` —
46
+ `https://esm.sh/typescript@5`, an **unpinned major range**. It was the one hop in the entire
47
+ live-example chain that was neither same-origin nor pinned: everything else routes through
48
+ `__TJS_LOCAL_BASE` or a `TJS_VERSION`-pinned CDN. Because it is fetched at runtime it appears
49
+ in no lockfile, so `bun audit` cannot see it and "which compiler ran in a reader's browser" had
50
+ no answer.
51
+
52
+ Now pinned, via the `typescriptUrl` option tjs-lang already exposes, to the **exact version
53
+ this repo type-checks with** — so a doc example is lowered by the same compiler that validates
54
+ the source it was extracted from. A drift test asserts the pin against the **installed**
55
+ compiler rather than the declared range, because `typescript` is a caret dependency here and a
56
+ range check would call `^5.9.3` satisfied while the pin said something else.
57
+
58
+ esm.sh stays as the host — tjs-lang measured it as the only CDN that reliably serves
59
+ `typescript` as ESM. The fix is the pin, not the host; a same-origin copy is the upstream ask.
60
+
61
+ Nobody was exposed: this repo's corpus contains no executable ` ```ts ` fences.
62
+
63
+ ### `MenuAction.action` is optional, so a drop-only menu item is expressible
64
+
65
+ `acceptsDrop` + `dropAction` with no `action` — a target you can drag onto but not click — is
66
+ a shape the runtime supports and `popDropMenu`'s docs describe, but the type required `action`.
67
+ When the new typecheck lane compiled the tests for the first time, four literals were silenced
68
+ with `as unknown as MenuItem` rather than the type being corrected.
69
+
70
+ The casts are gone. Two tests now pin what the widened type admits: a drop-only item appears
71
+ **disabled** in a click menu and is removed only under `hideDisabled` — the documented
72
+ `hideDisabled` contract. (The first draft of that test asserted it was filtered out entirely,
73
+ and failed; the finding implied a behaviour the code does not have.)
74
+
75
+ ### `buildSite` docs: `dist/` is reset only when the build owns it (#130)
76
+
77
+ Three shipped surfaces still promised an unconditional reset — the config table and the
78
+ gotchas list in `doc-site-system.md`, and the `prebuild` JSDoc, which reaches
79
+ `dist/doc-system/site/site-config.d.ts` and is what an adopter reads on editor hover.
80
+
81
+ `dist/` is cleared only when `emitLibrary` or `libraryTsconfig` means this build produces it.
82
+ A `libraryBuild` function owns the directory and may deliberately emit a subset, so it is never
83
+ cleared — **if you supply `libraryBuild`, cleaning `dist/` is yours.** The output dir is still
84
+ always reset, which is the part `prebuild` authors actually need to know.
85
+
86
+ ### Security: the source editor can no longer write files the build executes (#128)
87
+
88
+ `editableSources` writes were contained only to the project root — which includes
89
+ `.git/hooks/*`, `bunfig.toml` (preload), `package.json` scripts and `bin/`, every one of which
90
+ runs on the developer's next ordinary command. With CSRF closed (#90/#121) there was no known
91
+ path to an unauthorised write; this removes the *consequence* of a future one.
92
+
93
+ `editableSourcePaths` / `mayEditSource` build an **allow-list derived from the doc corpus**,
94
+ not a deny-list of dangerous names: the endpoint exists to edit the source of a page you are
95
+ looking at, so the writable set is exactly what the extractor scraped. A deny-list needs
96
+ extending every time someone invents a new way to make a file execute and is wrong by default;
97
+ this is right by default. It **fails closed** — an unreadable corpus permits nothing.
98
+
99
+ **The first version of this was not enough, and the pre-release review caught it.** A doc
100
+ source can itself be an executed script: `bin/make-icon-data.js` carries a `/*#` block, so it
101
+ was a doc source, so it was writable — and `bin/dev.ts` spawns it on *every* build. The
102
+ original tests passed because the synthetic corpus omitted `bin/`, and the live check used
103
+ `.git/hooks/pre-commit`, which the corpus never contained. Both proved something true and
104
+ irrelevant. There is now an `isExecutedByBuild` deny layer (all of `bin/`, the spawned `-cli`
105
+ scripts, `generate-css`), and the tests run against the **shipped** corpus with an assertion
106
+ that the dangerous entry is really in it — otherwise the refusal test proves nothing.
107
+
108
+ The **read** half is gated by the same predicate. It was left open initially on the grounds
109
+ that the reported issue was a drive-by write; the review was right that the argument for
110
+ symmetry was stronger, since "view source" only ever reads a doc's own source.
111
+
112
+ ### The dependency audit knows where a package sits (#56)
113
+
114
+ `runtimeReachable` / `classifyReach` walk the manifest graph and label an advisory's package
115
+ **runtime** or **build-only**. Reported from an app monorepo running `buildSite`: 18
116
+ high/critical advisories of which the runtime-reachable subset was a small fraction, so
117
+ blocking on all of them would have bricked local dev over risk they do not carry.
118
+
119
+ Conservative by construction — root `dependencies`, `optionalDependencies` **and
120
+ `peerDependencies`** are runtime seeds (a peer ends up in the consumer's app), and an
121
+ unresolvable package counts as reachable. A finding silently downgraded is the one outcome
122
+ worth engineering against.
123
+
124
+ `audit.blockOn: 'severity' | 'runtime'` defaults to `'severity'` — today's behaviour. Set
125
+ `'runtime'` and a build-only finding is reported but does not block. If the manifest walk fails
126
+ the filter is disabled entirely, because "we could not tell" must not read as "not reachable".
127
+
128
+ A build that passes **only because of an active gate** now says so:
129
+
130
+ ```
131
+ 🟡 Build: PASSING ON A WAIVER — 1 finding(s) at or above high are suppressed
132
+ by an active gate. This is not a clean audit.
133
+ ```
134
+
135
+ Only when the waiver is load-bearing: a gate suppressing something below the threshold would
136
+ have passed anyway, and flagging that would train people to ignore the marker.
137
+
138
+ ### The doc-test runner stopped executing pages that only *mention* tests
139
+
140
+ `docsWithTests` matched ` ```test ` as a **substring anywhere in the document**, so a page
141
+ merely writing about the test tier qualified — and was rendered in a background iframe with its
142
+ examples executed. A failure in an example on a page with *no tests* therefore surfaced through
143
+ the doc-test tier, and only sometimes, depending on how quickly the TypeScript compiler loaded.
144
+
145
+ That produced a **false green**: a standalone doc-tests run reported "62 passed" against a build
146
+ the full suite failed on. Page selection now requires a fence at the start of a line, the same
147
+ rule the doc extractor applies to `/*#`.
148
+
149
+ The example it was hiding was ours: a ` ```ts ` fence in `doc-site-system.md` — added in the
150
+ #93 write-up — was **executable**, so the doc-test lane ran `Bun.spawn` and `process.on` in a
151
+ browser and threw `ReferenceError: process is not defined`. It is now ` ```typescript `, which
152
+ is display-only. One character, and it had been red on `main` from the moment it was pushed.
153
+
154
+ Related and still open (#138): an example that *compiles and then throws* is caught by nothing
155
+ on a page without a ` ```test ` block. The build-time check catches what will not compile, not
156
+ what compiles and fails.
157
+
158
+ ### An existing `firebase.json` is checked against `outputDir` (#134)
159
+
160
+ `host: 'firebase'` scaffolds a config for a fresh project and keeps its hands off an existing
161
+ one — but checked nothing, so `buildSite` could write `docs/` while `firebase deploy` published
162
+ `.demo/`. Both commands succeed and the site serves whatever was there last. It now warns,
163
+ handling multi-site arrays, framework-aware hosting with no `public`, and `./docs` vs `docs`
164
+ spelling, and warns only when *every* declared target disagrees.
165
+
166
+ ### tjs-lang 0.13.4 → 0.13.11, and the two pins can no longer drift (#135)
167
+
168
+ We were pinned to a version npm has **deprecated**, seven releases behind. The deprecation's
169
+ threat model does not reach live-example — it is about a server transpiling submitted source —
170
+ but sitting on a deprecated version with seven releases of fixes is its own problem.
171
+
172
+ `TJS_VERSION` (what the *published site* fetches from a CDN) and the exact devDependency (what
173
+ tests and local builds use) live in different files, so a test now asserts they agree. Drift
174
+ there is invisible exactly where it matters: every lane stays green while the deployed site
175
+ transpiles with a different version.
176
+
177
+ ### `<tosi-example>` warns when executable fences collide (#139)
178
+
179
+ `js`/`tjs`/`ts` all write the same single-valued slot, so a second executable fence silently
180
+ overwrote the first — the earlier block vanished from the page, the example ran as the later
181
+ dialect, and nothing said so. Behaviour is unchanged (last one still wins); it now names the
182
+ file and which dialect survived. This is the seam a dialect selector needs, which is why it is
183
+ worth fixing before that feature rather than during it.
184
+
185
+ ### `getSelectText` returned the empty string on every call
186
+
187
+ `<tosi-filter>` read `select.options[select.selectedIndex]`, and `selectedIndex` has never
188
+ existed on `TosiSelect` — it selects by value. So every filter chip read ` "needle"` with two
189
+ blanks where the field and condition belong, which looks like a styling bug. Found by adopting
190
+ tosijs 1.10.0, whose removal of `Component`'s `[key: string]: any` index signature made the
191
+ compiler able to see it for the first time.
192
+
193
+ ### Markdown is no longer formatted, and `bun format` is green again
194
+
195
+ `*.md` is in `.prettierignore` by request from tosijs. `proseWrap` was already `preserve`, so
196
+ prettier's only remaining markdown behaviours were escaping literal characters
197
+ (`a * literal` → `a \* literal`), padding table cells, and rewriting `*` bullets — the first of
198
+ which edits a document's *content*, and markdown is the product here.
199
+
200
+ Separately, `bun format` had been exiting 1 on seven ESLint errors: CI gates `format-check`,
201
+ which runs prettier only, so the ESLint half of the documented pre-commit command was red
202
+ without failing anything. Among the fixes: two dead locals in the CSRF same-origin check
203
+ (checked rather than assumed — the logic was sound, only the initialisers were dead); a dead
204
+ `explanation` in `dev-server.ts` that was built and never used, left over from before
205
+ `invite-page.ts` took ownership of that copy in #114; and an
206
+ `AsyncFunction` construction failure that now rethrows with `{ cause }` so the engine's own
207
+ error — the one naming *which* parameter it choked on — is not discarded.
208
+
209
+ ### `icons.*` was typed `SVGElement`, and composites are spans
210
+
211
+ **This is why the release is a minor.** `SVGIconMap` declared every icon as
212
+ `ElementCreator<SVGElement>`. A **composite** icon — any name containing `$`, the stacking
213
+ syntax `icon-composition.md` documents and the demos use throughout — returns an
214
+ `HTMLSpanElement`.
215
+
216
+ Measured rather than reasoned about:
217
+
218
+ ```
219
+ icons['tosiHat$tosi']() → SPAN, instanceof SVGElement === false
220
+ icons.lock() → svg, instanceof SVGElement === true
221
+ ```
222
+
223
+ `resolveIcon` has always returned the honest `Element`; only the cast on the exported proxy
224
+ disagreed. It is now `ElementCreator<IconElement>`, and **`IconElement` is exported from
225
+ `icon-types.ts` for consumers holding an icon** — defined as `SVGElement | HTMLSpanElement`.
226
+ Both members carry `.style` and `.dataset`,
227
+ which is what callers actually reach for — widening all the way to `Element` would break
228
+ working code to no purpose.
229
+
230
+ **Why a minor rather than a patch.** Nothing about the runtime changed; a composite has always
231
+ been a span. But code that annotated an icon as `SVGElement` compiled before and does not now,
232
+ and "a patch never breaks you" is the contract. This breaks you out of a false belief, which is
233
+ still breaking.
234
+
235
+ **The lie was load-bearing.** Correcting it surfaced **14 sites in this repo** — `data-table`,
236
+ `notifications`, `rich-text`, `segmented` — that stored or passed an icon as `SVGElement`. Any
237
+ consumer doing the same has the same latent bug, which is the argument for fixing the type
238
+ rather than the tests. If your build newly complains, that is the bug being shown to you: the
239
+ fix is `IconElement`, or `as SVGElement` at a site you know is not a composite.
240
+
241
+ ### The test suite is type-checked now (#73)
242
+
243
+ `tsconfig.json` excludes `*.test.ts` — right for emit, and it meant nothing ever compiled them.
244
+ **48 errors had accumulated unseen** (the issue counted 40 when it was filed; it grows).
245
+ `tsconfig.tests.json` plus a CI step brings it to zero and holds it there.
246
+
247
+ The icons defect above is what that gate found on its first run. Two test files carried
248
+ duplicate imports — `sessionCookie` and `nodePath` — which is invalid ESM that the runtime
249
+ tolerated and nothing checked. The other 46 were genuine test-side noise, fixed honestly
250
+ rather than with blanket `any`.
251
+
252
+ A type-level test asserts nothing if nothing compiles it — which was already the argument for
253
+ `typecheck-guards`, applied to everything else.
254
+
255
+ ### The test lane itself, honestly
256
+
257
+ Three things the pre-release review found in the tests rather than in the code, recorded
258
+ because a suite that is quietly wrong is worse than one that is loudly red.
259
+
260
+ **Six tests were deleted and the suite stayed green.** Adding the #139 cases to
261
+ `insert-examples.test.ts`, `cat >` was used instead of `cat >>`, wiping two `describe` blocks
262
+ that covered still-shipping behaviour — grouping across a baked `<script>`, and fenced
263
+ execution modes. Nothing complained, because fewer tests all passed. That is the exact shape of
264
+ a coverage regression no gate catches: green is not a measurement of how much you asked. The
265
+ blocks are restored, and the #139 cases rewritten against the same stub creator the originals
266
+ used.
267
+
268
+ **`ENOENT reading "https://esm.sh/typescript@5"` during `bun test`** came from the same
269
+ rewrite: the real `liveExample` transpiled a `ts` fence, and the rejection leaked across test
270
+ files, printing under an unrelated file's header. Zero occurrences now — and worth noting that
271
+ the message named a URL while claiming `ENOENT`, which is why it read as noise for as long as
272
+ it did.
273
+
274
+ **`caddy-install.test.ts` was flaky about one run in ten.** Each of its tests cold-starts a
275
+ real `bun` to exercise the generated remote script, so its cost is interpreter startup — ~1.2s
276
+ each, with the file swinging 3.5s to 10.1s under load. Under bun's 5s default that put single
277
+ tests over the line whenever the machine was busy. They now carry a 20s budget that describes
278
+ the work; a test exceeding *that* is a real defect rather than machine load.
279
+
280
+ ### Every bin answers `--help`, and refuses what it does not understand (#85)
281
+
282
+ `caddy-install`, `deploy-preview`, `tunnel` and `release-notes` each carried their own
283
+ `flag()`/`has()` pair. One shared `parseArgv` replaces all four.
284
+
285
+ **Refusing unknown flags is the load-bearing part.** `tosijs-caddy-install --status` — a flag it
286
+ never had — used to read as "no flags at all", and the bin went on to ssh into the configured
287
+ host. The same shape was still live in `tosijs-deploy`. A tool that contacts a machine must not
288
+ read an instruction it does not understand as consent to proceed.
289
+
290
+ `--help` comes with it, and is parsed **before** the site config is read, so it works from any
291
+ directory. That matters because no lane runs a shipped bin's argument handling —
292
+ `smoke-consumer.ts` checks shebangs and deliberately does not execute the bins that reach the
293
+ network — so `tosijs-release-notes --help` shipped unrecognised and died on a raw `ShellError`
294
+ outside a git repo.
295
+
296
+ Also: `deploy:index` now asks for `PREVIEW_HOST` like the bins, instead of the `PREVIEW_SSH`
297
+ they moved off.
298
+
299
+ ### Magic-link codes are eight letters, no digits (#132)
300
+
301
+ ```
302
+ 🔗 code: JCYGCCVX (case-insensitive)
303
+ ```
304
+
305
+ The code is typed by hand into a floating keyboard on a headset. Crockford base32 was already
306
+ chosen for typo-resistance and that was right as far as it went — but it contained **digits**,
307
+ and on a headset the letter↔number switch is a trip to a different keyboard page. A reported XR
308
+ session minted `A70MDCD` and `X9Q3Z53`: three digits each in seven characters, so three round
309
+ trips for a code being gaze-and-pinched one glyph at a time.
310
+
311
+ **The extra character is free; the mode switch is not.** Eight letters of a 22-letter alphabet
312
+ is **35.7 bits against the old seven-of-32's 35.0** — slightly _stronger_, while never leaving
313
+ the alphabetic keyboard.
314
+
315
+ `I`, `L`, `O` and `U` stay excluded. `I`/`L` are indistinguishable across case, which matters
316
+ _more_ now that there are no digits, because the code is case-insensitive so the reader cannot
317
+ use case to tell them apart. Redemption also maps back the digits someone might type for a
318
+ letter they misread (`5`→`S`, `2`→`Z`, `8`→`B`, `6`→`G`) — pure upside, since a digit can never
319
+ be part of a real code — and ignores hyphens and spaces.
320
+
321
+ Rate limiting is unchanged and still does the heavy lifting: redemption is serialized at ~10
322
+ attempts/second, so 22⁸ takes ~174 years to exhaust and a five-minute window is about 1 in 18
323
+ million.
324
+
325
+ `dev-auth.ts` itself went on arguing from **32⁷** — ~111 years, 1 in 11 million — for the whole
326
+ of the change that made the token 22⁸. The conclusion never stopped holding, which is precisely
327
+ why nothing surfaced it: a safety argument that survives its own premises being replaced is not
328
+ being read. The figures are corrected in place, with the superseded ones kept beside them, since
329
+ this is the one file where the numbers *are* the argument rather than a description of it.
330
+
331
+ The invite page's code box (shipped for #75) covers most of the rest of the ask: bookmark the
332
+ host once, then each session is eight letters into a text field. The `dev.tosijs.net/<code>`
333
+ short URL needs host-side infrastructure and is tracked in `TODO.md`.
334
+
335
+ ### BREAKING (narrow): the root barrel no longer re-exports the doc system (#133)
336
+
337
+ `import { … } from 'tosijs-ui'` no longer gives you `code-editor`, `doc-browser`,
338
+ `doc-system/doc-system` or `live-example`. **Import them by subpath instead** — they were
339
+ already reachable that way, and nothing else changes:
340
+
341
+ ```typescript
342
+ import { tosiCode } from 'tosijs-ui/code-editor'
343
+ import { createDocBrowser } from 'tosijs-ui/doc-browser'
344
+ ```
345
+
346
+ **Why.** `code-editor` pulls CodeMirror; `live-example` and `doc-system/doc-system` each pull
347
+ `tjs-lang` independently. This package has no `sideEffects` field — correctly, since
348
+ `elementCreator()` registers elements at import time and a blanket `sideEffects: false` shakes
349
+ a bare `import 'tosijs-ui'` down to zero registrations — so a bundler must treat every
350
+ `export *` target as side-effectful and cannot drop it. All four were therefore eager in any
351
+ app that imported _anything_ from the barrel.
352
+
353
+ Measured by snowfox-app moving a 15MB React bundle to the bun bundler, and reproduced here:
354
+
355
+ | | size |
356
+ | ----------------------- | ------------------------------------ |
357
+ | barrel as shipped | 1.68 MB |
358
+ | barrel without the four | 0.38 MB |
359
+ | real app bundle | 15.21 → 13.86 MB (**1.35 MB, 8.9%**) |
360
+
361
+ The doc cluster was **77% of the barrel**. For contrast the 3D, map and lottie components —
362
+ the ones you would expect to be the heavy tail — cost about 10kB, because they already
363
+ lazy-load properly.
364
+
365
+ The doc site and CDN `<script>` users are unaffected: `index-iife.ts` imports all four
366
+ explicitly, so the weight lands on the bundle built _for_ the doc site rather than on an app
367
+ that imports a button.
368
+
369
+ ### New: `tosijs-ui/codemirror` — extend `<tosi-code>.editor` without a silent no-op (#131)
370
+
371
+ `<tosi-code>` exposes the raw `EditorView` and the 1.7 notes invite you to configure CodeMirror
372
+ through it. But CodeMirror 6 keys facets, `StateField`s and gutters by **object identity**, so
373
+ an extension built from a _second copy_ of `@codemirror/state` or `@codemirror/view` is not
374
+ mismatched — it is **silently ignored**. No error, no warning; the gutter never renders.
375
+
376
+ Reported with a real repro: a consumer's `@codemirror/view` resolved to `6.43.6` while ours
377
+ hoisted to a nested `6.43.10`, so their `gutter()` and `GutterMarker` came from a different
378
+ instance than the view. `@codemirror/state` happened to dedupe, which made it worse — enough
379
+ worked that the failure looked like a bug in their gutter code.
380
+
381
+ ```typescript
382
+ import { gutter, GutterMarker, StateField, StateEffect } from 'tosijs-ui/codemirror'
383
+ ```
384
+
385
+ Only `@codemirror/state` and `@codemirror/view` are re-exported — the two needed to extend a
386
+ view, exactly as the report identified.
387
+
388
+ **Why not peer dependencies**, which the issue proposed and is the obvious fix: a required peer
389
+ would make every consumer install two CodeMirror packages including the overwhelming majority
390
+ who never render an editor (`elementCreator()` registers eagerly, so importing tosijs-ui is not
391
+ evidence of using it), and an _optional_ peer is worse than either — the component silently
392
+ does nothing until you discover you needed 12 packages. Re-exporting removes the failure by
393
+ construction rather than policing it: there is no version to match and no `overrides` to write,
394
+ because there is only ever one copy.
395
+
396
+ ### `buildSite` no longer wipes `dist/` — it is your package output, not a site artifact (#130)
397
+
398
+ The prebuild ran `rm -rf dist/` on **every** `buildSite`, including from `devServer` and
399
+ including for projects that configure no library build at all. For the repos this API exists to
400
+ serve, `dist/` is the **published package output** — an input to `npm publish` — so the wipe
401
+ destroyed build products the current run does not rebuild.
402
+
403
+ Reported from tosijs with the full chain: two bundles (`module.debug.js`, `module.safe.js`,
404
+ exposed as the `./debug` and `./safe` subpath exports) are produced only under `--build`,
405
+ because they need a 53-file transpile too slow for the dev loop. `bun start` — or Playwright's
406
+ `webServer`, which runs it — wiped `dist/` and rebuilt five of seven. A publish from that tree
407
+ ships two subpaths that throw `ERR_MODULE_NOT_FOUND`, and it reached a commit once already.
408
+ Nothing local could catch it: size and smoke gates iterate the bundles the current run _built_,
409
+ so a bundle that was **deleted rather than skipped** is invisible to all of them.
410
+
411
+ The rule is now "clean only what you wholly generate":
412
+
413
+ | configuration | `dist/` cleaned? |
414
+ | -------------------------------------------------------------- | ---------------- |
415
+ | `emitLibrary` / `libraryTsconfig` (tsc emits the complete set) | yes |
416
+ | `libraryBuild` — a consumer function that may emit a subset | **no** |
417
+ | none configured — nothing here owns it | **no** |
418
+
419
+ The doc `outputDir` wipe is unchanged and stays: that directory is wholly generated, and the
420
+ build announces it. `dist/` is different.
421
+
422
+ Incidental: `emitLibrary` runs `tsc --incremental`, whose `.tsbuildinfo` lives in `dist/`.
423
+ Wiping it every run meant incremental compilation had never once done anything.
424
+
425
+ ### `bun start` serves in ~3.6s instead of ~23s, and prints 13 lines instead of 37
426
+
427
+ **20.1 of those 23 seconds were the dependency audit**, running synchronously before the port
428
+ was bound — where it hit its own 20s timeout, gave up, and **failed open**. So the stall
429
+ bought nothing on that run: it was the cost of a check that did not happen. Measured against
430
+ the live registry, `bun audit` takes **79.5s**.
431
+
432
+ The code justified blocking on the grounds that "the audit is sub-second… a gate you wait for
433
+ cannot be raced". The premise was false, so the conclusion was buying a twenty-second stall on
434
+ every start. It now runs after the server is up and reports when it lands, prefixed loudly
435
+ enough that a late advisory cannot be mistaken for part of the startup banner — non-blocking
436
+ must not mean easy to miss. **Release builds still block**: `buildSite` audits synchronously,
437
+ and `--test` gates there too, which is where waiting is the right trade.
438
+
439
+ **Build output is 13 lines, down from 37.** Eighteen of them were `bun build` listing every
440
+ chunk and every sourcemap — the same size information the build already summarises, only
441
+ longer. Captured now, and replayed **in full** on failure; verified by deliberately breaking
442
+ the bundle entry, which still prints the errors, exits 1, and restores the last good build.
443
+
444
+ Spam is not free: it trains people — and agents — to stop reading build output, which is
445
+ exactly what lets a real failure through.
446
+
447
+ The ePub was the assumed culprit and is innocent: 0.34s of the 23. Measuring first is the only
448
+ reason the actual cause was found.
449
+
450
+ ### `docs.json` is cache-busted, so a stale corpus stops surviving a deploy
451
+
452
+ Every generated URL went through the asset stamp — the stylesheet, the hydration bundle, the
453
+ IIFE — **except the one that matters most**. `docs.json` is the entire corpus: nav, routes and
454
+ every page's content, and it was emitted bare.
455
+
456
+ A static host sends no `Cache-Control` for it, so browsers apply **heuristic** caching — free
457
+ to invent a freshness lifetime when you decline to state one. The site then renders a previous
458
+ deploy's corpus against the current bundle, and nothing in the console says so. It presents as
459
+ pages that will not update, and the fix is a hard reload nobody thinks to try. It cost a
460
+ maintainer a long debugging session before it was traced to a Chrome cache entry.
461
+
462
+ The corpus gets **its own stamp**, hashed from `docs.json`'s own bytes rather than the project
463
+ version. The version is right for the bundles — it moves once per release, which is when they
464
+ change. The corpus does not work that way: it changes whenever anyone edits a doc, many times
465
+ within one version, and a preview host redeployed mid-version is the normal way this project
466
+ reads its own site. A version-keyed corpus URL would have been stale for exactly that workflow.
467
+
468
+ The dev server was already correct here — it sends `no-store, must-revalidate` on everything,
469
+ verified. This was only ever the built static site.
470
+
471
+ ### `<tosi-table>` stops silently capping at 10,000 rows (#82)
472
+
473
+ `maxVisibleRows` defaulted to a flat `10000` and `slice`d everything past it **without a
474
+ word**. A 25,000-row table showed 10,000, and every count, filter and sort then ran on the
475
+ truncated set — self-consistent, and disagreeing with the data. The property was documented
476
+ nowhere, so the only way to find it was to lose rows and go looking.
477
+
478
+ The number was wrong because it was sized as a _rendering-cost_ limit. It is a **layout**
479
+ limit. A virtual `listBinding` renders only the visible window, so the UI cost is O(1) in row
480
+ count — 300,000 rows scroll like 300 — and the array size is irrelevant. What binds is the
481
+ spacer element's height against the maximum height a browser will lay out.
482
+
483
+ So the cap is now derived at runtime: `maxElementHeight / rowHeight`, which at the default
484
+ 30px rows is on the order of half a million to a million rows. **Probed, not hardcoded**,
485
+ because the ceiling is engine- _and version_-specific — two Chromium measurements in this
486
+ project sit a factor of two apart (Chrome 151 clamps at 16777214px, the Playwright Chromium
487
+ at 33554428px). Any constant would be wrong for somebody, in the direction that loses rows.
488
+
489
+ And when a cap does bite, it now **says so in the console** with both numbers and how to
490
+ raise it. `rowHeight: 0` stays conservative on purpose: with no fixed row height there is no
491
+ virtualisation, every row is a real DOM node, and the cost genuinely is O(n).
492
+
493
+ Set `table.maxVisibleRows = n` to override, exactly as before.
494
+
495
+ ### A large `rowHeight: 0` table now complains, loudly (#84)
496
+
497
+ `rowHeight: 0` turns virtualisation off — every row becomes a real DOM node, and sorting,
498
+ filtering and group toggles walk all of them on every render. It is documented for "smaller
499
+ tables, or tables with variable row-heights", and that is the only thing it is for.
500
+
501
+ Past **1,000 rows** the table now warns **on every render** until a `rowHeight` is set. That
502
+ is deliberate, and deliberately unlike every other notice in this codebase, which fire once:
503
+ those describe states a developer may not be able to change, whereas this is a one-line fix
504
+ that persists until made, and a single notice scrolls out of the console and is gone.
505
+
506
+ The originally suggested fix was to binary-search the scroll-anchor walk. That was declined:
507
+ it optimises a configuration nobody should be in, and makes a misconfiguration marginally
508
+ less bad instead of surfacing it. A table big enough for the cost to matter is big enough to
509
+ virtualise, and virtual tables are O(1) in row count.
510
+
511
+ ### `<tosi-3d>` is deprecated — use `tosijs-3d`
512
+
513
+ Nothing breaks: it still works, still ships, and stays until a future major. But new work
514
+ should use **[`tosijs-3d`](https://www.npmjs.com/package/tosijs-3d)**.
515
+
516
+ It is a much better library, and maintaining both is duplicated effort spent against the
517
+ weaker one. `<tosi-3d>` is a thin Babylon wrapper that grew inside a general-purpose UI
518
+ library; `tosijs-3d` is a project actually about 3D and XR, with the declarative component
519
+ set and XR support that implies. Every hour spent here is an hour not spent there, and the
520
+ result would still be the lesser of the two.
521
+
522
+ `tosijs-3d` grew out of this component, so migration is mostly a rename.
523
+
524
+ The deprecation appears in three places, deliberately: the doc page (what people read), the
525
+ `@deprecated` tags on `B3d`/`b3d` (what editors surface), and a console notice printed **once
526
+ per page on first use** — not at import, since `elementCreator()` registers eagerly and a bare
527
+ `import 'tosijs-ui'` is not evidence anyone is using this; and not per instance, because a page
528
+ with several scenes would spam its own console, which is how a deprecation notice teaches
529
+ people to filter warnings rather than act on them.
530
+
531
+ ### The machine-health guard now reports 13GB of orphans instead of skipping them (#93)
532
+
533
+ `isDevProcess` decided what was even a _candidate_ for the runaway check, and excluded
534
+ `build` — because we spawn the bundler ourselves and a build peaking mid-run must not look
535
+ like a runaway to its own parent. But the exemption did not say "our bundler". It said
536
+ **every `bun build` on the machine, forever, regardless of owner or age**, so an orphan whose
537
+ parent died a week ago was exactly as exempt as one that started 200ms ago.
538
+
539
+ Reported with measurements: **69 orphaned `bun build --watch` processes holding 13.09GB**, ~55
540
+ of them spawned inside a single 61-minute window, on a 128GB box running these very dev servers
541
+ and sitting at 116MB free. The guard looked, and skipped every one — while resting on the
542
+ premise that "nothing noticed the runaways because nothing ever looked".
543
+
544
+ `--watch` is the distinguishing signal, and it was right there in the argv: a watcher is a
545
+ server whoever started it. The mid-rebuild false positive the exemption existed for is already
546
+ handled, and handled _precisely_, by parentage — the assessor filters `process.pid` plus
547
+ `descendantsOf(process.pid)` before assessing anything. That is the exact test where a
548
+ subcommand name was a blunt proxy for it, so this costs no false positives. `bun run` with
549
+ `--watch` is caught too; the reported machine had four such servers up to 18 days old.
550
+
551
+ **Visible is not the same as caught**, and the first pass at this shipped the difference. Every
552
+ trigger in the assessor asks "is _one_ process too big?", and 194MB is nowhere near any of them —
553
+ so dropping the exemption walked the guard past all 69 orphans and returned `ok` for 13GB, while
554
+ the note above claimed they had been dealt with. The assessor now carries a rule that never
555
+ consults RSS: **a watcher whose parent is gone (`ppid` 1) is waste at any size**, reported with
556
+ the `kill` command. One 12MB orphan warns. It warns rather than refuses, because a build that
557
+ will not start because of someone's stray process is a guard people switch off.
558
+
559
+ "A bunch of similar names" was the other candidate signal and is deliberately _not_ used: three
560
+ identical `bun --watch bin/dev.ts` lines are usually three sibling projects each running their
561
+ own server. Identical argv is not duplicate work — the distinguishing fact is the cwd, which
562
+ `ps` does not report.
563
+
564
+ **And the other half, in `doc-site-system.md`:** "shell out so the OS reclaims the memory on
565
+ exit" is only true if the child _exits_. A `dev.ts` that spawns `bun build --watch`, itself run
566
+ under `bun --watch dev.ts`, leaks one bundler per keystroke — the previous child is never
567
+ signalled. The doc now carries the reaping idiom (kill the previous child; reap on
568
+ `SIGINT`/`SIGTERM`/`beforeExit`) beside the advice that produced the leak. This project's own
569
+ builds are one-shot and were never affected.
570
+
571
+ ### `layout: "full-screen"` finally gets its scroll container (#119)
572
+
573
+ `.doc-content`'s `overflow` is an **inline** style set by the doc-browser, so the full-screen
574
+ layout's stylesheet `overflow: auto` was competing with an inline style and always lost. The
575
+ `height: 100%` landed and the scroller did not — a full-screen page taller than its box was
576
+ clipped and unscrollable. It sat one line below a `padding` that correctly went through a
577
+ variable, with a comment explaining exactly why it had to.
578
+
579
+ Both now go through `--doc-content-overflow` (default `hidden`), on the inline style **and**
580
+ the pre-hydration rule — they have to match, or an override would take effect only after
581
+ hydration and the page would visibly change shape.
582
+
583
+ The second consequence is the one worth knowing about: `overflow: hidden` on an ancestor
584
+ kills `position: sticky` in its subtree, so a page wanting a sticky viewport-sized window
585
+ inside the article previously needed `.doc-content { overflow: visible !important }`. An
586
+ `!important` in a consumer's stylesheet is this component failing to offer a seam.
587
+
588
+ ### A page with tests says so, even when tests are off (#113)
589
+
590
+ Tests default to on for localhost and off everywhere else, and when they are off the widget is
591
+ hidden — so **a page with failing tests looked identical to a page with no tests**. There was
592
+ an override, `localStorage['tosijs-ui-tests-enabled']`, but you had to know the key, which
593
+ means you had to already suspect there was something to see.
594
+
595
+ That bit hardest down the path we recommend: `tosijs-tunnel` necessarily serves from a
596
+ non-localhost hostname, so the sanctioned way to view a dev site remotely is exactly where test
597
+ state went invisible. A maintainer read a clean tunnelled page as "the failure must be
598
+ localhost-specific" and lost the thread.
599
+
600
+ The app menu now offers a **Tests** toggle — but only on a page that has tests, where it reads
601
+ `Tests (N not run)` while they are off. `pageTestCount()` is exported from live-example for
602
+ this: how many examples carry tests, regardless of whether tests are enabled — the number that
603
+ lets a UI say a runner exists here without turning it on. A published page with no tests offers nothing, so the
604
+ default stays as quiet as it should be. The default itself was never the problem; the silence
605
+ was.
606
+
607
+ ### `/version.json` stops restamping a site that did not change (#122)
608
+
609
+ `docs/` is committed here and in the sibling projects, and the stamp named
610
+ `git rev-parse HEAD` — so **every commit's `version.json` named its own parent, forever**, and
611
+ no build→commit cycle converged:
612
+
613
+ ```
614
+ build at A → version.json says A
615
+ commit → B contains a version.json saying A
616
+ rebuild → version.json says B → dirty
617
+ commit → C contains a version.json saying B → …
618
+ ```
619
+
620
+ `build-stamp.ts` already argued against a wall-clock stamp for exactly this reason and missed
621
+ that the argument applies one level up: `HEAD` is not a clock, but in a repo that commits its
622
+ own output it moves on the same cadence. Small, and it cost something real — a false positive
623
+ on `git status` at the moment you are trying to confirm a release tree is clean, so it gets
624
+ checked, re-checked, and eventually ignored.
625
+
626
+ The fix is the reporter's: if nothing else in the output changed, leave the stamp alone. A new
627
+ `contentHash` field records what the rest of the output hashed to, so the next build can tell
628
+ "changed" from "unchanged" without keeping a copy of the old tree — though as it happens the
629
+ previous build is still on disk (the build moves it aside as `<outputDir>.last-good` rather
630
+ than deleting it, and drops it only on success), so the comparison costs no extra bookkeeping.
631
+
632
+ Verified end-to-end, which is the only way to check a convergence claim: build → commit →
633
+ rebuild now leaves a **clean tree**.
634
+
635
+ The stamp is also more honest for it. It now names the last build that actually changed the
636
+ site, rather than the last one that happened to run. **Deleting `version.json` still forces a
637
+ fresh stamp** — the escape hatch for re-identifying an unchanged build, at no extra flag.
638
+
639
+ ### `describeHolder(null)` says nothing is building (#123)
640
+
641
+ `describeHolder(currentHolder(root))` is the natural spelling, and `currentHolder` returns
642
+ `null` whenever nothing holds the lock — the ordinary, healthy case. It rendered the alarm
643
+ template with the fields unfilled (`pid undefined … building in undefined`), reporting a scary
644
+ problem that did not exist, in a reader whose whole purpose is answering "who holds this?".
645
+
3
646
  ## 1.13.0
4
647
 
5
648
  ### tosijs `^1.9.1` — and a console with nothing in it