tosijs-ui 1.12.4 → 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.
- package/CHANGELOG.md +371 -0
- package/bin/tunnel.ts +26 -0
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js +41 -1
- package/dist/doc-browser.js +50 -10
- package/dist/doc-system/render.d.ts +1 -1
- package/dist/doc-system/render.js +28 -1
- package/dist/doc-system/site/build-lock.d.ts +16 -0
- package/dist/doc-system/site/build-lock.js +20 -0
- package/dist/doc-system/site/debug-sink.d.ts +39 -0
- package/dist/doc-system/site/debug-sink.js +72 -0
- package/dist/doc-system/site/dev-auth.d.ts +47 -2
- package/dist/doc-system/site/dev-auth.js +70 -3
- package/dist/doc-system/site/dev-server.d.ts +20 -0
- package/dist/doc-system/site/dev-server.js +192 -15
- package/dist/doc-system/site/docs.d.ts +22 -0
- package/dist/doc-system/site/docs.js +43 -2
- package/dist/doc-system/site/invite-page.d.ts +20 -0
- package/dist/doc-system/site/invite-page.js +59 -0
- package/dist/doc-system/site/orchestrator.js +4 -0
- package/dist/doc-system/site/site-config.d.ts +28 -0
- package/dist/doc-system/site/sourcemap-check.d.ts +19 -0
- package/dist/doc-system/site/sourcemap-check.js +39 -0
- package/dist/iife.js +77 -77
- package/dist/iife.js.map +11 -11
- package/dist/live-example/code-transform.d.ts +29 -4
- package/dist/live-example/code-transform.js +62 -6
- package/dist/live-example/component.js +2 -2
- package/dist/live-example/execution.js +3 -3
- package/dist/live-example/test-harness.js +9 -2
- package/dist/side-nav.d.ts +9 -0
- package/dist/side-nav.js +36 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +3 -2
- package/tls/create-dev-certs.sh +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,376 @@
|
|
|
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
|
+
|
|
3
374
|
## 1.12.4
|
|
4
375
|
|
|
5
376
|
### `.tjs` source files are documented again ([#108](https://github.com/tonioloewald/tosijs-ui/issues/108))
|
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
|
|
package/dist/data-table.d.ts
CHANGED
|
@@ -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[];
|
package/dist/data-table.js
CHANGED
|
@@ -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
|
}
|
package/dist/doc-browser.js
CHANGED
|
@@ -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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
6
|
+
export declare function unwrapLoneCustomElements(html: string): string;
|
|
7
7
|
export declare function renderDocMarkdown(text: string, opts?: {
|
|
8
8
|
bakes?: ExampleBakes;
|
|
9
9
|
}): string;
|
|
@@ -154,10 +154,37 @@ docMarked.use({
|
|
|
154
154
|
],
|
|
155
155
|
});
|
|
156
156
|
/** Render a doc's markdown text to HTML (synchronous, default marked options). */
|
|
157
|
+
/*
|
|
158
|
+
Markdown wraps a lone CUSTOM ELEMENT in a paragraph, and that paragraph is the bug.
|
|
159
|
+
|
|
160
|
+
marked classifies raw HTML by tag name, and it cannot know whether an unknown tag is block or
|
|
161
|
+
inline — so `<my-thing></my-thing>` alone on a line comes out as `<p><my-thing></my-thing></p>`.
|
|
162
|
+
The paragraph is auto-height, so a page whose whole content is one embedded app gets an element
|
|
163
|
+
whose `height: 100%` resolves against a 33px box (tosijs-ui#115, measured: `p` 33px inside a
|
|
164
|
+
842px `.doc-content`). `layout: "full-screen"` exists precisely for that page, and the promise it
|
|
165
|
+
makes was not being kept.
|
|
166
|
+
|
|
167
|
+
The unwrap is deliberately NARROW, because a heuristic here earns its own bugs. It fires only
|
|
168
|
+
when a paragraph's entire content is a single element whose tag name contains a hyphen — i.e. a
|
|
169
|
+
custom element, the one case marked provably cannot classify. `<p><img></p>` and
|
|
170
|
+
`<p><em>text</em></p>` are left exactly alone: those are real paragraphs, and a `<p>` around them
|
|
171
|
+
is what the author asked for.
|
|
172
|
+
*/
|
|
173
|
+
const LONE_CUSTOM_ELEMENT = /<p>\s*(<([a-z][a-z0-9]*-[a-z0-9-]*)\b[\s\S]*?<\/\2>)\s*<\/p>/gi;
|
|
174
|
+
export function unwrapLoneCustomElements(html) {
|
|
175
|
+
return html.replace(LONE_CUSTOM_ELEMENT, (whole, inner) => {
|
|
176
|
+
/*
|
|
177
|
+
Only when the element is the paragraph's WHOLE content. The regex already anchors both ends,
|
|
178
|
+
but a nested same-tag structure could let the lazy match close early and leave a tail — so
|
|
179
|
+
the result is checked for a stray closing tag rather than trusted.
|
|
180
|
+
*/
|
|
181
|
+
return inner.includes('</p>') ? whole : inner;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
157
184
|
export function renderDocMarkdown(text, opts = {}) {
|
|
158
185
|
currentBakes = opts.bakes;
|
|
159
186
|
try {
|
|
160
|
-
return docMarked.parse(text);
|
|
187
|
+
return unwrapLoneCustomElements(docMarked.parse(text));
|
|
161
188
|
}
|
|
162
189
|
finally {
|
|
163
190
|
currentBakes = undefined;
|
|
@@ -31,6 +31,22 @@ export declare function lockDecision(existing: LockHolder | null, self: {
|
|
|
31
31
|
/** The message the refused build prints. Names the holder, and what to do about it. */
|
|
32
32
|
export declare function describeHolder(holder: LockHolder): string;
|
|
33
33
|
export declare function isProcessAlive(pid: number): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Who, if anyone, holds this project's lock right now — or `null` if nobody live does.
|
|
36
|
+
*
|
|
37
|
+
* Exported so a `--stop` can target THIS project's dev server by pid instead of by argv
|
|
38
|
+
* pattern. `pkill -f 'bun bin/site.ts'` matches every dev server on the machine, because every
|
|
39
|
+
* project on this pipeline runs an identical command line — so a sibling checkout, a worktree
|
|
40
|
+
* or another agent's session dies to a command that reads as "restart mine" (tosijs-ui#117).
|
|
41
|
+
* Observed five times in one working day, twice while a tunnel link was in use by a remote
|
|
42
|
+
* reviewer.
|
|
43
|
+
*
|
|
44
|
+
* The record already existed for the build lock; only a reader and a command were missing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function currentHolder(root?: string, opts?: {
|
|
47
|
+
dir?: string;
|
|
48
|
+
isAlive?: (pid: number) => boolean;
|
|
49
|
+
}): LockHolder | null;
|
|
34
50
|
export interface AcquiredLock {
|
|
35
51
|
ok: boolean;
|
|
36
52
|
holder?: LockHolder;
|