tosijs-ui 1.9.0-rc.2 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +307 -39
  2. package/bin/deploy-preview.ts +78 -65
  3. package/bin/make-icon-data.js +93 -2
  4. package/bin/release-notes.ts +150 -0
  5. package/bin/resolve-site-config.ts +151 -1
  6. package/bin/tunnel.ts +99 -44
  7. package/deploy/Caddyfile +24 -13
  8. package/deploy/build-index.sh +8 -2
  9. package/dist/babylon-3d.js +2 -2
  10. package/dist/bodymovin-player.js +1 -1
  11. package/dist/carousel.js +1 -1
  12. package/dist/code-editor.d.ts +2 -2
  13. package/dist/code-editor.js +3 -3
  14. package/dist/data-table.d.ts +2 -2
  15. package/dist/data-table.js +6 -6
  16. package/dist/dialog.js +1 -1
  17. package/dist/doc-browser.d.ts +1 -1
  18. package/dist/doc-browser.js +13 -12
  19. package/dist/doc-system/book-html.d.ts +1 -1
  20. package/dist/doc-system/book-html.js +3 -3
  21. package/dist/doc-system/book-target.d.ts +51 -0
  22. package/dist/doc-system/book-target.js +136 -0
  23. package/dist/doc-system/css-var-editor.d.ts +1 -1
  24. package/dist/doc-system/css-var-editor.js +1 -1
  25. package/dist/doc-system/doc-system-styles.js +1 -1
  26. package/dist/doc-system/doc-system.d.ts +1 -1
  27. package/dist/doc-system/doc-system.js +8 -8
  28. package/dist/doc-system/nav-tree.js +1 -1
  29. package/dist/doc-system/release-notes.d.ts +33 -0
  30. package/dist/doc-system/release-notes.js +199 -0
  31. package/dist/doc-system/site/audit-guard.js +13 -6
  32. package/dist/doc-system/site/check-examples-cli.js +1 -1
  33. package/dist/doc-system/site/check-examples.d.ts +2 -2
  34. package/dist/doc-system/site/check-examples.js +1 -1
  35. package/dist/doc-system/site/dev-auth.d.ts +32 -1
  36. package/dist/doc-system/site/dev-auth.js +71 -1
  37. package/dist/doc-system/site/dev-server.d.ts +9 -2
  38. package/dist/doc-system/site/dev-server.js +167 -43
  39. package/dist/doc-system/site/docs.d.ts +23 -0
  40. package/dist/doc-system/site/docs.js +18 -2
  41. package/dist/doc-system/site/epub-cli.js +1 -1
  42. package/dist/doc-system/site/epub.d.ts +11 -2
  43. package/dist/doc-system/site/epub.js +60 -9
  44. package/dist/doc-system/site/generate-css.d.ts +2 -2
  45. package/dist/doc-system/site/generate-css.js +2 -2
  46. package/dist/doc-system/site/generate-site.d.ts +3 -3
  47. package/dist/doc-system/site/generate-site.js +4 -4
  48. package/dist/doc-system/site/index.d.ts +16 -16
  49. package/dist/doc-system/site/index.js +9 -9
  50. package/dist/doc-system/site/make-llms-txt.js +1 -1
  51. package/dist/doc-system/site/open-browser.js +10 -2
  52. package/dist/doc-system/site/orchestrator.d.ts +1 -1
  53. package/dist/doc-system/site/orchestrator.js +67 -15
  54. package/dist/doc-system/site/sections.js +2 -2
  55. package/dist/doc-system/site/site-config.d.ts +50 -27
  56. package/dist/doc-system/site/site-config.js +10 -0
  57. package/dist/editable-rect.js +2 -2
  58. package/dist/filter-builder.d.ts +1 -1
  59. package/dist/filter-builder.js +2 -2
  60. package/dist/float.js +1 -1
  61. package/dist/form.js +1 -1
  62. package/dist/header.js +1 -1
  63. package/dist/icon-data.js +314 -314
  64. package/dist/icon-svg.js +21 -3
  65. package/dist/icons/data/index.d.ts +313 -313
  66. package/dist/icons/data/index.js +313 -313
  67. package/dist/icons.d.ts +2 -2
  68. package/dist/icons.js +6 -4
  69. package/dist/iife.js +74 -66
  70. package/dist/iife.js.map +47 -47
  71. package/dist/index-iife.js +1 -1
  72. package/dist/index.d.ts +49 -49
  73. package/dist/index.js +49 -49
  74. package/dist/live-example/code-transform.d.ts +1 -1
  75. package/dist/live-example/component.d.ts +1 -1
  76. package/dist/live-example/component.js +16 -15
  77. package/dist/live-example/execution.d.ts +1 -1
  78. package/dist/live-example/execution.js +1 -1
  79. package/dist/live-example/index.d.ts +8 -8
  80. package/dist/live-example/index.js +6 -6
  81. package/dist/live-example/insert-examples.d.ts +2 -2
  82. package/dist/live-example/remote-sync.d.ts +1 -1
  83. package/dist/live-example/test-harness.d.ts +1 -1
  84. package/dist/live-example/test-harness.js +1 -1
  85. package/dist/live-example/types.d.ts +2 -2
  86. package/dist/live-example.d.ts +1 -1
  87. package/dist/live-example.js +1 -1
  88. package/dist/live-theme.js +1 -1
  89. package/dist/localize.d.ts +1 -1
  90. package/dist/localize.js +2 -2
  91. package/dist/mapbox.js +1 -1
  92. package/dist/menu.d.ts +2 -2
  93. package/dist/menu.js +5 -5
  94. package/dist/month.d.ts +2 -2
  95. package/dist/month.js +3 -3
  96. package/dist/notifications.js +2 -2
  97. package/dist/pocket-bar.d.ts +1 -1
  98. package/dist/pocket-bar.js +2 -2
  99. package/dist/pop-float.d.ts +1 -1
  100. package/dist/pop-float.js +2 -2
  101. package/dist/rating.js +1 -1
  102. package/dist/rich-text.d.ts +1 -1
  103. package/dist/rich-text.js +3 -3
  104. package/dist/segmented.js +2 -2
  105. package/dist/select.d.ts +1 -1
  106. package/dist/select.js +3 -3
  107. package/dist/sizer.js +2 -2
  108. package/dist/tab-selector.js +2 -2
  109. package/dist/tag-list.js +2 -2
  110. package/dist/tooltip.js +2 -2
  111. package/dist/tsconfig.tsbuildinfo +1 -1
  112. package/dist/value-renderer.js +13 -3
  113. package/dist/version.d.ts +1 -1
  114. package/dist/version.js +1 -1
  115. package/llms.txt +5 -1
  116. package/package.json +19 -12
