tosijs-ui 1.12.7 → 1.13.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,318 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0
4
+
5
+ ### tosijs `^1.9.1` — and a console with nothing in it
6
+
7
+ The peer floor moves to `^1.9.1`. **The binding reason is a type-surface change, not the console
8
+ cleanup** — our emitted declarations now reference `TosiProps` and `TosiStyleSheet` (renamed
9
+ upstream from `XinProps`/`XinStyleSheet`), so `dist/*.d.ts` _hard-requires_ a tosijs that exports
10
+ them. An adopter on an older tosijs gets type errors, not warnings. The floor also carries
11
+ tosijs's **agent-surface security fix**, and the deprecation cleanup below.
12
+
13
+ **Migration note if you are on tosijs 1.7.x:** this floor takes you through **tosijs 1.8.0**,
14
+ which its own notes describe as deviating from semver — it removed `data-ref` and `<xin-slot>`.
15
+ Read tosijs's 1.8.0 entry before upgrading; that step is not automatic.
16
+
17
+ Before 1.9.1, every page of every doc site logged warnings a consumer could not act on:
18
+
19
+ - `xinValue, tosiValue, xinPath…` — tosijs reading its own deprecated proxy property
20
+ ([tosijs#31](https://github.com/tonioloewald/tosijs/issues/31)). Nothing in any consumer's
21
+ code caused it.
22
+ - `bindText` / `bindList` — deprecated in 1.8, then **withdrawn as a category error**
23
+ ([tosijs#33](https://github.com/tonioloewald/tosijs/issues/33)). They are a pair: `bindList`
24
+ supplies the per-item template and `bindText` fills it, and `{ textContent: proxy }` was
25
+ never a replacement for the path form (measured: `textContent: '^.name'` inside a template
26
+ renders the literal string, `bindText: '^.name'` binds). `bindText`'s runtime warning
27
+ outlived its typings by one release and is gone in 1.9.1.
28
+
29
+ Verified on a from-scratch build rather than assumed: **the three deprecation warnings above are
30
+ gone.** An earlier draft of these notes claimed _zero console warnings_, and that was false — one warning remains and it is ours, not tosijs's. Instantiating `<tosi-tabs>` still logs an
31
+ on-event collision from our deprecated `onCloseTab` alias, which the elements factory reads as
32
+ event-handler sugar.
33
+ `onCloseTab` is a deprecated alias we keep for compatibility, so removing it is a breaking change
34
+ and waits for the next major. Stating it rather than claiming a clean console we do not have.
35
+
36
+ ### Live examples: pick a side by clicking, and see what actually changed
37
+
38
+ Both diff views — the resolvable one and the read-only one behind `<tosi-code>`'s review
39
+ overlay — now let you **click a coloured line** to take that side, and mark **the words that
40
+ actually differ** more strongly than the line containing them. Dragging to select text does not
41
+ change anything; those lines are also what you copy from.
42
+
43
+ `<tosi-example>`'s widget bar used to stay **open after every action** — you clicked "maximize"
44
+ and it sat there covering the corner of the thing you had just maximised, until you remembered
45
+ to click the handle again. It now closes once you have actually done something, its controls are ordered by reach (maximize nearest the handle), and the handle is the
46
+ tosi owl on a status-coloured badge — orange running, green passed, red failed.
47
+
48
+ **Tapping that handle on a touch device no longer flashes the menu open and shut.** A touch
49
+ device has no hover, but the browser still fires `pointerenter`/`pointerleave` around a tap, so
50
+ the hover _peek_ opened the bar and then undid itself before the click pinned it — you tapped
51
+ again believing it had failed, and that second tap closed it for real.
52
+
53
+ ### Source moved into the app menu
54
+
55
+ The floating `<> Source` chip that sat over the content is gone; Source is a submenu of the app
56
+ menu beside Language and Color Theme, and it disappears entirely on a page with no source file.
57
+
58
+ ### Security: cross-site requests can no longer reach the dev server's write endpoints
59
+
60
+ `POST /__docstore/source` and `POST /__build` were gated on the **peer address alone** — no
61
+ credential — and the handlers parse JSON regardless of `Content-Type`. That made a cross-site
62
+ `fetch(…, {mode:'no-cors'})` a _simple_ request: no preflight, peer is `127.0.0.1`, gate passes.
63
+ Any page you happened to visit while `bun start` was running could write any file under the repo
64
+ root, `.git/hooks/*` included — code execution at your next git operation
65
+ ([#90](https://github.com/tonioloewald/tosijs-ui/issues/90)).
66
+
67
+ All three state-changing endpoints — `/__docstore/source`, `/__build` and `/report` — now
68
+ require the request to be same-origin, via one shared predicate rather than a term repeated per
69
+ call site. It checks **both** `Sec-Fetch-Site` **and** `Origin`: the first is unforgeable but
70
+ absent on Safari < 16.4, Firefox < 90 and older WKWebViews, and those browsers send the second.
71
+ An earlier pass checked only the first and left exactly that population unprotected. Applied
72
+ **on the non-tunnel path only** — the tunnel is legitimately cross-site and
73
+ is gated by a session cookie instead. Non-browser callers (the CLI, `curl`) send no fetch
74
+ metadata and are unaffected; the attack this closes is specifically a _browser_ being induced to
75
+ make the request, and a browser always tells us.
76
+
77
+ The prose in `dev-auth.ts` had claimed `SameSite=Lax` gave "free CSRF protection". That was true
78
+ only of the tunnel path, which needs a cookie — the default loopback path needed nothing, so the
79
+ stated defence never covered its own primary case.
80
+
81
+ ### New dev-server endpoint: `POST /__build` (loopback only)
82
+
83
+ Called out under its own heading because it is a new HTTP surface on a tool people run all day.
84
+
85
+ It exists so `bun run build` can ask a running dev server to build instead of refusing (below).
86
+ It is gated exactly like `/__devlink`: **loopback and NOT via the tunnel**. Verified against a
87
+ real server rather than reasoned about — an anonymous tunnel request gets `401` from the session
88
+ gate, and an **authenticated** one gets `404` from this gate, which is the case that matters. A
89
+ remote visitor triggering builds would be a denial of service at best, and runs the project's own
90
+ build code at worst. `GET` does nothing.
91
+
92
+ Nothing else about the build lock changed: two builders racing `rm -rf` on one output tree is
93
+ still refused, and a stale lock still gets the old message.
94
+
95
+ ### ePub dates, and an `epub.modified` option
96
+
97
+ `dcterms:modified` is the only date an ePub reader can surface, and it is baked into a
98
+ committed, redistributed file — so it has to be deterministic _and_ coherent. The
99
+ version-derived encoding introduced in this release **wrapped** — twice. First `1.13.24` and
100
+ `1.14.0` produced the same instant; the repack then made `1.13.1-beta.1` byte-identical to
101
+ `1.13.0`, collided `1.0.1000` with `1.1.0`, and sent a CalVer `2026.9.3` to the year 5872. Each
102
+ attempt shipped a docblock asserting the ordering that execution disproved.
103
+
104
+ So the promise is narrowed to what is actually needed and can actually be held: **the same
105
+ version always yields the same instant, and different versions yield different ones.** It is no
106
+ longer ordered, because nothing reads these dates as a sequence — a reader sees one date on one
107
+ book — and a claim nobody needs, made three times and false three times, is worse than no claim.
108
+ The test sweep is generated rather than hand-picked, which is what would have caught the
109
+ adjacent-version collision.
110
+
111
+ New: **`epub.modified`** in the site config. The derived date is synthetic — it looks like a
112
+ date without being one — and until now an adopter had no way to publish the real one.
113
+
114
+ That option shipped broken for a few hours and is worth recording: `modified` was written
115
+ **above** a `...epubOpts` spread, and `epubOpts` _is_ `config.epub` — so the adopter's raw value
116
+ overwrote the sanitised one. `'2026-09-03'` reached the OPF unnormalised and `'not a date'`
117
+ reached it verbatim, and EPUB 3 requires exactly `CCYY-MM-DDThh:mm:ssZ`, so the obvious spelling
118
+ of the new option produced a file EPUBCheck rejects. The option assembly is now an exported
119
+ **`epubOptionsFor()`** with tests that drive **the call site**, not just the date function beside it
120
+ — the third time in this release that a green test asserted a pure function whose only consumer
121
+ threw the result away.
122
+
123
+ ### The book this project ships carries a real date again
124
+
125
+ `docs/tosijs-ui.epub` is redistributed, and its OPF has no `dc:date` — so `dcterms:modified` is
126
+ the only date a reader ever sees. The version-derived scheme hashed 1.13.0 to **2012**, fourteen
127
+ years adrift, where v1.12.8's book carried a correct contemporary date. This repo now sets
128
+ `epub.modified` explicitly, which is exactly what that new option is for; adopters who care
129
+ about the date should do the same, and adopters who don't still get a deterministic one.
130
+
131
+ A related tidy: a docblock asserting the monotonicity this function does not have had survived
132
+ three rewrites by sitting forty lines above it in a different file, and was shipping in `dist/`.
133
+ The behaviour is described in one place now.
134
+
135
+ ### One place decides the ePub date
136
+
137
+ `buildEpub` — the public export, and what `bun book` calls — now defaults and validates
138
+ `dcterms:modified` itself. It previously used `new Date()` with no validation, so the
139
+ determinism fix and the `epub.modified` handling lived at a single `buildSite` call site that
140
+ `bun book` never executed: that path still stamped a wall clock, re-dirtying the committed ePub,
141
+ and still passed a date-only string into an OPF that EPUBCheck rejects.
142
+
143
+ That is the fourth instance in this release of one shape: **a green test asserting a function
144
+ whose only consumer discards the result.** The date function was correct and tested; the
145
+ assembly threw its return value away. The predicate was correct and tested; two of three
146
+ endpoints used it. The touch guard was correct; the test retyped it. And the fix for the
147
+ _first_ three introduced the fourth, by testing a helper that one of two shipped paths called.
148
+
149
+ The answer is not another helper — it is a single choke point every path already goes through,
150
+ tested from the public entry point by asserting the OPF text.
151
+
152
+ ### Tests that can actually fail
153
+
154
+ Two "regression tests" added earlier in this release were **tautologies**: each re-declared the
155
+ production predicate inside the test file and asserted on the copy, so both passed forever and
156
+ stayed green when the real code was broken. A mutation test in the pre-release review killed
157
+ zero of them. They now mount the real `<tosi-pocket-bar>` and import the exported delegation
158
+ predicate, and were re-mutated to confirm the opposite — deleting the touch guards and the
159
+ pin-clear turns two tests red.
160
+
161
+ A re-review then found the same shape one layer down: the delegated-build queue —
162
+ `buildNow`/`buildWaiters` — had **zero** coverage, and an unconditional infinite loop passed the
163
+ whole green suite. The loop is fixed (it started a build on every pass instead of once, spinning
164
+ the dev server into ~30 builds and a `process.exit(1)` that blamed the user's watcher config),
165
+ and the decision is now a pure exported function, `nextBuildStep()`, with tests for the
166
+ contended orderings. `POST /report` was also found never to have received the CSRF gate — the
167
+ term had been spliced into two call sites by hand and the third missed, which is why all three
168
+ now share one named predicate.
169
+
170
+ Recorded here rather than buried because it is the same failure this project's notes already
171
+ describe from 1.9.0: work asserted in prose that the code did not do. A test that retypes the
172
+ condition it is testing is that failure wearing a lab coat.
173
+
174
+ ### What delegation guarantees (and what it refuses)
175
+
176
+ The delegation path above was hardened after a pre-release review found several ways it could
177
+ report a success nobody got. Worth stating, because "my build tool said it built" is a claim you
178
+ rely on:
179
+
180
+ - **It proves the build was yours.** `holder.port` is validated before it reaches a URL, the
181
+ request carries `?root=`, and the reply carries `pid`/`root`/`outputDir` which must match the
182
+ lock and the tree you asked for. A planted lock plus any listener answering `{"ok":true}`
183
+ previously printed "rebuilt this tree" and exited 0 over an untouched tree — stale locks from
184
+ other projects are ordinary on a dev machine, one of them usually on the shared default port.
185
+ - **It waits for the tree to settle**, not for whichever build finishes next — starting one
186
+ build and then parking, with a cap so a watcher that keeps firing degrades to a message
187
+ instead of a build storm. (The first attempt called `rebuild()` on **every pass**, and
188
+ `rebuild()` sets `pending` when a build is in flight — so the settled check could never pass
189
+ under contention and it re-armed forever, spinning the dev server into ~30 builds and a
190
+ `process.exit(1)` blaming the user's watcher config. It was correct when idle, which is why
191
+ it looked fine. A re-review caught it; the decision is now a pure function with tests for
192
+ the contended orderings, which had no coverage at all.) A reply used to be
193
+ released by an already-running build one line before a queued rebuild began wiping the output
194
+ directory — success reported while the tree was being emptied, right before the documented
195
+ `git add`.
196
+ - **It does not believe a reply it does not recognise.** A dev server predating this endpoint
197
+ answers the POST with the SPA shell; that now reports "restart your dev server" instead of
198
+ accusing your build of failing. Dev servers here live for days running the code they loaded,
199
+ so that is the normal upgrade path.
200
+ - **Its timeout is the server's real one.** The client promised ten minutes while `Bun.serve`
201
+ idles connections at 120s, so a _successful_ long build was reported as a failure. "Did not
202
+ answer in time — the build may still be running" is now distinguished from "unreachable".
203
+
204
+ `<tosi-pocket-bar>` also gained a public `close()`. The live-example bar was setting `open =
205
+ false` directly, which left the component's internal pin set — so it re-opened on hover, stuck
206
+ open over the content, and the next handle tap did nothing. On touch, where this release removes
207
+ the hover path, there was no way back at all.
208
+
209
+ ### Builds stopped fighting the dev server
210
+
211
+ `bun run build` with a dev server running now **asks it to build** instead of refusing, and
212
+ `bun run test-browser` uses its **own port (8798)** instead of reclaiming 8787 by killing
213
+ whatever holds it. Both produced the same workflow — kill the server, build, forget to restart
214
+ it — and the second twice took a live tunnel offline, where the symptom is a page reporting
215
+ "offline" with no hint which half died.
216
+
217
+ The generated ePub is **reproducible** now. It differed on every build, so it landed in every
218
+ diff and made a clean tree impossible after building — two independent causes:
219
+ `dcterms:modified` defaulted to `new Date()`, and the zip stored each file's mtime, and those
220
+ files had just been written. Entry mtimes are normalised to a constant before zipping (zip has
221
+ no flag for it; normalising the inputs is the standard fix), and the date is anchored to the
222
+ last commit that touched `package.json` rather than to a clock or to HEAD. After
223
+ commit-then-rebuild the tree carries one changed file (`docs/version.json`, which records the
224
+ commit by design) where it used to carry 68 plus a binary.
225
+
226
+ ### Icons
227
+
228
+ `shift` (⇧) — the standard shift-key outline, asked for by tosijs-3d and general enough to live
229
+ here rather than in one consumer. `tab` (⇥) — an arrow pointing at a vertical bar, built on
230
+ `arrowRight`'s proportions. And `fatArrowUp`/`fatArrowRight`/`fatArrowDown`/`fatArrowLeft`: one
231
+ drawing and four rotations via the same redirect mechanism the arrow and chevron families use.
232
+
233
+ The shift glyph is centred on 12,12 (apex y=4, base y=20; it was 3..20, half a pixel high) —
234
+ invisible head-on, and what makes each rotation land square instead of drifting off-centre.
235
+
236
+ With `delete`, `cornerDownLeft` and `command` already present, an on-screen keyboard now has
237
+ everything except caps-lock.
238
+
239
+ ### Smaller things
240
+
241
+ - **`<tosi-pocket-bar>` handles are brandable**: `--tosi-pocket-handle-bg`,
242
+ `--tosi-pocket-handle-radius` and `--tosi-pocket-handle-size`, all defaulting to exactly their
243
+ previous values, so nothing changes for an existing consumer. A circle needs all three —
244
+ `border-radius: 50%` on a content-sized flex button is an ellipse.
245
+ - The live-example handle uses them for the owl badge, and its **controls were reordered** so
246
+ maximize sits nearest the handle and tests furthest.
247
+ - The dead `.view-source` CSS went with the button it styled, rather than leaving rules for an
248
+ element nothing creates.
249
+ - **`bindText` in `<tosi-table>` briefly moved to `textContent`, then reverted back to
250
+ `bindText`.** `data-table.ts:2179` holds a proxy, which is the only shape the replacement
251
+ accepts — but with the deprecation withdrawn upstream, keeping it would have left the file
252
+ using two idioms for the same job with nothing gained. Net change: zero lines.
253
+
254
+ ## 1.12.8
255
+
256
+ ### Diffs: pick a side by clicking it, and see what actually changed
257
+
258
+ Two things, and they apply to **both** diff views — the resolvable one and the read-only one
259
+ used by `<tosi-code>`'s review overlay and the doc-browser's before-save screen.
260
+
261
+ **Clicking a coloured line picks that side**, equivalent to its button. The line is what a
262
+ reader is already looking at and pointing at; reaching for a small button to say "this one"
263
+ when the thing is under the cursor was friction with nothing behind it. Dragging to select
264
+ text does **not** change anything — those lines are also what you copy from, and without that
265
+ guard, selecting a line to copy it would silently flip the resolution and rewrite the value.
266
+
267
+ **Within a changed line, the words that actually differ are marked** more strongly than the
268
+ line around them. A whole-line wash says "something here changed", which for a one-word edit
269
+ throws away most of the signal — so the line colour tells you which side you are on, and the
270
+ run tells you what moved. `diffTokens(before, after)` and `tokenRunsForLines(lines)` are
271
+ exported if you want that data without the DOM.
272
+
273
+ Word runs, not characters: a character diff of `sat` → `sprawled` marks the shared `s` and
274
+ produces confetti. Whitespace and punctuation are tokens so the runs **reassemble each input
275
+ exactly** — a diff viewer that silently drops a space misrepresents the text it exists to show
276
+ — and whitespace between two changed words is absorbed into the change, or `sat on` renders as
277
+ two boxes with a hole punched between them. Lines over 400 tokens fall back to whole-line
278
+ marking rather than locking a tab on a minified bundle pasted into a diff.
279
+
280
+ The read-only view keeps its line order exactly. Pairing is computed per run of consecutive
281
+ changed lines, so an interleaved edit is marked correctly without the reordering that the
282
+ resolvable view's block grouping does.
283
+
284
+ `tokenRunsForLines` returns a **dense** array — one entry per input line, `undefined` where a
285
+ line has no runs. Worth stating because the obvious implementation (`new Array(n)`) is sparse,
286
+ and `map`/`forEach` silently skip holes: fine for the indexed read it was written for, a trap
287
+ for anyone iterating it.
288
+
289
+ ### Generated asset URLs are build-stamped
290
+
291
+ `/doc-system.css` and the hydration bundle are now emitted with a `?v=` stamp taken from the
292
+ same build identity that writes `/version.json`, so a page cannot claim one build while
293
+ loading another's cached bundle. Stale filenames going stale is not hypothetical — it happened
294
+ on this project's own doc site and cost a round of "is this a layout regression?" before
295
+ turning out to be a cached bundle, with a hard reload as the fix nobody thinks to try.
296
+
297
+ A query rather than content-hashed filenames, deliberately: `docs/` is committed in this repo
298
+ and its siblings, so hashing would add and delete a file on every build and put churn in every
299
+ diff.
300
+
301
+ **The stamp is the project's own version**, so it moves once per release — the granularity at
302
+ which a published site actually changes — and a rebuild between releases is byte-identical. The
303
+ first attempt derived it from the commit, which was precise and unusable: it re-stamped all 68
304
+ generated pages on every commit, and because the test lanes build too, it did not terminate
305
+ (commit → build → 68 dirty → commit → build). The honest tradeoff of the version, stated rather
306
+ than left to be discovered: two builds of the same version with different code share a stamp, so
307
+ a mid-version redeploy can still serve a cached bundle. That is exactly the previous state — no
308
+ worse — and it closes the case that actually bit, a cached bundle surviving _across_ releases.
309
+
310
+ Cross-origin URLs are left alone — `scriptUrl` may point at a CDN, and busting someone else's
311
+ cache was never the point — as are URLs that already carry a query or fragment.
312
+
313
+ Consumers of `tosijs-ui/site` get this automatically; `generateSite` also takes an explicit
314
+ `assetStamp` if you are driving it directly. Leave it unset and the output is unchanged.
315
+
3
316
  ## 1.12.7
4
317
 
5
318
  ### Live-example and doc-test failures now tell you where
@@ -334,6 +334,17 @@ const manualRedirects = {
334
334
  arrowDownRight: 'arrowUpRight90r',
335
335
  arrowDownLeft: 'arrowUpRight180r',
336
336
  arrowUpLeft: 'arrowUpRight270r',
337
+
338
+ // `shift` is the ⇧ key glyph and points UP, which is the wrong base for the
339
+ // `up-down-right-left/` folder convention (that expects a RIGHT-pointing base).
340
+ // Rather than redraw it sideways and make the keyboard glyph a rotation of an
341
+ // arrow nobody asked for, the four directions are declared here — the escape
342
+ // hatch this block exists for. The drawing is centred on 12,12 so each rotation
343
+ // lands square.
344
+ fatArrowUp: 'shift',
345
+ fatArrowRight: 'shift90r',
346
+ fatArrowDown: 'shift180r',
347
+ fatArrowLeft: 'shift270r',
337
348
  }
338
349
  Object.assign(iconRedirects, manualRedirects)
339
350
 
package/dist/diff.d.ts CHANGED
@@ -39,6 +39,32 @@ export declare function resolveDiff(blocks: DiffBlock[], choices: DiffResolution
39
39
  * in `after`), in display order.
40
40
  */
41
41
  export declare function diffLines(before: string, after: string): DiffLine[];
42
+ /** A run of text within a changed line, flagged if it is part of what actually differs. */
43
+ export interface TokenRun {
44
+ text: string;
45
+ changed: boolean;
46
+ }
47
+ /**
48
+ * Word-level diff of two lines, as runs that reassemble each input exactly.
49
+ *
50
+ * Returns everything flagged `changed` when the lines are too long to diff cheaply, or when
51
+ * they share nothing — in both cases the containing line highlight already says it.
52
+ */
53
+ export declare function diffTokens(before: string, after: string): {
54
+ removed: TokenRun[];
55
+ added: TokenRun[];
56
+ };
57
+ /**
58
+ * Token runs for each line of a FLAT line diff, pairing within each run of changed lines
59
+ * without reordering anything.
60
+ *
61
+ * The resolvable view groups changes into blocks, which reorders an interleaved edit
62
+ * (`-a +A -b +B` becomes `-a -b +A +B`) — better to resolve, but a changed rendering. The
63
+ * read-only view must keep its order exactly, and it turns out it does not have to give
64
+ * anything up for that: pairing is computed per run of consecutive non-context lines, and the
65
+ * lines are still emitted where they already were. Same marking, same order.
66
+ */
67
+ export declare function tokenRunsForLines(lines: DiffLine[]): Array<TokenRun[] | undefined>;
42
68
  interface DiffParts extends PartsMap {
43
69
  body: HTMLElement;
44
70
  }
@@ -68,6 +94,11 @@ export declare class TosiDiff extends Component<DiffParts> {
68
94
  whiteSpace: string;
69
95
  wordBreak: string;
70
96
  };
97
+ '.diff-line .line-text': {
98
+ minWidth: string;
99
+ whiteSpace: string;
100
+ wordBreak: string;
101
+ };
71
102
  '.diff-line .marker': {
72
103
  textAlign: string;
73
104
  userSelect: string;
@@ -111,6 +142,22 @@ export declare class TosiDiff extends Component<DiffParts> {
111
142
  background: string;
112
143
  color: string;
113
144
  };
145
+ '.diff-line .text.changed': {
146
+ borderRadius: string;
147
+ padding: string;
148
+ };
149
+ '.diff-add .text.changed': {
150
+ background: string;
151
+ };
152
+ '.diff-remove .text.changed': {
153
+ background: string;
154
+ };
155
+ '.diff-line[data-choice]': {
156
+ cursor: string;
157
+ };
158
+ '.diff-line[data-choice]:hover': {
159
+ filter: string;
160
+ };
114
161
  '.diff-line.not-chosen': {
115
162
  opacity: string;
116
163
  textDecoration: string;