tosijs-ui 1.12.3 → 1.12.5

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 (39) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/bin/tunnel.ts +26 -0
  3. package/dist/data-table.d.ts +1 -0
  4. package/dist/data-table.js +41 -1
  5. package/dist/doc-browser.js +50 -10
  6. package/dist/doc-system/render.d.ts +1 -1
  7. package/dist/doc-system/render.js +28 -1
  8. package/dist/doc-system/site/build-lock.d.ts +16 -0
  9. package/dist/doc-system/site/build-lock.js +20 -0
  10. package/dist/doc-system/site/debug-sink.d.ts +39 -0
  11. package/dist/doc-system/site/debug-sink.js +72 -0
  12. package/dist/doc-system/site/dev-auth.d.ts +47 -2
  13. package/dist/doc-system/site/dev-auth.js +70 -3
  14. package/dist/doc-system/site/dev-server.d.ts +20 -0
  15. package/dist/doc-system/site/dev-server.js +192 -15
  16. package/dist/doc-system/site/docs.d.ts +23 -0
  17. package/dist/doc-system/site/docs.js +121 -3
  18. package/dist/doc-system/site/invite-page.d.ts +20 -0
  19. package/dist/doc-system/site/invite-page.js +59 -0
  20. package/dist/doc-system/site/make-llms-txt.js +8 -1
  21. package/dist/doc-system/site/orchestrator.js +4 -0
  22. package/dist/doc-system/site/site-config.d.ts +28 -0
  23. package/dist/doc-system/site/sourcemap-check.d.ts +19 -0
  24. package/dist/doc-system/site/sourcemap-check.js +39 -0
  25. package/dist/iife.js +77 -77
  26. package/dist/iife.js.map +11 -11
  27. package/dist/live-example/code-transform.d.ts +29 -4
  28. package/dist/live-example/code-transform.js +62 -6
  29. package/dist/live-example/component.js +2 -2
  30. package/dist/live-example/execution.js +3 -3
  31. package/dist/live-example/test-harness.js +9 -2
  32. package/dist/side-nav.d.ts +9 -0
  33. package/dist/side-nav.js +36 -4
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/llms.txt +1 -1
  38. package/package.json +3 -2
  39. package/tls/create-dev-certs.sh +39 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,406 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.5
