tosijs-ui 1.10.0 → 1.12.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 (50) hide show
  1. package/CHANGELOG.md +613 -0
  2. package/LICENSE +202 -21
  3. package/NOTICE +79 -0
  4. package/bin/caddy-install.ts +9 -11
  5. package/bin/deploy-preview.ts +13 -11
  6. package/bin/release-notes.ts +43 -0
  7. package/bin/resolve-site-config.ts +84 -1
  8. package/bin/tunnel.ts +42 -12
  9. package/dist/.metadata_never_index +0 -0
  10. package/dist/code-editor-cm.d.ts +17 -8
  11. package/dist/crud.d.ts +155 -0
  12. package/dist/crud.js +822 -0
  13. package/dist/data-table.d.ts +37 -0
  14. package/dist/data-table.js +472 -19
  15. package/dist/doc-system/doc-system.d.ts +3 -0
  16. package/dist/doc-system/doc-system.js +68 -2
  17. package/dist/doc-system/release-notes.d.ts +34 -0
  18. package/dist/doc-system/release-notes.js +82 -0
  19. package/dist/doc-system/site/dev-auth.d.ts +101 -1
  20. package/dist/doc-system/site/dev-auth.js +240 -8
  21. package/dist/doc-system/site/dev-server.js +106 -6
  22. package/dist/doc-system/site/site-config.d.ts +9 -3
  23. package/dist/hash-state.d.ts +29 -0
  24. package/dist/hash-state.js +210 -0
  25. package/dist/iife.js +153 -150
  26. package/dist/iife.js.map +21 -13
  27. package/dist/index-iife.js +21 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +3 -0
  30. package/dist/live-example/code-transform.js +1 -1
  31. package/dist/localize.d.ts +19 -1
  32. package/dist/localize.js +150 -1
  33. package/dist/router.js +9 -1
  34. package/dist/schema-form/fields.d.ts +187 -0
  35. package/dist/schema-form/fields.js +593 -0
  36. package/dist/schema-form/json-schema.d.ts +41 -0
  37. package/dist/schema-form/json-schema.js +25 -0
  38. package/dist/schema-form/plugins.d.ts +45 -0
  39. package/dist/schema-form/plugins.js +55 -0
  40. package/dist/schema-form/unenforced.d.ts +11 -0
  41. package/dist/schema-form/unenforced.js +104 -0
  42. package/dist/schema-form/validator.d.ts +58 -0
  43. package/dist/schema-form/validator.js +111 -0
  44. package/dist/schema-form.d.ts +148 -0
  45. package/dist/schema-form.js +1472 -0
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/llms.txt +8 -2
  50. package/package.json +18 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,618 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.0