package/CHANGELOG.md CHANGED
@@ -1,6 +1,279 @@
1
1
  # Changelog
2
2
 
3
- ## 1.9.0-rc.2
3
+ ## 1.9.1
4
+
5
+ **`tosijs-ui` did not import under Node.** Shipped `dist/` used extensionless relative
6
+ imports (`from './site-config'`), which bun resolves and Node ESM does not — so a Node
7
+ consumer got `Cannot find module` on entry points that had nothing to do with bun. Every
8
+ entry point is affected, and this goes back well before 1.8.0.
9
+
10
+ Everything shipped now uses explicit `.js` specifiers, which Node requires and every
11
+ bundler accepts. **If you are on Node, upgrade; 1.9.0 and earlier will not resolve.**
12
+
13
+ Two runtime requirements are unchanged, and are now documented rather than implied — they
14
+ are honest constraints, not bugs, but the failure messages named symptoms rather than
15
+ causes:
16
+
17
+ | entry point | runtime |
18
+ | --- | --- |
19
+ | `tosijs-ui/site` | **bun** — it shells out, builds and spawns |
20
+ | `tosijs-ui`, `tosijs-ui/<component>` | a **browser** or a bundler targeting one |
21
+ | `tosijs-ui/icon-svg` | anything — deliberately DOM-free |
22
+
23
+ `engines` now declares bun, and `doc-site-system.md` has a "Runtimes — what runs where"
24
+ section.
25
+
26
+ **Why four test lanes missed it:** all four run under bun. `test-consumer` packs the real
27
+ tarball, installs it and builds from a foreign cwd — but with bun, so it proved the
28
+ package works for people exactly like us. It now imports every entry point through **Node**
29
+ and asserts the absence of module-resolution errors specifically, separating "packaging is
30
+ broken" from "this needs a DOM / needs bun". Verified against the published 1.9.0: all
31
+ three checks fail on it.
32
+
33
+ ## 1.9.0
34
+
35
+ Consumer repairs, remote access, and **authoring: many books from one corpus**.
36
+
37
+ **Anyone on 1.8.0 should upgrade, whether or not they want the new remote-editing
38
+ feature.** 1.8.0's `tosijs-ui/site` entry point does not import at all in a clean
39
+ install — `index.js` statically re-exports `devServer`, which imports `chokidar` at top
40
+ level, and `chokidar` was declared only in `devDependencies` (#32). 1.8.0 also shipped
41
+ 5.2 MB / 16 files of doc-site hydrate bundle into every consumer's `dist/` (#31), and
42
+ broke the `tosijs-make-icons` bin (#30). All three are fixed here.
43
+
44
+ ### Consumer repairs
45
+
46
+ - **`tosijs-ui/site` imports in a clean install again** (#32). `chokidar` is now an
47
+ optional peer, imported lazily by the watch path only — a plain `buildSite()` never
48
+ reaches for it.
49
+ - **The library package no longer carries the doc-site hydrate bundle** (#31). It built
50
+ into `dist/hydrate/` and shipped to everyone though nothing references it; one adopter's
51
+ package went from 0.62 MB / 398 files to **10.2 MB / 2888 files**. It now builds in a
52
+ temp dir. Caught only by reading `npm pack` output, which is why there is now a lane
53
+ that does exactly that (`bun run test-consumer`).
54
+ - **`tosijs-make-icons` no longer strips `fill-rule`** (#30), which rendered holes in
55
+ compound `evenodd` paths solid — a keyboard drawn as one path came out filled. It was
56
+ stripped in **three** places, not one. `fill-rule` is fill _topology_, not colour, and
57
+ takes no part in the tinting the strip exists for. Ships as a bin, so it affects anyone
58
+ generating their own icon data.
59
+ - **The shipped bins have shebangs** (#35, #36 — reported independently by both adopters).
60
+ Without one, `node_modules/.bin` shims hand TypeScript to the shell.
61
+ - **`/version.json`'s `generator` reports tosijs-ui's version, not the consumer's** (#37).
62
+ It read `package.json` from the _cwd_, so the one field that answers "which tosijs-ui
63
+ built this?" named the wrong package entirely.
64
+ - **A failed "save to source" says why** (#34). The server answers 501 with an actionable
65
+ reason and the client discarded it for a generic "Save failed.", so an unconfigured
66
+ server was indistinguishable from a broken one and the edit appeared to vanish.
67
+ - **New: `iconSvg(name)` / `iconNames()`** (#33) — raw SVG markup with **no DOM
68
+ required**, for build scripts, ePub passes and server-rendered templates. `defineIcons()`
69
+ could write to the icon map but nothing could read it.
70
+
71
+ ### Remote access — view and edit your dev server from anywhere
72
+
73
+ `preview.tunnel` exposes the dev server running on your machine at an authenticated public
74
+ URL over an SSH reverse tunnel, so you can read and edit real source from a phone or a
75
+ borrowed laptop. The box does no compute — it terminates TLS and routes — which is what
76
+ lets one small VPS front many projects. Two hostnames make the posture legible:
77
+
78
+ | host | what it is | gate |
79
+ | -------------------------------- | ----------------------------- | --------------- |
80
+ | `<project>.dev.example.com` | read-only snapshot, shareable | invite cookie |
81
+ | `<project>.edit.dev.example.com` | live workspace, yours | session, always |
82
+
83
+ `tosijs-tunnel --link` prints a single-use link; opening it once exchanges the token for a
84
+ durable `HttpOnly; Secure; SameSite=Lax` session cookie and redirects with the token
85
+ stripped, so it never lands in history or a `Referer`. `tosijs-deploy` publishes the static
86
+ snapshot and self-registers a Caddy fragment, so adding a project edits no shared file.
87
+
88
+ **Write authorization keys on the LISTENER, never on a peer address or a header.** Tunnel
89
+ traffic arrives on a dedicated loopback listener, and anything arriving there needs a valid
90
+ session — because a reverse tunnel counterfeits "local" by construction.
91
+
92
+ ### Security
93
+
94
+ - The tunnelled workspace's **read** gate keyed on `X-Forwarded-*` rather than the
95
+ listener, so any forwarder that omits those headers — `ssh -R` with `GatewayPorts yes`,
96
+ `ngrok tcp`, `socat`, iptables DNAT, nginx `proxy_pass`, HAProxy without
97
+ `option forwardfor` — read the entire uncommitted working tree unauthenticated, while
98
+ `requireToken` promised "nothing at all, not even the page". Now keyed on the listener,
99
+ like every other gate.
100
+ - `POST /report` was unauthenticated and reachable through the tunnel — a stranger could
101
+ fabricate `{passed:N, failed:0}` and make the test lane **exit green on a suite that
102
+ never ran**. Now local-only.
103
+ - Build-error text (absolute paths from your machine) was injected into every served page
104
+ with no auth check. The label stays public; the detail needs a session.
105
+ - `rsync --delete` accepted any absolute path two segments deep — including `/usr/lib` and
106
+ `/etc/caddy`, which it would have _mirrored_, i.e. emptied. Now an allowlist of preview
107
+ roots, and never the root itself: one dropped path segment would otherwise have deleted
108
+ every other project on a shared box, including the Caddy fragments that route them.
109
+ - `tunnel.requireToken` defaults to **`true`**. The hostname is not a secret — Let's
110
+ Encrypt publishes every certificate to public CT logs — so the session gate carries the
111
+ weight. See the docs for what the hostname discloses and what to do about it (#38).
112
+
113
+ ### Fixed
114
+
115
+ - **A 403 could destroy uncommitted work.** An unauthorized read fell through to GitHub
116
+ `main`, so you edited the _published_ file, and the save then handed you a download of
117
+ it — applying which silently reverts your working copy.
118
+ - **`?t=` was intercepted on every request and method**, so any dev server answered
119
+ `GET /?t=12345` with "that invite link has been used" instead of the page, and 401'd
120
+ POSTs carrying `t`. (`t` is the classic cache-buster name.)
121
+ - **Tunnel ports are derived, not fixed** (#39). The bin fell back to a hard-coded 8788
122
+ while the server used `PORT + 1`; they agreed only when `PORT` was 8787. `remotePort` is
123
+ now derived per project too, so two projects on one host cannot collide. One resolver
124
+ serves both, and `tosijs-tunnel --status` reports the ports it would use.
125
+ - **The last-good build protection moved to where the wipe is** — it lived in the dev
126
+ server's watch branch, so `bun run build`, CI, adopters and `bun start`'s own initial
127
+ build had none of it. A red `tsc` no longer discards a freshly generated site either.
128
+ - Two dev servers can coexist; the idle-exit timer counts requests again, not just builds;
129
+ "Build failed" is no longer overwritten by test results seconds later.
130
+
131
+ ### ⚠️ `hidden` now actually withholds
132
+
133
+ **If you have been using `draft: true` or `hidden: true` for working notes or unfinished
134
+ chapters, they were public.** `hidden` only removed a doc from the nav and the book — its
135
+ full text was still written into `docs.json`, and it still got a pre-rendered page at its
136
+ own URL. The code comment claimed "drafts don't ship"; a probe confirmed they did, twice
137
+ over.
138
+
139
+ `hidden: true` now means **not published at all**: absent from `docs.json`, from the
140
+ generated pages, from every book, and from `llms.txt`. It is filtered at _extraction_,
141
+ because the corpus is the thing that gets published and any filter applied after it is
142
+ written is too late.
143
+
144
+ It is inherited, and a child **cannot** un-hide itself from a hidden parent — accidentally
145
+ publishing one chapter of a withheld section is the failure worth preventing.
146
+
147
+ ### Books
148
+
149
+ A `book` value on a doc selects which volume it binds into. Volumes are discovered from
150
+ the corpus, so a second book needs no configuration:
151
+
152
+ ```text
153
+ <!--{ "book": "field-guide" }--> → its own volume
154
+ <!--{ "book": ["default", "field-guide"] }--> → bound into both
155
+ <!--{ "book": "none" }--> → on the site, in no book
156
+ ```
157
+
158
+ Like `hidden`, it is inherited down the `parent` chain — you mark a section, not every
159
+ leaf — with the **nearest declaration winning outright**, so a chapter can divert to
160
+ another volume, join several, or opt out. A list is what gets you shared front matter: a
161
+ glossary or licence page bound into every volume from one source file rather than copied
162
+ per book. `"none"` anywhere in a list wins, because a contradiction should resolve to
163
+ withholding. `"default"` exists as a writable name for the main volume precisely so a list
164
+ can include it — without one, an array could only ever express "not the default".
165
+
166
+ `config.book` (the manifest that curates and orders docs _within_ a volume) and a doc's
167
+ `book` metadata (_which_ volume) are different things that share a word; both are
168
+ documented.
169
+
170
+ ### Release notes assemble themselves
171
+
172
+ `tosijs-release-notes` builds a CHANGELOG section from `[tag]` bullets in commit bodies,
173
+ so notes are written next to the diff rather than reconstructed from memory at the end —
174
+ which is where three false claims in the 1.9.0 notes came from. One bullet per
175
+ separately-interesting _thing_, layered on the conventional subject rather than replacing
176
+ it: a `type:` prefix can only ever represent one change, and the 1.9.0 review commit did
177
+ ten things.
178
+
179
+ tosijs-release-notes # assemble the section
180
+ tosijs-release-notes --check # gate: nothing since the last tag is unaccounted for
181
+
182
+ `--check` fails when an annotation is missing from the changelog, and separately reports
183
+ any commit whose `[fix]`/`[new]` bullets are contradicted by a **markdown-only diff** —
184
+ the shape of the worst 1.9.0 bug, where an auth-gate fix was asserted by a commit whose
185
+ diff never touched the file it named. Matching is deliberately loose: rewriting a bullet
186
+ into prose is the intent, and demanding a literal match would only train people to paste.
187
+
188
+ This section was assembled by the tool and then written up by hand, which is the intended
189
+ workflow — it generates a skeleton and a coverage gate, never the prose. The gate then
190
+ caught two things this very section had dropped, including a bug found by running the tool
191
+ over this repo's own 2026-04/05 history: a bullet used as the commit _subject_ swallowed
192
+ the entire body as continuation text, so one note came out as three paragraphs of
193
+ implementation detail.
194
+
195
+ ### Also
196
+
197
+ - `tosijs-make-icons` emits clean, stable, formatter-conformant output, and honours your
198
+ prettier config if prettier is resolvable. Generated icon data no longer churns against
199
+ a formatter (and should still be added to `.prettierignore`).
200
+ - `resolveBook` → `resolveBooks`, returning a list. New in this release; no migration.
201
+
202
+ ### Late fixes (rc.3 → rc.4)
203
+
204
+ Three review passes ran against this release. The last two found defects **introduced by
205
+ the previous pass's fixes**, which is why these are listed separately rather than folded
206
+ in silently.
207
+
208
+ **⚠️ `deploy/Caddyfile` is now a TEMPLATE — action required if you installed it.** It
209
+ shipped in every tarball carrying a real ACME email, a specific preview domain, and a
210
+ literal `__PREVIEW_TOKEN__`, while its own header told you to `scp` it to `/etc/caddy`.
211
+ Following that gave you a preview host whose entire invite gate was a string published in
212
+ a public repo, issuing certificates under someone else's Let's Encrypt account. Set
213
+ `PREVIEW_TOKEN`, `ACME_EMAIL` and `PREVIEW_DOMAIN` in `/etc/caddy/preview.env` and
214
+ re-install with `deploy:caddy`, which now substitutes all three, **refuses to install if
215
+ any placeholder survives**, and validates into a temp file before replacing the live
216
+ config rather than after.
217
+
218
+ - **Every ePub volume shipped an identical `dc:identifier`, `dc:title` and cover.**
219
+ `unique-identifier` is EPUB3's primary key, so importing two volumes into Apple Books
220
+ or Calibre made the second replace the first — you would silently lose a book. Volumes
221
+ now derive their own identity; `epub.volumeTitles` names them properly.
222
+ - **An ePub volume matching no documents shipped as a success** — a typo'd book name
223
+ produced a valid ~10KB book with a cover, a nav and no chapters, at exit 0. It now fails
224
+ and lists the volumes that do exist. Relatedly, a corpus where _every_ doc names a
225
+ volume no longer errors on its empty default bucket.
226
+ - **An invite link was unredeemable** on a proxied dev server with no `preview.tunnel`
227
+ block: the lock armed off a config section that did not exist while the `?t=` reader,
228
+ correctly gated on it, refused to look. One predicate now, exported and tested.
229
+ - **`tosijs-tunnel` ignored `PORT` and `--port`**, targeting one port while the server
230
+ listened on another — the drift #39 was meant to end, reintroduced by the commit that
231
+ claimed to end it.
232
+ - **The deploy and tunnel bins printed ✅ over failed remote steps.** `tosijs-deploy` now
233
+ distinguishes _deployed_ from _routed_ and exits non-zero when the files landed but
234
+ nothing serves them; `tosijs-tunnel` marks a public URL that is not routed.
235
+ - **Preview roots other than `/srv/preview` registered successfully and routed nothing** —
236
+ the write side was de-hardcoded while the Caddyfile glob and index script still named
237
+ the default. One place decides now, and a mismatch says so. Override with
238
+ `preview.caddySitesDir`.
239
+ - **`release-check` cleared an empty range** at exactly the release boundary, because
240
+ `git describe` returns the nearest tag including prereleases. It now baselines on the
241
+ last stable release _this commit descends from_, so a hotfix tagged out of order or a
242
+ maintenance branch cannot mislead it.
243
+ - **Publishing now gates on `release-check`.** It was added mid-cycle, and the first thing
244
+ it caught was nine unrecorded annotations including a `[break]` — the `deploy/Caddyfile`
245
+ change above, which would otherwise have shipped with no migration note at all.
246
+ - **The release gate hardened against itself.** `release-check` is now baselined on the
247
+ last stable release this commit _descends from_ (`git describe --exclude`), so a hotfix
248
+ tagged after a later release, or a maintenance branch, cannot mislead it — a repo-wide
249
+ tag scan got both wrong. Its coverage matcher also stopped crying wolf: it filtered
250
+ short words out of the annotation but not out of the changelog, so entries that were
251
+ plainly written up got reported as missing.
252
+ - **`isLockedDown()` and `hasTunnel()` are exported** from the auth module. The predicate
253
+ existed in three places, one of them recomputed inside its own regression test — so
254
+ reverting the fix left the entire suite green. The test imports the real function now.
255
+ - **The browser-test lane keys on haltija's `ready` signal** (haltija ≥ 1.6.1, added in
256
+ response to us filing it): a server that is up but has no connected tab is no longer
257
+ adopted, because `hj navigate` then fails with "no browser reachable". That cost three
258
+ lane runs in a day, each needing a manual `pkill`. The floor also named 1.5.5, a version
259
+ that was never published.
260
+ - **New "Host bootstrap" docs**, because the preview host needs setting up once before
261
+ any project can register. Both bins write fragments that `import preview_site` /
262
+ `import tunnel_site` — snippets that only exist once the host has a Caddyfile — so an
263
+ un-bootstrapped box failed `caddy validate` forever, for every project, silently. That
264
+ failure now names the missing prerequisite.
265
+ - `epub.volumeTitles` is now accepted by `defineSiteConfig` — it was documented and
266
+ type-rejected. `extractDocs`'s hidden-filtering gained tests at the tier that matters:
267
+ real files, real entry point, verified by mutation.
268
+
269
+ ### For maintainers of adopting projects
270
+
271
+ `bun run test-consumer` packs the tarball, installs it into a scratch project, and builds
272
+ from that project's cwd. Every other lane runs _in this repo, from this repo, with one dev
273
+ server_ — and four regressions shipped anyway, each living outside exactly that envelope.
274
+ More unit tests would have caught none of them; the gap was context, not depth.
275
+
276
+ ### 1.9.0-rc.2 (prerelease detail)
4
277
 
5
278
  Fixes everything both consumers hit within minutes of `rc.1`. Three were regressions
6
279
  introduced by rc.1 itself.
@@ -21,8 +294,8 @@ introduced by rc.1 itself.
21
294
  broken".
22
295
  - **New lane: `bun run test-consumer`** — packs the tarball, installs it into a scratch
23
296
  project, runs every bin through the `node_modules/.bin` shims, and builds a site from
24
- that project's cwd. Every other lane runs *in this repo, from this repo, with one dev
25
- server*, and all four rc.1-era regressions lived outside that envelope. It found a real
297
+ that project's cwd. Every other lane runs _in this repo, from this repo, with one dev
298
+ server_, and all four rc.1-era regressions lived outside that envelope. It found a real
26
299
  bug on its first run.
27
300
  - **A failed "save to source" now says why** (#34). The endpoint answers 501 with an
28
301
  actionable sentence ("editableSources is not enabled…") and the client threw it away
@@ -30,8 +303,7 @@ introduced by rc.1 itself.
30
303
  broken one and the edit appeared to vanish. The server's reason is surfaced, and every
31
304
  failure path now states that the edit is still in the editor.
32
305
 
33
-
34
- ## 1.9.0-rc.1
306
+ ### 1.9.0-rc.1 (prerelease detail)
35
307
 
36
308
  Release candidate. Everything the nine-lens review and the first adopters raised is
37
309
  addressed; no features are pending. Install with `tosijs-ui@rc`.
@@ -39,10 +311,10 @@ addressed; no features are pending. Install with `tosijs-ui@rc`.
39
311
  **Edit and view are now separate hostnames**, which makes the security posture legible
40
312
  without reading config:
41
313
 
42
- | host | what it is | gate |
43
- | --- | --- | --- |
44
- | `<project>.dev.example.com` | read-only static preview — shareable | invite cookie |
45
- | `<project>.edit.dev.example.com` | live editable workspace — yours | session, always |
314
+ | host | what it is | gate |
315
+ | -------------------------------- | ------------------------------------ | --------------- |
316
+ | `<project>.dev.example.com` | read-only static preview — shareable | invite cookie |
317
+ | `<project>.edit.dev.example.com` | live editable workspace — yours | session, always |
46
318
 
47
319
  - **`tunnel.requireToken` now defaults to `true`.** The old default (read-open) was
48
320
  justified by "an expired link should degrade to a readable page when you open a second
@@ -53,10 +325,10 @@ without reading config:
53
325
  deliberately if you want a live read-only audience.
54
326
  - **An edit host with no tunnel behind it explains itself** — a 503 saying the workspace
55
327
  is offline, with a link to the static preview, instead of a bare 502. It deliberately
56
- does *not* fall back to serving the snapshot: you would think you were looking at live
328
+ does _not_ fall back to serving the snapshot: you would think you were looking at live
57
329
  work when you were not.
58
330
  - **A spent invite link says so.** The likely culprit is a chat-app link-preview bot,
59
- whose GET *is* the first use; previously you got a silent redirect and discovered it
331
+ whose GET _is_ the first use; previously you got a silent redirect and discovered it
60
332
  when a save failed.
61
333
 
62
334
  **Security fixes** (all found by review or adopters, none reported in the wild):
@@ -67,14 +339,14 @@ without reading config:
67
339
  - Build-error text (absolute paths from your machine) was injected into every served page
68
340
  with no auth check. The label stays public; the detail needs a session.
69
341
  - `rsync --delete` accepted any absolute path two segments deep — including `/usr/lib`
70
- and `/etc/caddy`, which it would have *mirrored*, i.e. emptied. Now an allowlist of
342
+ and `/etc/caddy`, which it would have _mirrored_, i.e. emptied. Now an allowlist of
71
343
  preview roots, extracted and tested.
72
344
 
73
345
  **Adopter-reported fixes** (all from tosijs-3d, all consumer-facing):
74
346
 
75
347
  - **`tosijs-make-icons` stripped `fill-rule`** (#30), so a compound path using `evenodd`
76
348
  for holes — a keyboard drawn as one path — rendered the holes solid. `fill-rule` is
77
- fill *topology*, not colour, and takes no part in the tinting the strip exists for.
349
+ fill _topology_, not colour, and takes no part in the tinting the strip exists for.
78
350
  It turned out to be stripped in **three** places, not one; the `stroked/` branch is the
79
351
  one that bit. This ships as a bin, so it affects anyone generating their own icon data.
80
352
  - **The doc-site hydrate bundle was written into the library `dist/`** (#31) and shipped
@@ -92,7 +364,7 @@ without reading config:
92
364
  **Correctness:**
93
365
 
94
366
  - **A 403 could destroy uncommitted work.** An unauthorized read fell through to GitHub
95
- `main`, so you edited the *published* file, and the save then handed you a download of
367
+ `main`, so you edited the _published_ file, and the save then handed you a download of
96
368
  it — applying which silently reverts your working copy. 401/403 is now its own case in
97
369
  both editors.
98
370
  - **The last-good build protection moved to where the wipe is.** It lived in the dev
@@ -104,8 +376,7 @@ without reading config:
104
376
  - `?t=` is only intercepted when a tunnel is configured, and only on GET — `t` is the
105
377
  classic cache-buster name, and a 302'd POST loses its body.
106
378
 
107
-
108
- ## 1.9.0-beta.3
379
+ ### 1.9.0-beta.3 (prerelease detail)
109
380
 
110
381
  Fixes three blockers found by the nine-lens review of beta.2, all in the remote-editing
111
382
  feature, plus a rebuild loop the review did not catch because nothing automated runs
@@ -113,20 +384,19 @@ feature, plus a rebuild loop the review did not catch because nothing automated
113
384
 
114
385
  - **`bun run tunnel --link` never worked.** It ran `pgrep -f 'bun bin/dev.ts'`, which does
115
386
  not match the documented start command (`bun --watch bin/dev.ts`) — so the headline flow
116
- was unreachable. Worse, it *did* match `bun bin/dev.ts --build-only`, whose process exits
387
+ was unreachable. Worse, it _did_ match `bun bin/dev.ts --build-only`, whose process exits
117
388
  before the signal handler is registered, so the default SIGUSR2 disposition **killed
118
389
  in-flight builds**. Now it asks the dev server over a loopback-only `/__devlink` endpoint:
119
390
  no process guessing, no signals, and it cannot touch a sibling project. (`/__devlink` is
120
391
  refused over the tunnel — a read-only visitor minting a write session would be privilege
121
392
  escalation.)
122
- - **Write authorization failed OPEN.** It inferred "this is local" from the *absence* of
393
+ - **Write authorization failed OPEN.** It inferred "this is local" from the _absence_ of
123
394
  `X-Forwarded-*`, so any forwarder that doesn't set them — `ssh -R` with `GatewayPorts yes`,
124
395
  `ngrok tcp`, `socat`, iptables DNAT, nginx without `forwardfor` — delivered an off-machine
125
396
  request as `{peer: 127.0.0.1, no headers}` and got an **arbitrary repo write**, which the
126
397
  watcher rebuilds and runs. The dev server now binds a **separate loopback-only port** for
127
398
  tunnel traffic; arriving there always requires a session. Which socket you connected to is
128
- not something a client can forge. `bun run tunnel` also verifies the remote port is bound
129
- to loopback and says so loudly if it isn't.
399
+ not something a client can forge.
130
400
  - **The decision guarding that RCE had no tests**, while its own comment claimed a
131
401
  regression test that did not exist. It is now `mayWriteSource()` — pure, exported, and
132
402
  covered including a named regression case for the fail-open above.
@@ -140,13 +410,13 @@ feature, plus a rebuild loop the review did not catch because nothing automated
140
410
  workspace, so silently adopting a neighbour's config would publish the wrong project.
141
411
  - **Fix: the tunnel probed the wrong port**, so forwarding to a dead listener failed
142
412
  silently with a 502 and a healthy-looking dev server (tosijs-ui#28). `ssh -R` can't
143
- catch it — the *remote* bind succeeds, so `ExitOnForwardFailure` stays quiet; it's the
413
+ catch it — the _remote_ bind succeeds, so `ExitOnForwardFailure` stays quiet; it's the
144
414
  local end that connects to nothing. It now probes the port it actually forwards to and
145
415
  **refuses to open**, naming the likely cause (including "your tosijs-ui predates the
146
416
  tunnel listener").
147
417
  - **Tunnelled workspaces self-register too** (tosijs-ui#29). Static previews have
148
418
  self-registered since beta.1, but a tunnelled host still needed a block hand-added to
149
- the *shared* Caddyfile — so the one remaining manual edit was the **riskier** one: a
419
+ the _shared_ Caddyfile — so the one remaining manual edit was the **riskier** one: a
150
420
  typo there breaks routing for every project on the box, which is exactly what the
151
421
  fragment design set out to abolish. `tosijs-tunnel` now writes its own fragment with
152
422
  the same validate-before-reload discipline, and the shared file names no hostnames at
@@ -162,13 +432,12 @@ feature, plus a rebuild loop the review did not catch because nothing automated
162
432
  watcher path and declared an **optional peer**, so building needs nothing extra and the
163
433
  watching dev server explains itself if it is missing.
164
434
  - **Fix: `bun start` was in a rebuild loop** (measured at 899 restarts in ~40s), which made
165
- the documented dev command unusable. The build stamp imported the *generated*
435
+ the documented dev command unusable. The build stamp imported the _generated_
166
436
  `src/version.ts`, putting it in `bun --watch`'s module graph while prebuild rewrote it every
167
437
  build. The import is gone, and generated files are now written **only when the content
168
438
  changes**, which makes the whole class of self-write loops impossible.
169
439
 
170
-
171
- ## 1.9.0-beta.2
440
+ ### 1.9.0-beta.2 (prerelease detail)
172
441
 
173
442
  > Prerelease — install with `tosijs-ui@beta`. `latest` stays on 1.8.0.
174
443
 
@@ -182,7 +451,7 @@ same watcher, same files.
182
451
  which is what lets one small VPS serve many projects. The work stays where the data is.
183
452
  - **Magic-link auth.** Two tokens on purpose: the **link** rides in a URL so it is the
184
453
  one that leaks (history, `Referer`, proxy logs, chat-app link previews) and is
185
- therefore *single-use and 15 minutes*; the **session** never appears in a URL, arrives
454
+ therefore _single-use and 15 minutes_; the **session** never appears in a URL, arrives
186
455
  as an `HttpOnly` cookie, and is durable. A durable token pasted into a URL would be
187
456
  strictly worse than basic auth. `SameSite=Lax` gives the write endpoint free CSRF
188
457
  protection.
@@ -205,8 +474,7 @@ same watcher, same files.
205
474
  session) — Caddy serving static files has no session store — but equivalent to the
206
475
  basicauth it replaces, with no dialog.
207
476
 
208
-
209
- ## 1.9.0-beta.1
477
+ ### 1.9.0-beta.1 (prerelease detail)
210
478
 
211
479
  > **Prerelease.** The preview-host system works end to end and is in daily use, but the
212
480
  > plan it belongs to (`REMOTE-ACCESS-PLAN.md`) has two more phases. Published under the
@@ -284,7 +552,7 @@ same watcher, same files.
284
552
  shown), plus an **advisories-per-package tally** — a dependency that keeps producing them
285
553
  is a code smell worth replacing rather than patching.
286
554
  - **Fix (issue #24, reported by a consumer): nav `order` now sorts numerically.** Section nav
287
- and the generated `<!-- toc -->` built a zero-padded *string* key and compared it lexically,
555
+ and the generated `<!-- toc -->` built a zero-padded _string_ key and compared it lexically,
288
556
  so a fractional order silently sorted to the **end** of its section — `order: 1.5` landed
289
557
  after `2`, `3`, … because `"01.5" > "0002"` as text. "Insert a page at N.5" reads like a
290
558
  supported idiom and did the opposite with no warning. The same padding capped `order` at 4
@@ -299,7 +567,7 @@ same watcher, same files.
299
567
  above was effectively unfindable. The comparator needs no delimiter; the NUL is gone.
300
568
  - **Adopting the gate — budget for one upgrade, not one pin.** From the first real
301
569
  adoption (`tosijs`, which went from 12 blocking advisories to zero): if the gate flags
302
- **`brace-expansion`**, note that `GHSA-mh99-v99m-4gvg` marks *every* version below
570
+ **`brace-expansion`**, note that `GHSA-mh99-v99m-4gvg` marks _every_ version below
303
571
  **5.0.8** affected, so the override is mandatory — and `brace-expansion@5` breaks
304
572
  **eslint 8**'s bundled `minimatch@3` with `expand is not a function`. In practice that
305
573
  means **an eslint 8 → 10 migration** (flat config + typescript-eslint 8), not a one-line
@@ -360,13 +628,13 @@ same watcher, same files.
360
628
  it never sits over the code editors in side-by-side mode). The collapsed handle carries the
361
629
  example's pass/fail color, "run tests" is a checkbox that greys when off, and the bar stays
362
630
  visible when the example is maximized.
363
- - **Build system is fragility-tolerant.** An *unsupported import* in a live example (a bare
631
+ - **Build system is fragility-tolerant.** An _unsupported import_ in a live example (a bare
364
632
  specifier the runtime can't resolve) now downgrades that block to display-only with a warning
365
633
  instead of failing the whole build; genuine syntax errors still fail the build.
366
634
  - **Removed a render-skip anti-pattern** from `<tosi-segmented>`, `<tosi-color>`, and
367
- `<tosi-form>`. They previously depended on `render()` being *skipped* after interaction via a
635
+ `<tosi-form>`. They previously depended on `render()` being _skipped_ after interaction via a
368
636
  `valueChanged` flag — fragile timing that broke under tosijs's parts changes. Renders are now
369
- idempotent: they skip only *provably-redundant* writes by comparing values (colors compared
637
+ idempotent: they skip only _provably-redundant_ writes by comparing values (colors compared
370
638
  **parsed**, so a `#rrggbb ↔ rgba()` round-trip no longer clobbers the caret/selection in the
371
639
  field being edited).
372
640
  - **Code editor background is now a neutral off-white** (off-black in dark mode) instead of a
@@ -391,7 +659,7 @@ same watcher, same files.
391
659
  build was locked to one mount — adding a custom domain to a GitHub project page flipped the
392
660
  live site to the root while `docs/` still pointed at `/repo`, 404ing every asset into a bare,
393
661
  unstyled shell with no warning. Functional URLs are now emitted **relative to each page**, so
394
- one build works at `/repo`, a custom-domain root, or a moved mount with no rebuild. *Metadata*
662
+ one build works at `/repo`, a custom-domain root, or a moved mount with no rebuild. _Metadata_
395
663
  URLs (`canonical`, `og:url`, `og:image`, `sitemap.xml`) stay absolute via `baseUrl`+`basePath`
396
664
  — so `basePath` now only affects metadata, never the page's assets. (The hydrated SPA's own
397
665
  nav/`pushState` and `__TJS_LOCAL_BASE` remain root-relative — tracked in #16.)
@@ -424,7 +692,7 @@ same watcher, same files.
424
692
  - **Fix: "edit page source" showed HTML instead of source in a dev server without
425
693
  `editableSources`.** The `/__docstore/source` endpoint was only routed when
426
694
  `editableSources` was on; otherwise the request fell through to the SPA `index.html`
427
- fallback and returned the rendered page at status 200 — so the client loaded HTML *as* the
695
+ fallback and returned the rendered page at status 200 — so the client loaded HTML _as_ the
428
696
  source (and save read HTML). The endpoint is now handled unconditionally and answers a clean
429
697
  `501` when editing is disabled, so `loadSource` falls back to the GitHub raw source (read-only
430
698
  editing, matching the deployed site). Belt-and-suspenders: the client also rejects any
@@ -432,8 +700,8 @@ same watcher, same files.
432
700
  more fully in `doc-site-system.md`.
433
701
  - **Local example save now gives feedback.** "Save changes (local)" writes a per-browser
434
702
  scratchpad (not the file) — it used to do so silently, reading as a no-op. It now posts a
435
- toast (`postNotification`): *"Saved to this browser only — use 'Save to source' to write the
436
- file."*
703
+ toast (`postNotification`): _"Saved to this browser only — use 'Save to source' to write the
704
+ file."_
437
705
 
438
706
  - **`valueRenderer(type)` + declarative `<tosi-table>` column `type`.** A new exported helper
439
707
  turns a compact type string into a reusable, locale-aware renderer with a sensible default
@@ -449,7 +717,7 @@ same watcher, same files.
449
717
 
450
718
  - **Deprecation cleanup: `on<Event>` component callbacks → `handle<Event>`.** tosijs reserves the
451
719
  `on<Event>` prefix for elements-factory event-handler sugar and now warns when a component
452
- *defines* such a property. Renamed the internal ones: `onResize` → `handleResize` (`size-break`,
720
+ _defines_ such a property. Renamed the internal ones: `onResize` → `handleResize` (`size-break`,
453
721
  `side-nav`, `code-editor`, `babylon-3d`, `tab-selector`) and `onScrollEnd` → `handleScrollEnd`
454
722
  (`data-table`). Verified: those pages now load with zero `on<Event>` deprecation warnings.
455
723
  (`<tosi-tabs>`'s public `onCloseTab` still uses the old name — a breaking rename deferred to a
@@ -517,7 +785,7 @@ the real transpiler + editor on demand; saving keeps the transpiled JS.
517
785
  With `importResolver` enabled, a live example can `import` any npm package — resolved through a
518
786
  same-origin service worker (tjs-lang's import-resolver). Three execution modes are flaggable on
519
787
  the fence: `inline` (default), `iframe` (DOM/CSS isolation), `ide` (recognized; iframe path for
520
- now). Fence grammar is order-free: `` ```ts:ide#demo `` and `` ```ts#demo:ide `` both parse.
788
+ now). Fence grammar is order-free: ` ```ts:ide#demo ` and ` ```ts#demo:ide ` both parse.
521
789
 
522
790
  ### Doc-system & build
523
791