4
+
5
+ ### Fixed: 49 controls a screen reader announced as "button" and "edit text"
6
+
7
+ Lighthouse scored the doc site 91 on accessibility, and the cause was entirely mechanical.
8
+ `<tosi-table>` builds a column-options button that contains only an icon, and an editable cell
9
+ that is an empty `<input>` — neither has any text to derive an accessible name from. On the
10
+ data-table page alone that is 37 unnamed buttons and 12 unnamed fields. The doc-browser's search
11
+ box made it worse in a quieter way: it had a `placeholder` and nothing else, on **every page of
12
+ every site built with `tosijs-ui/site`**. A placeholder is not a label — it is an unreliable
13
+ accessible name and it vanishes the moment anything is typed.
14
+
15
+ The controls now carry a name: `title` on the column-options button, where the tooltip is a
16
+ genuine win for sighted mouse users too, and `aria-label` on the cells and the search field,
17
+ where a tooltip on every hover would just repeat the column header as noise.
18
+
19
+ To be precise about what `title` buys, because its two halves land on different people: the
20
+ **accessible name** is real and works — `title` feeds the accessible name computation, so
21
+ VoiceOver on iOS/iPadOS announces these controls, as do NVDA and JAWS. The **visible tooltip** is
22
+ mouse-only, and a screen-reader user was never relying on it.
23
+
24
+ No visible label is added, deliberately. The vertical-more glyph is the standard overflow
25
+ affordance and reads as one; spelling it out on every column header would be clutter that makes
26
+ the header worse for everyone in order to restate what the icon already says. The defect was that
27
+ the control had no name in the accessibility tree, and that is what is fixed.
28
+
29
+ Localized tables get this through the existing `data-tosi-localized` directive rather than a
30
+ `<tosi-localized>` child, because an `<input>` cannot contain a child element. Add
31
+ **`Column Options`** to your localized strings; the editable cells reuse the column names you
32
+ already localize, so an editable table wants those present even if you are happy with
33
+ untranslated headers.
34
+
35
+ The regression test is in `tests/a11y-names.pw.ts`, and its first version **passed with both
36
+ fixes reverted** — it waited for `buttons.length > 2`, which the nav chrome satisfies on its own,
37
+ and then asserted an empty list against a page that had not built the controls yet. `toEqual([])`
38
+ is trivially true when there is nothing to check. Every assertion there now has a positive
39
+ precondition that the things under test exist, and the mutation that removes the fixes fails it
40
+ by name.
41
+
42
+ Not fixed, and not ours: the two images without `alt` on the 3D page come from Babylon's own
43
+ default loading screen.
44
+
45
+ ### Fixed: on a narrow screen, tapping a nav link left you looking at the nav
46
+
47
+ A regression from 1.12.3's full-screen work, reported from a phone-width window. The sidebar
48
+ fills the screen, you tap a link, the URL changes — and the nav stays up. The article you asked
49
+ for never appears.
50
+
51
+ Leaving full-screen had been written as the tidy mirror of entering it: `navVisible = true`. But
52
+ that setter's show-the-nav branch writes `contentVisible = false`, and it ran on **every**
53
+ navigation to a non-full-screen page — immediately after the nav click handler had set
54
+ `contentVisible = true` to do precisely the thing the reader asked for. The layout code was
55
+ overwriting a decision that belongs to the person reading.
56
+
57
+ The two directions are asymmetric now, and that is the point rather than an oversight: entering
58
+ full-screen states what it needs, and stepping out of it releases only what it claimed
59
+ (`alwaysCompact`). Which pane is showing at a narrow width is not the layout's business.
60
+
61
+ **Nothing in the test suite could have caught this.** Every layout test ran at 1400px, where
62
+ normal mode shows the nav and the content together and `contentVisible` has no visible effect —
63
+ the bug was invisible at every width the tests used. There are now narrow-viewport tests for
64
+ both directions, and the mutation that restores the old line fails them.
65
+
66
+ ### `<tosi-sidenav>` no longer races a timer to hide its own load-time animation
67
+
68
+ Its defaults are the compact arrangement — 50/50 columns, margin -100% — so the first layout pass
69
+ moves it to wherever it actually belongs. With a transition live, that move _animates_: the
70
+ sidebar visibly slides into place on load.
71
+
72
+ The suppression was `setTimeout(…, 250)`, which is a race rather than a rule — it assumes the
73
+ first successful layout pass lands within 250ms of the element upgrading. Measured here it lands
74
+ ~70ms after (chromium 236→297ms, webkit 213→291, firefox 229→304), leaving ~175ms spare, which
75
+ is exactly why this never reproduced locally. But `handleResize` returns early while
76
+ `offsetParent` is null, so on a bigger corpus or a slower device the first pass can be deferred
77
+ past the window, and then the animation is visible.
78
+
79
+ It now releases on the first pass that actually computes something, one frame later so the
80
+ corrected values paint before transitions come back. No duration is correct for every page; a
81
+ rule needs no number.
82
+
83
+ **This does not eliminate the hydration flash, and it is worth being honest about the limit.** It
84
+ removes one race. What remains is the interval between the element upgrading — at which point its
85
+ shadow defaults apply — and the first layout pass correcting them: ~70ms here, invisible, but a
86
+ real window in which the element is painted wrong rather than not painted. The principled next
87
+ step is for the defaults to _be_ the common case, and it is written up in `TODO.md` rather than
88
+ attempted blind, since the flash does not reproduce on this machine and changing paint defaults
89
+ you cannot observe is how you trade a flash you cannot see for one you can.
90
+
91
+ ### Decided: a nav-toggle override does not survive Back
92
+
93
+ Left open in 1.12.3 and now settled — **metadata wins on arrival**. A page declaring
94
+ `layout: "full-screen"` is full-screen however you reach it: a link, Back, or a fresh load. The
95
+ override is a transient viewing choice rather than a property of the document, and since the nav
96
+ button is always offered on such a page, restoring it is one tap.
97
+
98
+ Worth recording that the alternative was real. Browsers restore scroll position on Back, so "put
99
+ the view back how I left it" is an established expectation, not an exotic one. It was rejected
100
+ because the state would have to live in `history.state`, which makes the same URL behave
101
+ differently arrived-at-by-Back than clicked — trading the current surprise for a new one — and
102
+ adds state to a path that has none.
103
+
104
+ No behaviour change; the tests that pinned it now say it is settled rather than pending.
105
+
106
+ ### The invite screen has a code box, so an installed PWA can let itself back in ([#75](https://github.com/tonioloewald/tosijs-ui/issues/75))
107
+
108
+ The screen offered only a command to run _on the machine hosting the server_. The devices a
109
+ tunnel exists for have no such keyboard — and an installed PWA has no address bar either, because
110
+ iOS gives a Home Screen app its own cookie jar and its own launch URL. An unauthenticated launch
111
+ was a black rectangle with no way out of it.
112
+
113
+ It now offers the 7-character code box directly: the same affordance as the `dev.tosijs.net`
114
+ landing page, moved to where it is actually needed. A plain GET form produces `/?t=CODE`, which is
115
+ the path the link already takes, so this needed no new server code. The input disables
116
+ autocapitalize, autocorrect and spellcheck, because the code is case-insensitive and a phone
117
+ keyboard's help is noise; there is a viewport meta, because a code box you cannot hit is not much
118
+ better than none.
119
+
120
+ **What this deliberately does not attempt** is carrying a session across the install. The
121
+ separate cookie jar is a privacy feature rather than a bug, and the issue's suggested "don't
122
+ redirect" fix would bake a live credential into a home-screen icon — working once, then failing
123
+ forever with no address bar to correct it. Seven characters is the better trade.
124
+
125
+ ### A leading metadata comment is no longer the page title ([#100](https://github.com/tonioloewald/tosijs-ui/issues/100))
126
+
127
+ The title came from line one, literally. A file opening with its metadata block — the documented
128
+ way to set `order` or `parent` — published under the title `<!--{ "order": 2 }-->`, while `order`
129
+ itself parsed correctly. So the metadata visibly worked and the title visibly did not, which is a
130
+ confusing pair to be handed. Hit while adding `CHANGELOG.md` and `Migration.md` to a doc site,
131
+ which is precisely where a leading metadata comment is most natural.
132
+
133
+ Leading HTML comments are skipped now, including ones that wrap across lines, and an
134
+ unterminated comment yields no title rather than lifting text out of it — publishing something
135
+ the author had deliberately hidden would be worse than an empty string. One rule, used by both
136
+ call sites that previously had their own copy of the line.
137
+
138
+ No URLs move: slugs derive from the filename, not the title, so an affected page gets a correct
139
+ title at the address it already had.
140
+
141
+ **The other half of that issue was already fixed**: `docPaths` entries have been in the dev
142
+ server's watch set since #49, so editing a root `Migration.md` does trigger a rebuild.
143
+
144
+ ### `debugSink` — telemetry back from a page that is not on this machine ([#99](https://github.com/tonioloewald/tosijs-ui/issues/99))
145
+
146
+ `haltijaDev` is localhost-gated for good reason: an injected drive-my-browser channel reachable
147
+ from the network is RCE-adjacent. But that leaves a page served to a headset or a phone with no
148
+ way to say **anything** — and in a WebXR session there is no console and `requestAnimationFrame`
149
+ is suspended, so every usual readback path is gone at once. The remaining channel is a human
150
+ reading an in-scene panel aloud.
151
+
152
+ `debugSink: true` accepts `POST /__debug-sink` and appends one line per event to a JSONL file
153
+ whose path it prints at startup, so an agent can `tail -f` it:
154
+
155
+ ```javascript
156
+ navigator.sendBeacon('/__debug-sink', JSON.stringify({ t: Date.now(), tag, ...event }))
157
+ ```
158
+
159
+ `sendBeacon` is the point — it needs no headers, reads no response, and survives the page going
160
+ away.
161
+
162
+ **Why this is reachable off-loopback when `/__docstore/source` is not.** It appends opaque bytes
163
+ to a scratch file, outside the repo, that the build never reads and nothing serves back. No code
164
+ runs; nothing is reconfigured. The source endpoint is remote code execution by design and stays
165
+ gated. This is a log.
166
+
167
+ Still **off by default**: an unauthenticated write endpoint on a LAN-reachable server should be a
168
+ decision you made rather than one you inherited. Bounded so a looping page cannot fill a disk —
169
+ bodies over 64 KB refused (413), the file stops at 32 MB (507). Newlines in a payload are
170
+ escaped, because one event must be one line or `tail -f` shows a split event and the reader
171
+ cannot tell.
172
+
173
+ ### The dev cert covers the machine's LAN IP ([#92](https://github.com/tonioloewald/tosijs-ui/issues/92))
174
+
175
+ Every name the cert carried was useful only _on_ this machine, except `<host>.local` — which is
176
+ precisely the one another device is most likely to fail to resolve. Reported from a Quest:
177
+ `tosi.local` resolved to **IPv6 only**, including a link-local `fe80::…` a headset gets and
178
+ cannot route, while the LAN IP worked. The host never sees it, because it resolves mDNS
179
+ natively, so it fails only on the devices the name was added for.
180
+
181
+ Private ranges only (10/8, 172.16/12, 192.168/16) — a public address on a dev cert would be a
182
+ name this machine has no business claiming. A DHCP lease change moves the address and the cert
183
+ does not follow; the script says so, and prints the URLs another device can use.
184
+
185
+ Note this fixes the **name mismatch**, not the untrusted CA: mkcert's root still is not on the
186
+ headset, so you either install it there or use the tunnel, which has a real certificate.
187
+
188
+ ### A dropped session says the server restarted, instead of blaming your cookies ([#114](https://github.com/tonioloewald/tosijs-ui/issues/114))
189
+
190
+ Sessions live in memory and die with the process. That is the design, not an oversight: a
191
+ credential should not outlive the thing that granted it, and nothing about a session — or the
192
+ token exchanged for it — is ever written to disk. **That has not changed and will not.**
193
+
194
+ What was wrong is that a cookie from a previous run and a cookie the server had never seen
195
+ produced the _identical_ screen. So a reader whose dev server had restarted was told "invite
196
+ links expire", and reasonably concluded their cookie was expiring — the one explanation the
197
+ evidence ruled out. The reporter's own words: the misleading failure "is the part I would most
198
+ want fixed".
199
+
200
+ The cookie now carries a per-process boot id (`<bootId>.<token>`), so the next process can tell a
201
+ cookie it _issued_ from one it has never seen. Both are still refused, identically; only the
202
+ sentence differs:
203
+
204
+ > **The dev server restarted, so your session ended with it.** Sessions are held in memory on
205
+ > purpose — they never outlive the process that issued them, and they are never written to disk.
206
+ > Your browser still holds the cookie; there is simply nothing on this side to match it to.
207
+
208
+ The prefix is not a credential and admits nobody: a cookie with the right run id and a wrong
209
+ token is refused exactly as before, and there is a test asserting that. An absent or malformed
210
+ cookie is _not_ reported as a restart — claiming one we cannot evidence would be its own lie.
211
+
212
+ ### A bundle that references an unserved sourcemap now says so at build time ([#103](https://github.com/tonioloewald/tosijs-ui/issues/103))
213
+
214
+ `--sourcemap=linked` appends a `sourceMappingURL` comment, so devtools fetches that file on every
215
+ load. When the map is not in the served root the fetch fails — in exactly the session where
216
+ someone is reading the console carefully for something else. It cost the reporter a wrong
217
+ hypothesis while chasing a slow load.
218
+
219
+ The uglier half was already fixed by #116 in this same release: the SPA fallback used to answer
220
+ `.map` requests with the **HTML shell**, so devtools received a web page where a JSON map should
221
+ be. Missing assets 404 now.
222
+
223
+ The remaining half is a build-time warning naming the file and where it was expected. It checks
224
+ what is **served**, not what was built — those differ whenever `bundleOutDir` is set, and the
225
+ copy across is best-effort by design, so trusting the step is what let this ship. It warns rather
226
+ than fails: a map is a debugging convenience and no site should fail to build without one.
227
+
228
+ The check is a pure, tested function rather than an inline block, because the branch it lives in
229
+ is one **this repo's own build never takes** — our bundle comes from `bin/dev.ts`, not from
230
+ `buildSite`. That is exactly why the bug reached an adopter instead of us.
231
+
232
+ ### `resolveUnder` asserts path containment instead of inheriting it ([#96](https://github.com/tonioloewald/tosijs-ui/issues/96))
233
+
234
+ Defence in depth: there is no live traversal, and the reporter checked the classes carefully —
235
+ `/../../../etc/passwd`, percent-encoded and double-encoded forms all stay inside the served root
236
+ today. But they stay inside for two reasons the static handler does not own: the WHATWG URL
237
+ parser collapses `../` before `.pathname` is read, and `.pathname` is never the raw request line.
238
+ Both are properties of the **caller**, so a future call with a path from a config value, a
239
+ manifest entry or a header inherits nothing. The asserted version already lived ten lines away
240
+ in the same file.
241
+
242
+ Now an exported, tested predicate — a test that went through the server would prove nothing,
243
+ since the URL parser makes a violation unreachable from outside.
244
+
245
+ ### `bun run stop` — stop THIS project's dev server, not every one on the machine ([#117](https://github.com/tonioloewald/tosijs-ui/issues/117))
246
+
247
+ There was no way to stop a dev server, so what everyone reached for was
248
+ `pkill -f 'bun bin/dev.ts'` — which matches **every** dev server on the machine, because every
249
+ project on this pipeline runs an identical command line. A sibling checkout, a worktree or
250
+ another agent's session died to a command that reads as "restart mine". Observed five times in
251
+ one working day, twice while a tunnel link was in use by a remote reviewer.
252
+
253
+ The record needed for this already existed: the build lock stores `pid`, `port` and `root`,
254
+ per-project, with staleness decided by liveness rather than age. It needed a reader and a
255
+ command. `bun run stop` (or `bun bin/dev.ts --stop`) signals that pid and nothing else, and says
256
+ plainly when nothing is running.
257
+
258
+ ### The dev server can no longer die silently ([#91](https://github.com/tonioloewald/tosijs-ui/issues/91))
259
+
260
+ Reported after a ~9.4h run: `pgrep` said running, `curl` got nothing, the last log line was a
261
+ **successful** build, and there was no error, signal or exit anywhere. Every diagnostic a person
262
+ would reach for answered "fine"; it was found the next day because a human said the site was
263
+ down.
264
+
265
+ Worth correcting the report's own diagnosis, because it does not survive the evidence: the idle
266
+ path **announces itself** before exiting, and no such line was logged — so the idle timeout is
267
+ not what stopped that listener. What did is still unknown, which is exactly why the fix targets
268
+ the symptom rather than a cause.
269
+
270
+ The health tick now probes the server's own listener (`GET /__alive`, a bare 204 that
271
+ deliberately does **not** count as activity — a self-check that did would hold the idle timer
272
+ open forever). Two consecutive failures and it exits non-zero with an explanation. A process that
273
+ cannot serve has no value, and the one useful thing left to it is to stop claiming to be alive.
274
+
275
+ ### `tosijs-tunnel --link` stops printing links that cannot work ([#94](https://github.com/tonioloewald/tosijs-ui/issues/94))
276
+
277
+ `--link` asks the dev server for a token, which succeeds whether or not a tunnel exists. The URL
278
+ is well-formed and the token is real — it just answers 503, because nothing is listening
279
+ upstream. Printed with exit 0 and no comment, that is indistinguishable from the far side being
280
+ down.
281
+
282
+ It still prints the link, since you may be about to start the tunnel, but it now says so and
283
+ exits non-zero, so a script treating 0 as "a usable link was produced" is telling the truth.
284
+
285
+ ### Doc tests survive a context key with a slash in it ([#111](https://github.com/tonioloewald/tosijs-ui/issues/111), [#112](https://github.com/tonioloewald/tosijs-ui/issues/112), [#109](https://github.com/tonioloewald/tosijs-ui/issues/109))
286
+
287
+ Context keys become **function parameter names** — the runner builds each block as
288
+ `new Fn(...contextKeys, body)`. `test-harness.ts` carried its own copy of the rule that turns a
289
+ key into an identifier, `key.replace(/-/g, '')`, which strips hyphens and leaves slashes and
290
+ `@`. So an ordinary specifier like `'tosijs-3d/demo-utils'` became the parameter name
291
+ `tosijs3d/demoutils`, and **every test in that file died before one of them ran** with V8's
292
+ `Arg string terminates parameters early`. The examples on the same page rendered fine, because
293
+ they went through the shared sanitiser — so nothing looked wrong except a red badge.
294
+
295
+ One implementation now, in `code-transform.ts`, used by all five call sites. It also handles
296
+ what the shared version had never needed to: a leading digit (`'3d-tools'`), a reserved word
297
+ (`'class'`), and a key that reduces to nothing.
298
+
299
+ **Two keys that reduce to the same identifier now throw, by name.** Quietly suffixing the second
300
+ would leave one module bound to a name no rewritten import references — undefined at runtime with
301
+ nothing to read. The message says which two keys and what to do:
302
+
303
+ ```
304
+ example context keys "tosijs-3d" and "tosijs/3d" both reduce to the identifier "tosijs3d",
305
+ so an import from one of them could not be told from the other. Rename one of the context keys.
306
+ ```
307
+
308
+ There is also a structural test asserting **nothing outside `code-transform.ts` sanitises a
309
+ context key**. That is the one that would have caught this: every unit test of the shared helper
310
+ passes with the divergent copy restored, because they exercise the helper rather than the call
311
+ site — which is exactly how the bug survived.
312
+
313
+ ### A missing asset 404s instead of impersonating a page ([#116](https://github.com/tonioloewald/tosijs-ui/issues/116))
314
+
315
+ The dev server fell back to the SPA shell for **any** unknown path, so a missing
316
+ `waterbump.png` answered `200 text/html`. Babylon's `Texture` fetched it, failed to decode a web
317
+ page as an image, and substituted its checkerboard — a missing asset presented as a _styling
318
+ choice_, and was complimented before it was diagnosed.
319
+
320
+ Unknown **routes** still get the shell, because client-side routing depends on it. The
321
+ distinguishing signal is a dot in the last path segment: an extension was asked for, so a file
322
+ was meant. `/v1.2/guide` is still a route; `/thing.png` is not. The 404 names the path, because
323
+ a bare "File not found" sends people to the wrong layer — the reporter went through their
324
+ texture pipeline before suspecting the server.
325
+
326
+ ### `staticDirs` are watched, so a replaced asset is not served stale ([#110](https://github.com/tonioloewald/tosijs-ui/issues/110))
327
+
328
+ `buildSite` copies `staticDirs` into the output on every build, but nothing watched them — so
329
+ re-exporting a GLB over `static/model.glb` left the previous copy served indefinitely, with no
330
+ error and no hint. Both files exist and only their contents differ, which is the hardest version
331
+ of stale to see, and the workaround people find is touching a source file to provoke a rebuild.
332
+
333
+ Same shape as #49, where `docPaths` was the omission. Verified end to end: a replaced static file
334
+ is now picked up in about half a second.
335
+
336
+ ### A lone custom element is no longer wrapped in a paragraph ([#115](https://github.com/tonioloewald/tosijs-ui/issues/115))
337
+
338
+ Reported by tosijs-3d: a doc page whose entire content is one custom element — the case
339
+ `layout: "full-screen"` exists for — got an element that would not fill. marked classifies raw
340
+ HTML by tag name and cannot know whether an unknown tag is block or inline, so
341
+ `<my-editor></my-editor>` alone on a line came out as `<p><my-editor></my-editor></p>`. That
342
+ paragraph is auto-height, so `height: 100%` resolved against a **33px** box inside an 842px
343
+ content area.
344
+
345
+ Measured before and after on the same page: 33px, then **842 of 842** on all three engines. The
346
+ comment promising "a definite height, so a child asking for `height: 100%` gets one" is finally
347
+ true rather than aspirational.
348
+
349
+ The unwrap is deliberately narrow, since a heuristic here earns its own bugs: it fires only when
350
+ a paragraph's entire content is a single element whose tag name contains a hyphen — a custom
351
+ element, the one case marked provably cannot classify. `<p><img></p>`, `<p><em>text</em></p>`
352
+ and `<p>see <my-thing></my-thing> here</p>` are all left exactly as they are, and mutation
353
+ testing covers both directions — unwrapping everything fails three tests, unwrapping nothing
354
+ fails one.
355
+
356
+ ### The inline doc tier runs at two widths now
357
+
358
+ The same gap, one level down: the doc-test runner's iframe was hardcoded to 800x600, so **every
359
+ inline `test` block had only ever run wide**. Its size is now overridable, and the Playwright
360
+ doc-test spec runs the whole tier a second time at 390x844.
361
+
362
+ An iframe is a real viewport, so this is not a simulation — verified that
363
+ `matchMedia('(max-width: 600px)')` matches inside a 390px frame and not a 1200px one, and that
364
+ the override reaches the runner (390px frame, 386 inner; 800/796 without it).
365
+
366
+ **Widths only, deliberately.** Touch cannot be emulated from inside a page: an iframe inherits
367
+ the host's input characteristics, so `(pointer: coarse)` stays false and `maxTouchPoints` stays 0
368
+ however small the frame is. `navigator.maxTouchPoints` _can_ be redefined, and doing so would be
369
+ worse than skipping it — the property would lie while the media queries told the truth, so a test
370
+ could pass against a page behaving as desktop. Touch belongs to a Playwright context
371
+ (`hasTouch`, `devices[…]`); noted in `TODO.md` as worth its own spec, since everything
372
+ touch-shaped here is currently tested only with a mouse.
373
+
374
+ ## 1.12.4
375
+
376
+ ### `.tjs` source files are documented again ([#108](https://github.com/tonioloewald/tosijs-ui/issues/108))
377
+
378
+ Converting a source file from `.ts` to native `.tjs` **silently deleted its documentation page**,
379
+ and its `llms.txt` entry with it. Reported from tosijs, mid-port: one page lost per converted
380
+ module.
381
+
382
+ The silence was the real defect. The build exited 0, and the internal-link check passed —
383
+ a page that was never generated is linked from nowhere, so "41 slugs, no 404s" is a _pass_. The
384
+ only signal was a slug count nothing asserts on. `'x.tjs'.endsWith('.ts')` is `false`, so the
385
+ `llms.txt` filter excluded it too, without even the mercy of an accidental match.
386
+
387
+ `.tjs` is now scraped, in both places. The doc-block syntax is identical across these languages —
388
+ a doc block is just a comment — so there is nothing language-specific to support. The list _is_
389
+ the feature, which is exactly why an omission from it is invisible.
390
+
391
+ **And a guard for the next one.** A file carrying what looks like a doc block, in an extension we
392
+ do not scrape, is now **named** in the build output rather than passed over:
393
+
394
+ ```
395
+ ⚠️ 1 file(s) look documented but are not scraped, so they have no page:
396
+ src/thing.vue
397
+ Scraped extensions: .ts, .js, .tjs, .css. Add one to SCRAPED_SOURCE_EXTENSIONS if it should be documented.
398
+ ```
399
+
400
+ It warns rather than fails: a file may legitimately contain the sequence without wanting to be
401
+ documentation, and a doc site that refused to build over a comment would be worse than the bug.
402
+ But whatever the next extension turns out to be, its absence can no longer happen quietly.
403
+
3
404
  ## 1.12.3