4
+
5
+ ### tosijs-ui is Apache-2.0 as of this release (MIT through 1.11.1)
6
+
7
+ **Nothing already published changes licence.** 1.11.1 and everything before it stay MIT under
8
+ the terms they shipped with — a licence grant already made cannot be withdrawn, and this is not
9
+ an attempt to. The change applies from 1.12.0 forward.
10
+
11
+ **Why.** tosijs went Apache-2.0 in its 1.8.0, and `dist/iife.js` inlines tosijs, so this package
12
+ already redistributes Apache-2.0 code and already had to carry its NOTICE. Matching the licence
13
+ of the thing we are built on removes the mismatch rather than papering over it, and it is the
14
+ right direction on its own terms: Apache-2.0 adds an explicit **patent grant** and a
15
+ patent-retaliation clause that MIT simply does not address.
16
+
17
+ **What it means for you, concretely:**
18
+
19
+ - **Using tosijs-ui in an app, or hosting what you build with it — unaffected.** No new
20
+ obligation.
21
+ - **Redistributing it** (vendoring, forking, shipping a bundle that contains it) — §4(d) asks
22
+ you to carry the attribution text from `NOTICE`, and §4(b) to mark files you modified.
23
+ - **GPLv2-only projects can no longer use it.** This is the one real loss and it is worth
24
+ stating plainly rather than burying: Apache-2.0 is incompatible with GPLv2-only. GPLv3+ is
25
+ fine. If you are on GPLv2-only, 1.11.1 remains MIT and remains available.
26
+ - **You gain a patent grant** you did not have under MIT.
27
+
28
+ The root `NOTICE` now carries our own attribution plus tosijs, marked, Feather Icons and
29
+ CodeMirror. It ships in the package (`files`), so a redistributor has it without hunting.
30
+
31
+ ### Upstreams: verified against tosijs 1.8.0 and tjs-lang 0.13.4
32
+
33
+ Both are admitted by our existing peer ranges (`^1.7.8`, `^0.13.1`) — so adopters of 1.11.0 resolve to them today whether or
34
+ not we bump. All six lanes pass against both: unit (1118), Playwright ×2, haltija (60),
35
+ consumer (44 checks), typecheck, format.
36
+
37
+ The peer floors are **unchanged**. Nothing here needs anything new in either release, and the
38
+ floor encodes required fixes rather than a date. `TJS_VERSION` in
39
+ `src/live-example/code-transform.ts` moved to 0.13.4 in lockstep with the dep — it is the CDN
40
+ pin used when an adopter has no tjs-lang peer installed, and the two silently diverging would
41
+ mean testing one transpiler and shipping another. Both CDN assets verified reachable at that
42
+ version.
43
+
44
+ ### Licence: a NOTICE file, because the iife redistributes Apache-2.0 code
45
+
46
+ **tosijs is Apache-2.0 as of 1.8.0** (BSD-3-Clause through 1.7.x). `dist/iife.js` inlines
47
+ tosijs and marked so a page can use the components from a single `<script>` tag, which makes
48
+ this package a redistributor, and Apache-2.0 §4(d) asks redistributors to carry the NOTICE
49
+ text of what they redistribute. There is now a `NOTICE` file at the root, shipped in the
50
+ package, reproducing tosijs's verbatim.
51
+
52
+ **This does not travel with `import 'tosijs-ui'`.** The ESM build bundles neither — tosijs and
53
+ marked are peer dependencies there, resolved from the consumer's own `node_modules`. The
54
+ obligation attaches to the iife and to anything built from it. tosijs-ui itself remains MIT.
55
+
56
+ ### Known: the iife grew
57
+
58
+ `dist/iife.js` is **419.1 KB gzipped, up from 402.3 KB** — +16.8 KB (+4.2%) — entirely from
59
+ tosijs 1.8.0, whose own notes measure +2.9 kB on an app bundle that touches no new API (the
60
+ contract seam, path-segment guard and binding bookkeeping sit on the ordinary path). Ours is
61
+ larger because the iife bundles more of tosijs than a typical app does. Recorded rather than
62
+ absorbed silently: the gate on weight here has always been the printed gzip delta.
63
+
64
+ ### Note for anyone on tosijs 1.7.x
65
+
66
+ tosijs 1.8.0 **deviates from semver and says so** — it removes `data-ref` and `<xin-slot>`
67
+ markup handling, reduces `<xin-blueprint>`/`<xin-loader>` to warning tombstones, and flips
68
+ `on<Event>` member precedence and type-contradicting attribute writes. A consumer on
69
+ `^1.7.9` receives all of it on a routine update. **tosijs-ui uses none of them** — verified by
70
+ grep and by the full lane run, which is why our peer range still spans both. If your own app
71
+ uses any, read tosijs's 1.8.0 notes before updating.
72
+
73
+ ## 1.11.0
74
+
75
+ **Schema-driven editing.** Three new pieces that compose: `<tosi-schema-form>` renders a form
76
+ from a JSON Schema, `<tosi-crud>` puts search, list and edit over a store you supply, and
77
+ `hashState` keeps filter and selection in the URL so a filtered list with a record open is a
78
+ link you can send.
79
+
80
+ **The dev bridge is usable on a headset.** The edit-link token is now **seven characters** you
81
+ can read off one screen and type on another, instead of twenty-two of mixed-case base64url.
82
+ That change came from watching the feature fail in practice: people gave up and typed LAN IP
83
+ addresses instead, and a credential too painful to use is not protecting anything.
84
+
85
+ > **If you are on 1.10.0, this is a three-version jump.** 1.10.1 and 1.10.2 were tagged in git
86
+ > but **never published to npm** — `latest` has been 1.10.0 throughout. Nothing is lost: every
87
+ > change in those releases is in this one, and their notes are below where they always were.
88
+ > They are simply not installable, so `npm i tosijs-ui@1.10.2` will fail.
89
+
90
+ ### Breaking
91
+
92
+ - **`tosijs-schema` floor is `^1.8.0`** (optional peer). 1.8.0 enforces `oneOf`,
93
+ `exclusiveMinimum` and `exclusiveMaximum` — which is what
94
+ [tosijs-schema#8](https://github.com/tonioloewald/tosijs-schema/issues/8), filed from this
95
+ work, asked for — and exports `unenforcedKeywords`. The form now asks the **registered
96
+ validator** which keywords it does not check, falling back to a local list only when a
97
+ validator cannot answer. Against 1.7.0 a `oneOf` field carried "oneOf is not validated";
98
+ against 1.8.0 it carries nothing and the value is genuinely checked. Verified with
99
+ `bun bin/verify-schema-dep.ts --version=1.8.0` (17/17).
100
+
101
+ - **Validation is supplied, not imported.** `setSchemaValidator({ validate, inferSchema, unenforcedKeywords })` —
102
+ one line, anywhere, before or after render. The CDN `<script>` build and any `tosijs-ui/site`
103
+ doc site register it themselves, so only an ESM consumer writes it.
104
+
105
+ The reason is worth knowing, because it is not a preference. A bare `import('tosijs-schema')`
106
+ in shipped code is either **resolved** by your bundler — which fails the build for anyone who
107
+ did not install it, including people using only `<tosi-table>` — or left **external**, which
108
+ cannot resolve in a browser and kills validation for everyone. Both were measured. There is
109
+ no third option, so the component asks for two functions instead of a package. The upside is
110
+ that they are just functions: an Ajv wrapper, a house validator or a test stub all work.
111
+
112
+ - **`tjs-lang` moves to `^0.13.1`** (from `^0.12.0`), and `TJS_VERSION` in `code-transform.ts`
113
+ moves in lockstep. 0.12.0 is **deprecated on npm**, and the deprecation names this release's
114
+ own combination — _"tosijs-schema >=1.5.0 breaks the battery atoms' output validation in these
115
+ versions. Upgrade to 0.13.1."_ Worse for adopters, `^0.12.0` does not admit 0.13.x, so anyone
116
+ on current tjs-lang got a hard `ERESOLVE`. The tjs CodeMirror extension is still bundled
117
+ rather than externalized — the constraint that silently no-ops highlighting if it breaks.
118
+
119
+ - **The edit-link token is 7 Crockford base32 characters**, and `linkTtlMinutes` defaults to
120
+ **5** (was 15). A link minted by an older server is not redeemable by a newer one; both are
121
+ in-memory and per-process, so this only matters across a restart mid-session.
122
+
123
+ ### New
124
+
125
+ **`<tosi-schema-form>`** — a form from a JSON Schema. `value` is the state and `change` fires on
126
+ edit, the same contract as every other component here.
127
+
128
+ - Scalars, enums, `const`, and `format`-driven input types. Anything it cannot render is a
129
+ labelled placeholder saying so, never a silently omitted field — a field that vanishes is
130
+ indistinguishable from a schema that never mentioned it, which is how an editor loses data.
131
+ - **Nested objects** become `<details>` sections to any depth, with fully-qualified leaf paths
132
+ (`address.geo.lat`), so value sync and error keying work at depth with no special cases.
133
+ `required` is scoped to the object that declares it, which is what JSON Schema means: a
134
+ required `city` inside an optional `address` says _if you give an address, it needs a city_.
135
+ - **Arrays** with add, remove and reorder. Each element expands against `items`; scalar items
136
+ are a single field at the index (`tags.0`), object items expand to their properties
137
+ (`items.0.sku`).
138
+ - **Unions** (`anyOf`/`oneOf`), each rendered as what it actually is: `[X, null]` is just an
139
+ optional X, an all-`const` union is a `<select>`, an all-object union is a variant picker plus
140
+ the matching branch's fields. Anything else names the shapes it saw ("a union of string |
141
+ object"). Discriminators are derived from the property every branch pins to a different
142
+ `const`, or declared as `x-discriminator`.
143
+ - **Format plugins** — `registerFieldPlugin(format, plugin)` claims a schema `format`. The
144
+ plugin owns the control; the form keeps the label and the error slot. Plugins are dispatched
145
+ first and for every node kind, so `unsupported` is the fallback, not the ceiling. Register
146
+ whenever you like: styles are injected on registration and live forms rebuild.
147
+ - **Validation is optional.** Without a validator the form still renders and edits and simply
148
+ reports no errors, which is a smaller failure than refusing to render.
149
+ - **No schema? It infers one** from the value, via `inferSchema`. Read it back from `.schema`,
150
+ edit it, set it again — that round trip is the point.
151
+
152
+ **`<tosi-crud>`** — search, list, edit over a `CrudStore` adapter (`list` / `save?` / `delete?`,
153
+ promise-returning). There is **no transport in the component**: REST, a DocStore, IndexedDB, an
154
+ array in a closure and a mock all satisfy the same three methods. Omit `save` and the form is
155
+ read-only; omit `delete` and its button is not shown. `columnsFromSchema()` lets one description
156
+ of the shape drive both surfaces — strictly better than the table's own inference, which reads
157
+ `Object.keys(array[0])` and silently loses the column for a property the first row lacks. The
158
+ search term and selected id live in the URL hash, so typing **replaces** the history entry while
159
+ selecting a record **pushes** one: back leaves the record rather than un-typing your search a
160
+ letter at a time.
161
+
162
+ **`hashState({ namespace, mode })`** — key-value state in the page hash. Every key is namespaced
163
+ in the URL and bare in the API, and a write never touches a key it did not put there, so two
164
+ instances (or a hashState beside a hash router) share one URL without deleting each other. That
165
+ is the failure that made `createDocBrowser` grow a `'memory'` routing mode, so `mode: 'memory'`
166
+ is here from the start.
167
+
168
+ **Editable `<tosi-table>`** — set `editable` and cells become inputs, per-column overridable
169
+ either way; a column with its own `dataCell` is never made editable. A `schema` drives the
170
+ control type and validates edits, using the same model the form uses. A `change` event carries
171
+ `{ item, field, oldValue, newValue, error }` and commits on `change`, not `input`: an event per
172
+ keystroke would make `3` a legitimate intermediate state of typing `35`.
173
+
174
+ **Localization** — `localize(pattern, values)` fills `{name}` placeholders **after** translating,
175
+ so the key is the whole sentence (`'Add {item}'`, never `'Add ' + item`). A concatenated
176
+ sentence is untranslatable in principle: the translator sees a dangling fragment and cannot move
177
+ the placeholder to where their language puts it. An unknown placeholder stays visible as
178
+ `{name}` rather than blanked. `<tosi-schema-form>` localizes its own chrome and rebuilds on
179
+ locale change.
180
+
181
+ **The dev bridge.** `mintLinkToken()` / `normalizeLinkToken()`: Crockford base32 excludes `I`,
182
+ `L`, `O` and `U`, so the `0`/`O` and `1`/`l` mistypes that hurt most on a floating keyboard are
183
+ impossible rather than merely unlikely. `randomInt`, not `randomBytes % 32`. Redemption folds
184
+ case, accepts `I`/`L` as `1` and `O` as `0`, and ignores hyphens — normalising on **redemption**
185
+ rather than only when minting, because case-insensitivity that exists in the alphabet but not in
186
+ the comparison is a claim rather than a behaviour, and the failure it produces is a correct human
187
+ being told they typed it wrong. `--link` prints the code on its own line as well as in the URL.
188
+
189
+ The TTL is 5 minutes because the link is a **bearer token** for its lifetime — which the
190
+ `dev-auth` header comment now says plainly instead of describing the single-use rule it stopped
191
+ following in 1.10.0. About 35 bits is ample for what this actually is: an online-only guess
192
+ against a `Map` lookup, for a token redeemed seconds after minting, that mints only a write
193
+ session `mayWriteSource` still gates.
194
+
195
+ Guess-rate control: redemption runs **one at a time** and every attempt occupies at least 100ms,
196
+ so ten attempts a second against 32⁷ is ~111 years to exhaust and about 1 in 11 million inside a
197
+ five-minute window (measured — 20 concurrent attempts complete at 9.8/sec). After ten
198
+ consecutive failures the slot widens to a second. Not a lockout: the door never closes on a
199
+ human, it only gets slower to knock on. An earlier draft of this used escalating delays plus a
200
+ lockout, and replacing it was the right call — that version had two tunable constants, a counter
201
+ needing reset, and a lockout an attacker could trigger, which is a denial of service against the
202
+ developer on the one credential they need in order to work.
203
+
204
+ `tosijs-tunnel` prints the host **and where the address came from** before it acts —
205
+ `tosijs-deploy` is dry-run by default and shows its target, this one is not.
206
+
207
+ **Tooling.** `bin/verify-schema-dep.ts` runs the twelve requirements from tosijs-schema#6 plus
208
+ both directions of #7 against a candidate release and exits non-zero on any failure.
209
+ `PREVIEW_HOST` now also resolves from `~/local-secrets/tosijs-preview.env`, because the previous
210
+ advice — export it from your shell profile — is present for a human and **absent for every
211
+ tool**: non-interactive shells inherit no interactive profile, so an agent or CI step sees
212
+ nothing and reports a missing credential rather than a missing `export`. `readLocalSecret` parses
213
+ `KEY=value` only and never evaluates; a credentials file that needs a shell to read is one that
214
+ can run code. `bin/preview-host.test.ts` includes a guard that greps the committed config for a `user@host`
215
+ literal — and asserts the pattern would have caught the real one, so it cannot pass by being
216
+ wrong. `bin/smoke-consumer.ts` gained four things it could not previously see: resolution of the
217
+ new subpaths from an installed tarball, a `tsc --noEmit` over an installed consumer with
218
+ `skipLibCheck: false` and no optional peers, a bundle entry that actually imports the library,
219
+ and both vendored types pinned by assignability tests in both directions.
220
+
221
+ - **`<tosi-table>` gained `full-width-header`.** A table whose columns add up to less than its
222
+ container left a strip of blank space on the right for no reason; with the attribute the
223
+ leftover width goes to the last **unpinned** column. Right-pinned columns are skipped —
224
+ they sit against the right edge by definition, so stretching one would push the space back
225
+ into the middle. When the columns overflow, nothing changes: every column keeps its width
226
+ and the table scrolls, because the row width is `max(sum, 100%)` and `1fr` then resolves to
227
+ exactly the width it was given. Header and body share one `grid-template-columns`, so they
228
+ stretch together.
229
+
230
+ ### Process
231
+
232
+ - **`bun run release-check` must run AFTER the release commit, and it is the last thing before
233
+ `git tag`.** The range is `<last tag>..HEAD`, so a bullet written _in_ the release commit is
234
+ not in the range you checked a moment earlier — 1.11.0 shipped a commit whose body claimed
235
+ the gate was green while it exited 1 on the state that commit created. The range is also
236
+ exclusive of the since-commit, so tagging turns the gate green without the bullet ever being
237
+ written: the annotation escapes in both directions. Now documented in `CLAUDE.md`.
238
+ - The `unenforced.ts` parity test is a **subset** check rather than equality. Upstream growing
239
+ the set of keywords it enforces is good news, and an equality assertion turned that into a
240
+ red build. Upstream _shrinking_ below our fallback is the dangerous direction — the fallback
241
+ would claim a keyword is checked when it is not — and that is still caught.
242
+
243
+ ### Fixed after the pre-release review
244
+
245
+ The nine-lens review ran **three times** against this release. Everything in this section was
246
+ found by it and fixed before tagging.
247
+
248
+ - **Editable numeric cells silently truncated what you typed.** Typing `19.95` into a
249
+ `<tosi-table editable>` number cell committed **95**; `-4` committed **4** — and every
250
+ commit fired `change` with `error: null`, so nothing reported that a digit had gone. Two-way
251
+ `bindValue` wrote the model on every `input`, and `<input type=number>` sanitizes an
252
+ intermediate `"19."` to `""` under the HTML value-sanitization algorithm, so the binding
253
+ wrote `""` straight back into the focused input. The binding is one-way now and
254
+ `handleCellChange` is the sole model write — which also makes "commits on `change`, not on
255
+ `input`" true of the _model_ and not only of the event.
256
+
257
+ It was invisible to every lane because every test assigned `.value` programmatically or
258
+ typed with no delay, and neither ever lets a frame run between keystrokes. There are three
259
+ tests that genuinely **type**, with a per-character delay, across all three engines.
260
+
261
+ The nine-lens review ran twice against this release. Everything below was found by it and
262
+ fixed before tagging.
263
+
264
+ - **`<tosi-crud>` destroyed unsaved edits** — one frame after any keystroke, with no user
265
+ action, and `save()` then posted the unedited record.
266
+ - **`<tosi-crud>`'s table selection never followed the form**, so a `#?people.id=2` deep link
267
+ opened the record with nothing highlighted — the feature these notes headline, producing a
268
+ list that did not show where you were. **Back** also left the opened record on screen while
269
+ the URL said otherwise, so back-then-reload showed you two different things; and a
270
+ hash-driven selection change fired no `change` event. Saving a new record now writes its id
271
+ to the hash, which is both correct and what makes "absent id means deselect" safe.
272
+ - **`<tosi-crud>` rebuilt the entire table on every keystroke.** Identity-guarded; measured at
273
+ zero table renders per keystroke afterwards.
274
+ - **`<tosi-schema-form>` emitted duplicate `change` events** — a checkbox click produced
275
+ three. `<tosi-table>` fixed the identical hazard in this release; the sibling shipped
276
+ without it.
277
+ - **The "not validated" note skipped containers**, so `uniqueItems` on an array and `allOf` on
278
+ an object — exactly the keywords a validator is most likely to skip — carried nothing.
279
+ - **`columnsFromSchema` re-derived property types** and got `{anyOf:[{type:'boolean'},
280
+ {type:'null'}]}` wrong, and emitted only `title` as the column name — so a title-less schema
281
+ gave headers reading `firstName` beside a form showing "first name".
282
+ - **`<tosi-table>`'s column menu still concatenated localized words**, which the localize docs
283
+ in this same release condemn by name. All five languages checked rendered wrong:
284
+ `Sortieren Aufsteigend`, `숨기기 열`, `非表示 カラム`, `隐藏 列`, `Cacher Colonne`.
285
+ `localizePhrase(key, fragments)` asks for the whole sentence and joins the fragments only
286
+ when nobody has translated it — so an existing translation table keeps its behaviour exactly
287
+ and adding one row upgrades it. Nothing is orphaned.
288
+ - **Six `on<Event>` members were shadowed by the element factory**, which warns about it in
289
+ the console. Renamed to `handle<Event>` per the framework's own guidance.
290
+ - **The no-validator path had no coverage in any tier** — the default for every ESM adopter,
291
+ while every lane ran with a validator registered. It has its own test file now. Two real
292
+ defects were behind that gap: removing a validator left the warnings permanently
293
+ suppressed, and a single once-flag meant whichever component spoke first silenced the rest.
294
+ - **A form with no schema and no validator rendered an empty box**, warning about validation
295
+ when the problem was that it had nothing to render. It says so on screen now, and the
296
+ warning names the right problem.
297
+ - **`change.detail.oldValue` was `undefined` for every cell edit after the first.** The
298
+ baseline was deleted on each commit rather than rolled forward, and the delete ran _before_
299
+ the equality guard — so a no-op commit wiped it, and clearing a numeric cell as the second
300
+ edit fired no `change` at all and skipped the coercion write, leaving a schema-typed integer
301
+ holding a raw string.
302
+ - **The documented `setSchemaValidator` recipe was wrong**, and only for the people it was
303
+ written for. `{ validate, inferSchema }` omits `unenforcedKeywords`, so the form fell back
304
+ to a keyword list frozen at tosijs-schema 1.7.0 and labelled every `oneOf` and
305
+ `exclusiveMinimum` field "not validated" while 1.8.0 was checking it. Our own doc site was
306
+ correct because the iife passed all three — so the failure was visible _only_ to the ESM
307
+ adopters the docs instruct. Pass all three.
308
+ - **`crud.table` and `crud.form` threw before hydration** — the only two accessors on the
309
+ class without the guard the rest have — so the documented way to reach the composed parts
310
+ threw on the line the docs told you to write. They return `null` now. The snippet was also
311
+ in a display-only fence, so no lane ran it; it is executable and tested. And its second line
312
+ was wrong too: `crud.form.readOnly = true` was silently reverted on the next render.
313
+ - **`<tosi-crud>` wrote `history.replaceState` on every search keystroke.** WebKit throws past
314
+ ~100 calls in 10s — a held key clearing a long term gets there — and the throw preceded the
315
+ debounce, so search silently stopped working with an uncaught error. The URL write moved
316
+ inside the debounce, where the store query already was.
317
+ - **An empty `PREVIEW_HOST` swallowed the whole resolution chain.** `??` only skips
318
+ null/undefined, so `PREVIEW_HOST=''` — what a CI `env:` renders for a missing secret —
319
+ counted as "set" and hid both the site config and `~/local-secrets`, the exact symptom that
320
+ rung was added to abolish.
321
+ - **Adding or moving an array item dropped focus.** Every row is rebuilt, so the button you
322
+ just clicked stopped existing: you could not press ↓ twice to move an item two places, or
323
+ add two rows from the keyboard. Focus is restored by role and index, skipping controls that
324
+ are disabled at the destination.
325
+ - **`<tosi-schema-form>` was quadratic in the keystroke path** — one root-scoped
326
+ `querySelector` per field, twice, per keystroke. Measured on the built component: 800 fields
327
+ cost **240 ms** per keystroke before and **0.64 ms** after. An index built with the DOM
328
+ replaces the scans.
329
+ - **The redemption gate had 21 tests and its wiring had none.** Reverting the dev server to a
330
+ bare `redeemLink()` call restored both the unbounded queue and the unthrottled guess rate
331
+ while leaving every lane green — a security control held in place by nothing but the diff.
332
+ The decision is an exported function now, like the others in that file, and is tested.
333
+ - **Notes read `root.uniqueItems is not validated`** on a field called Tags — upstream's
334
+ `unenforcedKeywords` answers with paths. The leading `root.` is stripped and deeper segments
335
+ kept, since for a container the keyword may be several levels down.
336
+ - **These notes themselves published the old preview-host precedence**, inverted against the
337
+ code and against another paragraph thirty lines earlier. Corrected, with a test asserting
338
+ all four places that state it agree — that sentence had been written three different ways.
339
+ - **A duplicate mid-file import** in `dev-auth.test.ts`, found only because nothing had ever
340
+ type-checked the test suite. (~150 accumulated type errors remain; that is tracked, not
341
+ fixed here.)
342
+ - **The two vendored-type anti-drift guards were never type-checked** — `tsconfig.json`
343
+ excludes `*.test.ts`, so both were inert and one was **red**: `json-schema.test.ts` asserted
344
+ our `JSONSchema` is assignable to theirs, which TypeScript cannot decide because both types
345
+ recurse through `additionalProperties`. The guard now asserts the direction it CAN decide —
346
+ theirs → ours, the assignment the code actually performs when storing an inferred schema —
347
+ and the reverse is exercised at runtime by validating an ours-typed schema through their
348
+ validator. `bun run typecheck-guards` (`tsconfig.guards.json`) runs in CI beside the
349
+ existing typecheck; mutation-verified that drifting the vendored type turns it red.
350
+ - **`<tosi-table>`'s four column-menu captions** now use `localizePhrase`, so the docs and the
351
+ docs' own worked example agree.
352
+
353
+ ### Also fixed in the review passes
354
+
355
+ - `<tosi-schema-form>` let the control's native `change` escape to consumers, so a form edit
356
+ fired a duplicate event — the `input`-only handler's `stopPropagation()` never saw a
357
+ `change`. Three events per checkbox click became one.
358
+ - `localizePhrase` asked "did the output differ from the input?" rather than "does a row
359
+ exist", so a locale that deliberately maps a phrase to itself fell back to joined fragments
360
+ — the behaviour the row was added to replace. It also re-implemented annotation stripping
361
+ with a weaker regex than the module's own, which already disagreed about `tag\#42`.
362
+ - `--tosi-spacing-50` is not a variable — the scale is `-xs/-sm/-lg/-xl` — and the `var()`
363
+ fallback made the mistake invisible. The three genuinely-new customization points
364
+ (`--tosi-error`, `--tosi-border`, `--tosi-border-radius`) are documented on the schema-form
365
+ page rather than left accidental.
366
+ - `<tosi-table>`'s `fieldFor` was documented as "cached per render pass" and cached nothing,
367
+ while allocating a schema and running the validator's keyword walker per editable cell per
368
+ render.
369
+ - `SiteConfig.tunnel.linkTtlMinutes` said "Default 15" two lines above "Defaults to 5"; the
370
+ summary line is what an editor tooltip shows.
371
+ - `tosijs-tunnel --link` now prints the 7-character code as well as the URL, which the docs
372
+ already described and only the dev server's own banner did.
373
+ - The "No preview host" instructions promised a `700` credentials directory and gave a command
374
+ that neither created nor secured it — and failed outright on a machine that did not have it.
375
+ - `src/docs/utilities.md` was a five-line stub duplicating a section `helper-libraries.md`
376
+ already provides. Removed.
377
+ - `src/docs/migrating.md` has a **1.11.0 section** — the validator seam, the `tjs-lang` floor,
378
+ and the edit-token change. It exists for "releases you may be upgrading past" and had no
379
+ entry despite this release having a Breaking section.
380
+
381
+ - `<tosi-table>`'s `change` event reported `oldValue: undefined` whenever no `focus` event had
382
+ fired on the cell — an unfocused browser window, or any consumer who sets `.value` and
383
+ dispatches `change`, which is what an adopter's test does. The old value came from a
384
+ `WeakMap` filled on focus, which existed because two-way `bindValue` used to overwrite the
385
+ model as the user typed. The one-way binding shipped earlier in this release ended that, so
386
+ the old value is now read from the model immediately before the write — no focus event
387
+ required, and one mechanism instead of two. Caught by the haltija lane, which the other
388
+ three lanes do not substitute for.
389
+
390
+ - `crud.save()` threw away the record it sent when the store returned nothing. A 204-style
391
+ adapter satisfies `Promise<any>` by resolving `undefined`, which is a perfectly ordinary
392
+ thing for a REST store to do, and the row went blank. It now keeps what it sent when the
393
+ store has nothing to say.
394
+ - The `doc-system.pw.ts` nested-doc-system flake — the **fourth** instance this release of a
395
+ spec racing the page's own inline doc tests. The runner is now disabled for that whole
396
+ spec file rather than per test, because after four the fix plainly belonged at the file
397
+ level. 6/6 clean since.
398
+
399
+ - `<tosi-doc-system>` gave up on the first failed `docs.json` fetch, so a doc site rendered
400
+ nothing at all when one request lost — a real-user failure, not only the test flake that
401
+ surfaced it. It now retries three times with full-jitter backoff (250ms base, 4s cap) and
402
+ honours `Retry-After`. It retries **only 429, 5xx and network errors**: a 404 or a 403 is an
403
+ answer, and re-asking cannot change it — which is what stops a retry turning an overloaded
404
+ server into a self-inflicted DoS.
405
+
406
+ ### Known issues
407
+
408
+ - **firefox intermittently reports `no WebAssembly compiler available`** under the full
409
+ parallel test lane, which stops the inline-WASM example loading at all. Filed as
410
+ [tjs-lang#36](https://github.com/tonioloewald/tjs-lang/issues/36): a `wasm {} fallback {}`
411
+ block should take its fallback branch when the engine declines to compile, which is exactly
412
+ what that branch is for. Affects the test lane, not shipped behaviour — the example
413
+ degrades to not rendering rather than misreporting, and the doc-test guard against _silent_
414
+ fallback is unaffected.
415
+
416
+ - **[#102](https://github.com/tonioloewald/tosijs-ui/issues/102)** — `<tosi-table>`'s header
417
+ and body columns can disagree when a programmatic `columns` assignment lands while a render
418
+ is already in flight. The reporting app changes column visibility on a page-size change,
419
+ which is what produces the timing. The host app arguably should not reassign columns
420
+ mid-render, but a torn header/body is ours either way. Targeted at **1.11.1**; the path
421
+ predates 1.11.0.
422
+
423
+ ### Fixed
424
+
425
+ **Packaging — this one affects existing `<tosi-table>` users.** `<tosi-table>` no longer reaches
426
+ for a schema library at all: it is the component people use _without_ one, and it must never make
427
+ anyone install something in order to build.
428
+
429
+ `tosijs-schema` was imported by
430
+ shipped `dist/` and declared in **no dependency field**, so a consumer's build failed on a
431
+ package they were never told they needed. It is now a declared optional peer at `^1.7.0` — which
432
+ is what the design note said from the start — and nothing we ship imports a type from an optional
433
+ peer any more: `import type` is erased from emitted JS but **not** from emitted `.d.ts`, and
434
+ declaring a peer optional does not help because TypeScript has no notion of an optional peer.
435
+ A second instance had been shipping for a while: `dist/code-editor-cm.d.ts` imported a type from
436
+ `tjs-lang/editors/codemirror`, a declared optional peer that TS2307s anyway (reported upstream as
437
+ tjs-lang#28). Verified from a packed tarball in a clean project: `tsc` and `vite build` both exit
438
+ 0 with the peers absent.
439
+
440
+ - The `./*` subpath wildcard mapped `tosijs-ui/schema-form/fields.js` to
441
+ `dist/schema-form/fields.js.js`, so the extension-ful spelling — what an adopter copying this
442
+ repo's own ESM import style would write — did not resolve at all. A second `./*.js` wildcard
443
+ fixes it, strictly additively.
444
+
445
+ **Localization was wrong in shipped data, and the keys were why.** `<tosi-table>`'s column-header
446
+ menu asked for bare words, and our own translation table shows the cost: `Right` came back in the
447
+ _correct_ sense rather than the direction in four of nine languages (sv `Rätt`, zh `正确的`, es
448
+ `Bien`, it `Giusto`), `Column` as `柱子` (a pillar), `Sort` as `种类` / `종류` (a kind of thing),
449
+ `Show` as `Espectáculo` / `Spettacolo` (the kind with a stage). Every one is a competent
450
+ translation of the word it was given — which is the point. The keys now carry sense annotations
451
+ (`Right#direction`, `Column#table`, `Sort#order`, `Show#reveal`, `Hide#conceal`, `Pin#fasten`,
452
+ `Ascending#sort-order`), and `demo/src/localized-strings.ts` has matching rows with the
453
+ wrong-sense entries corrected, verified in a browser across sv/es/zh. Annotating a key is
454
+ backward compatible, so this cannot orphan an existing table.
455
+
456
+ **`<tosi-crud>` destroyed unsaved edits.** Four specs now cover delete, its rejection path and
457
+ `createNew`→`save`; mutation testing confirms that restoring the defects turns two of them red.
458
+
459
+ `showSelected()` compared `form.value !== _selected`,
460
+ and `setByPath` is immutable, so after the first keystroke that was permanently true and every
461
+ render wrote the record as loaded back over the edit — one frame later, with no user action, and
462
+ `save()` then posted the unedited record. The fix keeps the load guard
463
+ (`_loaded !== _selected`) — one invariant, one place — and drops a form-change listener that set
464
+ `_selected = form.value`, which nothing needed: `value` and `save()` read `form.value` directly,
465
+ and `remove()` is better with the record as opened than with a half-edited copy of it. Mutation
466
+ testing settled that: the two were independently sufficient, so neither was pinned and either
467
+ could have been removed without a single test failing.
468
+
469
+ - `<tosi-crud>`'s destructive paths ran in no lane, and the gap hid two defects: `createNew()`
470
+ followed by `remove()` sent `store.delete({})`, and the Delete button raised an unhandled
471
+ rejection on a rejecting store.
472
+ - Grid navigation swallowed arrows, Home and End inside editable cells, so the caret could not be
473
+ moved and focus jumped away mid-edit; on an enum `<select>` the cell could not be changed by
474
+ keyboard at all. Escape and Tab stay with the grid.
475
+ - The cell's native `change` no longer escapes the cell. It bubbles, so a listener on the table
476
+ received both — theirs with a `detail`, the input's with none.
477
+ - The "this keyword is not validated" note rendered **only for unions**, so the honesty it exists
478
+ for covered exactly one keyword while the docs promised otherwise.
479
+ - `validate` is called with `strict: true`. Without it the validator samples: `maxProperties` is
480
+ skipped entirely and a bad element deep in a long array is stepped over.
481
+ - The optional-peer degrade was silent. It now warns once naming the package and the seam, and
482
+ `validationAvailable` lets a Save handler tell _"this conforms"_ from _"nobody checked"_.
483
+ - We read `x-discriminator` first. `discriminator` is an OpenAPI keyword that tosijs-schema's own
484
+ `agentContract` refuses, so a schema written to pass their check spells it `x-`.
485
+ - The schema→control layer existed twice and had drifted: a `const` property was readonly in the
486
+ form and a freely editable text box in the table.
487
+ - Variant detection scores branches on how many required keys are present, rather than demanding
488
+ every key — a half-filled variant used to match nothing and show an empty box.
489
+ - `readOnly` hides the add/remove/reorder controls instead of disabling them.
490
+ - The array controls carry stable classes (`.schema-move-up`, `.schema-move-down`,
491
+ `.schema-remove`) alongside their localized `title`/`aria-label`: a localized tooltip cannot
492
+ also be a selector.
493
+ - The hash router treats `?…` in the hash as a query, not part of the path. `#/invoices/42?q=x`
494
+ previously matched no route at all.
495
+
496
+ The intermittent Playwright failure was chased rather than re-run until green: `hash-state.pw.ts`
497
+ on WebKit, about one run in four, because the page's own inline doc test writes into the hash
498
+ asynchronously and could land after the spec cleared it. A doc page under test is also a page
499
+ running tests.
500
+
501
+ **The dev bridge's own guard was a denial of service.** Serialized redemption on an
502
+ unauthenticated path with no depth cap meant 50 junk requests delayed a legitimate link by 42
503
+ seconds, and a trickle outgrew the drain so the denial outlasted the attack. Depth is capped;
504
+ overflow is refused instantly with 503 and `Retry-After`. The clock is also read on **arrival**,
505
+ so a token valid when you clicked can no longer expire while queued.
506
+
507
+ **Preview host handling.** A project's own `preview.host` outranks
508
+ `~/local-secrets/tosijs-preview.env`; the machine-global file holds one host, and when it won,
509
+ running `tosijs-tunnel` in someone else's checkout pointed it at your box — and that bin does not
510
+ merely print a target. One `resolvePreviewHost()` now serves all three bins. The tunnel's link
511
+ line no longer claims "Single-use edit link (valid 15 min)", and `doc-site-system.md` no longer
512
+ shows `host:` in its example config. `UPSTREAM.md` claimed 0.12.0 was the latest tjs-lang; stale
513
+ by two minors.
514
+
515
+ ### Housekeeping: we published a preview address, and rotated it
516
+
517
+ Our own `tosijs-site.config.ts` committed the preview host as an ssh target
518
+ (`user@address`) — an **address, not a credential**. Nothing an adopter installs is
519
+ affected, and this is not a security advisory for anyone but us. Recording it because the
520
+ practice it produced is worth adopting.
521
+
522
+ Verified rather than assumed, since "we're sure there were no keys in there" is exactly the
523
+ sort of claim that deserves checking:
524
+
525
+ - **no private key material anywhere in the repo's history** — no `BEGIN … PRIVATE KEY`
526
+ block in any commit on any ref, and no `.pem`, `.key`, `id_rsa` or `id_ed25519` file ever
527
+ added. `*.pem` is gitignored; the only tracked files under `tls/` are a README and the
528
+ cert-generation script.
529
+ - access to that box always required a key that was never published, so the exposure was an
530
+ address someone could try to connect to — not a way in.
531
+
532
+ It has been **rotated regardless**, because a value that has ever been committed to a public
533
+ repo is public: `git log -S` finds it in seconds, and rewriting history does not un-publish
534
+ anything.
535
+
536
+ The practice that came out of it, now in `tosijs-coding-practices` → `deployment.md`:
537
+ machine-local credentials live in `~/local-secrets/` — a `700` directory beside the repos,
538
+ never inside one, so committing them is structurally impossible rather than merely against
539
+ the rules. `tosijs-ui`'s bins resolve `--host=` > `PREVIEW_HOST` > `PREVIEW_SSH` > site config >
540
+ `~/local-secrets/tosijs-preview.env` — the machine-global file is **last**, so a project that
541
+ names its own host is never silently redirected at someone else's box — and a regression test
542
+ now greps our own
543
+ config for a `user@host` literal. The rule had been documented in `site-config.ts` the whole
544
+ time and violated in the same repo — which is why the guarantee is now a directory.
545
+
546
+ ## 1.10.2
547
+
548
+ > **Never released.** This version was tagged but never published, and the tag has since been
549
+ > deleted. Everything below shipped in **1.11.0** instead — including the `killStrayServer`
550
+ > fix, which is why the withdrawal is safe rather than a dropped security patch. npm went
551
+ > straight from 1.10.0 to 1.11.0. Kept here because these changes are real and a reader
552
+ > tracing when something landed needs to find them. ([#101](https://github.com/tonioloewald/tosijs-ui/issues/101))
553
+
554
+ **The dev server no longer serves stale content after a rebuild.** This is the one that
555
+ presented as random flakiness across several sessions: a served `/docs.json` disagreeing with
556
+ the file on disk made the doc browser's route match fail, so live-example insertion silently
557
+ skipped — the page rendered, hydration reported success, custom elements were defined, and
558
+ **zero examples appeared, with no error anywhere**.
559
+
560
+ The cause was a cache key of `path + lastModified`, with a comment asserting that a rebuild
561
+ invalidates it naturally. It does not: `lastModified` is millisecond-granularity, and six
562
+ rapid writes to one file report the same value. A rebuild that rewrote a file inside one
563
+ millisecond kept serving the first compressed body until the process restarted — which is why
564
+ a restart "fixed" it. The cache is now emptied on every completed rebuild, and the key carries
565
+ the file size. (#50)
566
+
567
+ **`killStrayServer` no longer shoots an unrelated process.** Reclaiming the port checked that
568
+ the holder was a JS runtime (`bun`/`node`/`deno`) — but "a JS runtime" is not "our dev
569
+ server", and a colleague's dev server, a language server or a test runner would all pass. It
570
+ now requires the process to be working in **this project**, and skips with a message naming
571
+ the directory when it is not. If the working directory cannot be read it skips too: refusing
572
+ to start costs you ten seconds, shooting the wrong process costs you an afternoon. (#77)
573
+
574
+ **`bun run release-check` now checks the version against what actually changed** — it blocks a
575
+ patch that carries a `[break]` annotation or touches a security-relevant path, and warns on a
576
+ `[change]`. The nine-lens review triggers on the version _letter_, so it only ever fired when
577
+ the letter was already right; 1.9.9 was prepared, gated, tagged and pushed as a patch while
578
+ relocating a build artifact and loosening a security default, and was caught only because a
579
+ human asked for a review anyway. (#78, #79)
580
+
581
+ **Docs:** `bundleOutDir` is in the Bundle config table it belongs in, and `checkExamples` —
582
+ including `contextKeys`, which **extends** the default context rather than replacing it — is
583
+ documented where adopters actually look. (#80, #81)
584
+
585
+ ## 1.10.1
586
+
587
+ > **Never released.** This version was tagged but never published, and the tag has since been
588
+ > deleted. Everything below shipped in **1.11.0** instead — including the `killStrayServer`
589
+ > fix, which is why the withdrawal is safe rather than a dropped security patch. npm went
590
+ > straight from 1.10.0 to 1.11.0. Kept here because these changes are real and a reader
591
+ > tracing when something landed needs to find them. ([#101](https://github.com/tonioloewald/tosijs-ui/issues/101))
592
+
593
+ Two fixes to 1.10.0's row grouping and scroll preservation, both reported by snowfox against
594
+ a real wide, grouped, pinned table.
595
+
596
+ **A pinned column that repeats a group value is no longer a hole.** A column that is both
597
+ `pinned` and listed in `nonRepeatingGroupedRowCells` used `visibility: hidden` on its repeated
598
+ cells — which suppresses _everything the cell paints_, including the opaque background
599
+ `.col-pinned` exists to provide. On every row but the first of its group, horizontally
600
+ scrolled cells showed through the sticky column **and were clickable there**: a visual hole
601
+ and a hit-testing hole from one rule. Repeated cells now hide their _content_ (transparent
602
+ text, `display: none` on element children, `user-select: none`) and keep painting their box.
603
+ (#83)
604
+
605
+ **`preserveScroll` now restores horizontal position too.** It restored `scrollTop` alone, and
606
+ the tables that most need it are the wide ones — normally read scrolled sideways, with pinned
607
+ columns as the identity and the interesting columns off to the right. Sorting or toggling a
608
+ group snapped back to column 0, so the row stayed put while appearing to say something
609
+ completely different. `scrollLeft` is captured independently of the row anchor, so it also
610
+ survives on a table that is scrolled sideways but still at the top — which used to return
611
+ early and lose the columns. (#86)
612
+
613
+ Group ids are memoized per render in a `WeakMap`; the map is replaced each render, so a
614
+ cached id can never outlive the values it came from.
615
+
3
616
  ## 1.10.0
4
617
 
5
618
  Mostly a **silent-failure** release: several things that were quietly not working, or quietly