4
405
 
5
406
  ### Back now has layout coverage, because a reader noticed before the tests did
package/bin/tunnel.ts CHANGED
@@ -191,6 +191,32 @@ if (has('link')) {
191
191
  `\n🔗 Edit link:\n ${url}\n` +
192
192
  (code ? `\n code: ${code} (case-insensitive)\n` : '')
193
193
  )
194
+
195
+ /*
196
+ A link nobody can reach is worse than no link.
197
+
198
+ `--link` asks the dev server for a token, which succeeds whether or not a tunnel exists — the
199
+ URL is well-formed and the token is real, it just resolves to a 503 from the reverse proxy
200
+ because nothing is listening upstream. Printed with exit 0 and no comment, that is
201
+ indistinguishable from the far side being down, and the reporter lost a chunk of an afternoon
202
+ to the difference (#94).
203
+
204
+ Checked AFTER printing, deliberately: the link is still the thing you asked for, and you may
205
+ be about to start the tunnel. What changes is that the output says so and the exit code agrees
206
+ — a script that treats 0 as "a usable link was produced" is now telling the truth.
207
+
208
+ `running()` is the same predicate `--status` uses, matching this project's own forward by its
209
+ exact ports and host rather than by an argv substring.
210
+ */
211
+ const forwards = await running()
212
+ if (forwards.length === 0) {
213
+ console.error(
214
+ `⚠️ No tunnel is running, so that link will answer 503.\n` +
215
+ ` The token is real — nothing is listening on the far side yet.\n` +
216
+ ` Start one with \`tosijs-tunnel\` (or \`bun run tunnel\`), then use the link.\n`
217
+ )
218
+ process.exit(1)
219
+ }
194
220
  process.exit(0)
195
221
  }
196
222
 
@@ -341,6 +341,7 @@ export declare class TosiTable extends WebComponent {
341
341
  sortByColumn: (columnOptions: ColumnOptions, direction?: "ascending" | "descending" | "auto") => void;
342
342
  popColumnMenu: (target: HTMLElement, options: ColumnOptions) => void;
343
343
  get captionSpan(): ElementCreator;
344
+ labelAttrs(key: string, attr?: string): Record<string, string>;
344
345
  get visibleRows(): any[];
345
346
  get visibleSelectedRows(): any[];
346
347
  get selectedRows(): any[];
@@ -343,6 +343,7 @@ const table = tosiTable({
343
343
  return button(
344
344
  {
345
345
  class: 'td actions-btn',
346
+ title: 'Row actions',
346
347
  onClick(e) { e.stopPropagation() },
347
348
  onMouseup(e) { e.stopPropagation() },
348
349
  },
@@ -979,8 +980,11 @@ You'll need to make sure your localized strings include:
979
980
  - Unpin
980
981
  - Left
981
982
  - Right
983
+ - Column Options
982
984
 
983
- As well as any column names you want localized.
985
+ As well as any column names you want localized — these are also used as the accessible name
986
+ (`aria-label`) of each editable cell, so an editable table wants them present even if you are
987
+ happy with the untranslated headers.
984
988
  */
985
989
  /*{ "parent": "Components" }*/
986
990
  import { Component as WebComponent, elements, vars, varDefault, tosiValue, getListItem, getListBinding, tosi, } from 'tosijs';
@@ -1875,6 +1879,7 @@ export class TosiTable extends WebComponent {
1875
1879
  role: 'gridcell',
1876
1880
  tabindex: -1,
1877
1881
  ariaColindex: String(colIndex + 1),
1882
+ ...this.labelAttrs(typeof col.name === 'string' ? col.name : col.prop || '', 'aria-label'),
1878
1883
  style,
1879
1884
  onMouseup: stop,
1880
1885
  onTouchend: stop,
@@ -2051,6 +2056,7 @@ export class TosiTable extends WebComponent {
2051
2056
  const menuButton = !(this.nosort && this.nohide)
2052
2057
  ? button({
2053
2058
  class: 'menu-trigger',
2059
+ ...this.labelAttrs('Column Options'),
2054
2060
  onClick(event) {
2055
2061
  popColumnMenu(event.target, col);
2056
2062
  event.stopPropagation();
@@ -2854,6 +2860,40 @@ export class TosiTable extends WebComponent {
2854
2860
  get captionSpan() {
2855
2861
  return this.localized ? tosiLocalized : span;
2856
2862
  }
2863
+ /*
2864
+ An accessible NAME for a control whose visible content carries none: the column menu is an
2865
+ icon alone, and an editable cell is an empty input. A screen reader announced them as bare
2866
+ "button" and "edit text" — 49 of them on the data-table doc page.
2867
+
2868
+ `title` for the menu button rather than `aria-label`, because title counts toward the
2869
+ accessible name AND shows a tooltip, so it serves sighted mouse users too. The cells get
2870
+ `aria-label` instead: a tooltip on every cell hover, repeating the column name the header
2871
+ already displays, is noise.
2872
+
2873
+ The two halves of `title` reach different people. The accessible NAME works — it is part of
2874
+ the accessible name computation, so VoiceOver on iOS/iPadOS announces it, as do NVDA and
2875
+ JAWS. The VISIBLE tooltip is mouse-only, and a screen-reader user was never relying on it.
2876
+
2877
+ No visible label, by design: the vertical-more glyph is the standard overflow affordance and
2878
+ is read as one, so labelling it on every column header would be clutter that restates what
2879
+ the icon already says. The defect being fixed is an empty accessibility tree, not an unclear
2880
+ UI.
2881
+
2882
+ The `data-tosi-localized` directive, rather than a `<tosi-localized>` child, because it
2883
+ re-applies on locale change and needs no child element — an `<input>` cannot contain one.
2884
+ It is filtered to mutations of the data attribute itself, so writing the target attribute
2885
+ here does not re-enter the observer.
2886
+ */
2887
+ labelAttrs(key, attr = 'title') {
2888
+ if (!key)
2889
+ return {};
2890
+ return this.localized
2891
+ ? {
2892
+ [attr]: localize(key),
2893
+ 'data-tosi-localized': JSON.stringify({ [attr]: key }),
2894
+ }
2895
+ : { [attr]: key };
2896
+ }
2857
2897
  get visibleRows() {
2858
2898
  return tosiValue(this.rowData.visible);
2859
2899
  }
@@ -547,6 +547,11 @@ export function createDocBrowser(options) {
547
547
  const searchField = input({
548
548
  slot: 'nav',
549
549
  placeholder: 'search',
550
+ // A placeholder is not a label: it is not a reliable accessible name, and it
551
+ // disappears the moment anything is typed. This is the only text input in the
552
+ // doc-browser chrome, so it was the one unnamed field on every page of every
553
+ // site built with `tosijs-ui/site`.
554
+ 'aria-label': 'Search documentation',
550
555
  type: 'search',
551
556
  style: {
552
557
  width: 'calc(100% - 10px)',
@@ -625,9 +630,28 @@ export function createDocBrowser(options) {
625
630
  return;
626
631
  if (appliedFullScreen === wantsFullScreen)
627
632
  return;
628
- // The same one control the nav button uses — the layout is just asking for the nav to be
629
- // away, and the sidenav decides how that is achieved.
630
- sidenav.navVisible = !wantsFullScreen;
633
+ /*
634
+ ASYMMETRIC on purpose, and the symmetric version was a real regression.
635
+
636
+ Entering full-screen asks for the nav to be away, and `navVisible` is the right control:
637
+ the sidenav decides how that is achieved at the current width.
638
+
639
+ LEAVING it must only undo `alwaysCompact`. Writing `navVisible = true` looks like the tidy
640
+ mirror image, and it clobbers `contentVisible` — which on a narrow screen is owned by the
641
+ person reading. Tapping a nav link there is supposed to switch you to the content; the nav
642
+ click handler sets `contentVisible = true` to do exactly that, and this ran afterwards on
643
+ the same navigation and set it straight back to false. The nav stayed up and the article
644
+ never appeared: reported as "touching a link in the sidebar doesn't switch to content
645
+ anymore", and correctly blamed on the full-screen work.
646
+
647
+ So: full-screen states what it needs, and stepping out of it states only what it is
648
+ releasing. Whether the nav or the content is showing at a narrow width is not this code's
649
+ business.
650
+ */
651
+ if (wantsFullScreen)
652
+ sidenav.navVisible = false;
653
+ else
654
+ sidenav.alwaysCompact = false;
631
655
  appliedFullScreen = wantsFullScreen;
632
656
  refreshNavToggle();
633
657
  };
@@ -1681,14 +1705,30 @@ export function createDocBrowser(options) {
1681
1705
  return;
1682
1706
  }
1683
1707
  const currentFilename = String(app.currentDoc.filename);
1684
- // Create a hidden iframe that loads the full page. Keep its real 800x600
1685
- // layout size (layout-dependent example tests need it) but park it
1686
- // off-screen rather than rely on opacity:0 over the visible page — Chromium
1687
- // (and haltija on top of it) still composites a 0-opacity layer at 0,0,
1688
- // which flashes as the frame navigates page to page.
1708
+ /*
1709
+ Create a hidden iframe that loads the full page. Keep a real layout size — layout-dependent
1710
+ example tests need one — but park it off-screen rather than rely on opacity:0 over the
1711
+ visible page: Chromium (and haltija on top of it) still composites a 0-opacity layer at 0,0,
1712
+ which flashes as the frame navigates page to page.
1713
+
1714
+ THE SIZE IS OVERRIDABLE, because 800x600 was the only width the inline tier had ever run at.
1715
+ A doc-system layout regression at phone width shipped in 1.12.3 and was found by a reader,
1716
+ not by the suite: every test ran wide, where the two panes show together and the bug is
1717
+ invisible. An iframe is a real viewport — `matchMedia` and element sizing answer to its
1718
+ dimensions, verified — so re-running the tier narrow costs nothing but the size.
1719
+
1720
+ Only dimensions, deliberately. Touch cannot be faked from inside a page: an iframe inherits
1721
+ the host's input characteristics, so `(pointer: coarse)` stays false and `maxTouchPoints`
1722
+ stays 0 however small the frame is. `navigator.maxTouchPoints` can be redefined, and doing so
1723
+ would be worse than useless — it lies to code that reads the property while the media queries
1724
+ still say fine-pointer, so tests would pass on a page behaving as desktop. Touch emulation
1725
+ belongs to the Playwright lane, which can set it per context.
1726
+ */
1727
+ const vp = globalThis.__tosiTestViewport;
1728
+ const vpWidth = Number(vp?.width) > 0 ? Number(vp?.width) : 800;
1729
+ const vpHeight = Number(vp?.height) > 0 ? Number(vp?.height) : 600;
1689
1730
  const testFrame = document.createElement('iframe');
1690
- testFrame.style.cssText =
1691
- 'position: fixed; left: -10000px; top: 0; width: 800px; height: 600px; opacity: 0; pointer-events: none;';
1731
+ testFrame.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${vpWidth}px; height: ${vpHeight}px; opacity: 0; pointer-events: none;`;
1692
1732
  document.body.appendChild(testFrame);
1693
1733
  // Listen for test results posted from the iframe
1694
1734
  const messageHandler = (event) => {
@@ -3,7 +3,7 @@ export interface ExampleBake {
3
3
  js: string;
4
4
  }
5
5
  export type ExampleBakes = Map<string, ExampleBake>;
6
- /** Render a doc's markdown text to HTML (synchronous, default marked options). */
6
+ export declare function unwrapLoneCustomElements(html: string): string;
7
7
  export declare function renderDocMarkdown(text: string, opts?: {
8
8
  bakes?: ExampleBakes;
9
9
  }): string;