tosijs-ui 1.12.1 → 1.12.3
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 +257 -0
- package/README.md +1 -1
- package/bin/release-notes.ts +9 -2
- package/dist/data-table.js +33 -6
- package/dist/doc-browser.d.ts +2 -0
- package/dist/doc-browser.js +121 -3
- package/dist/doc-system/doc-system-styles.js +56 -1
- package/dist/doc-system/release-notes.js +17 -2
- package/dist/doc-system/site/dev-auth.d.ts +17 -0
- package/dist/doc-system/site/dev-auth.js +15 -0
- package/dist/doc-system/site/dev-server.d.ts +22 -0
- package/dist/doc-system/site/dev-server.js +236 -5
- package/dist/doc-system/site/docs.d.ts +19 -0
- package/dist/doc-system/site/docs.js +11 -0
- package/dist/doc-system/site/generate-site.js +12 -1
- package/dist/doc-system/site/make-llms-txt.d.ts +5 -1
- package/dist/doc-system/site/site-config.d.ts +19 -1
- package/dist/editable-rect.js +11 -4
- package/dist/float.js +8 -1
- package/dist/iife.js +79 -79
- package/dist/iife.js.map +11 -11
- package/dist/side-nav.d.ts +11 -0
- package/dist/side-nav.js +65 -1
- package/dist/sizer.js +8 -1
- package/dist/track-drag.js +51 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,262 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.3
|
|
4
|
+
|
|
5
|
+
### Back now has layout coverage, because a reader noticed before the tests did
|
|
6
|
+
|
|
7
|
+
Reported from the keyboard as "sometimes the full-screen page and the full-screen page with the
|
|
8
|
+
nav open look like different history entries". Measured, it is neither history nor
|
|
9
|
+
inconsistent: the nav toggle creates **no** history entry (`history.length` unchanged across
|
|
10
|
+
repeated toggles, URL unchanged). What happens is that **Back re-asserts the page's declared
|
|
11
|
+
layout** and discards the override — so leaving a page with the nav open and returning finds it
|
|
12
|
+
closed.
|
|
13
|
+
|
|
14
|
+
That is defensible and it is what ships: a full-screen page is full-screen whenever you arrive
|
|
15
|
+
at it. Whether Back should instead restore what you left is now an open decision in `TODO.md`,
|
|
16
|
+
with the measurements attached, rather than whatever fell out of the implementation.
|
|
17
|
+
|
|
18
|
+
The gap the report exposed was in the tests, not the feature: `doc-system.pw.ts` already covered
|
|
19
|
+
Back for URL, title and content, but nothing checked its effect on **layout**, and every layout
|
|
20
|
+
test reached its pages by clicking links. Both directions are covered now — and labelled by what
|
|
21
|
+
they actually do, since one is a genuine regression test (dropping `removeAttribute('data-layout')`
|
|
22
|
+
on navigation fails it) and the other is a contract tripwire that survives every mutation tried
|
|
23
|
+
against it. A test that cannot fail is usually decoration; that one is deliberate, and saying so
|
|
24
|
+
is the difference.
|
|
25
|
+
|
|
26
|
+
### The dev server sends `Cache-Control: no-store` on everything
|
|
27
|
+
|
|
28
|
+
It previously sent no `Cache-Control` at all, and that is not neutral: a browser may invent a
|
|
29
|
+
freshness lifetime when you decline to state one, and Safari is the most willing to. So the dev
|
|
30
|
+
server would rebuild correctly while the browser kept serving the previous build — which
|
|
31
|
+
presents as "my fix did not work", and is only escaped by emptying the cache by hand. It cost
|
|
32
|
+
exactly that here: a bug was reported against code that had already been fixed.
|
|
33
|
+
|
|
34
|
+
All three of the file-server's exits are covered — injected HTML, compressed assets, and
|
|
35
|
+
binaries that stream untouched — because they are separate responses and adding the header to
|
|
36
|
+
the obvious one is precisely how the other two get missed. Each is independently
|
|
37
|
+
mutation-verified.
|
|
38
|
+
|
|
39
|
+
**Dev server only.** The built site in `docs/` is unaffected; it is a static site for a real
|
|
40
|
+
host, and telling a CDN never to store it would be actively wrong.
|
|
41
|
+
|
|
42
|
+
### `layout: "full-screen"` — and `<tosi-sidenav alwaysCompact>` underneath it
|
|
43
|
+
|
|
44
|
+
`full-width` shipped in 1.12.2 and `full-screen` did not, because it needed something from the
|
|
45
|
+
sidenav that did not exist. It exists now, and it is one line rather than a new layout.
|
|
46
|
+
|
|
47
|
+
**`<tosi-sidenav>` gained `navVisible`** — the one control a "show me the navigation" button
|
|
48
|
+
needs. Read it, flip it:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
sidenav.navVisible = !sidenav.navVisible
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It resolves what hiding the nav _means_ at the current width — forcing compact on a wide screen,
|
|
55
|
+
simply showing the content on a narrow one — so no caller has to know, and a narrow-screen
|
|
56
|
+
request does not outstay itself when the window is widened. `layout: "full-screen"` uses the
|
|
57
|
+
same property, so the feature is general rather than something the doc-browser knows a trick
|
|
58
|
+
about.
|
|
59
|
+
|
|
60
|
+
**And `alwaysCompact` underneath it.** Compact mode already shows the nav _or_ the content
|
|
61
|
+
and takes turns between them — which is precisely what a full-screen page wants — so this just
|
|
62
|
+
removes the width test rather than adding a second way to lay out. It is a named state and not a
|
|
63
|
+
`minSize` no viewport can reach, because the second one works and reads as a bug to the next
|
|
64
|
+
person.
|
|
65
|
+
|
|
66
|
+
**`layout: "full-screen"`** then means: no reading measure, no nav, no gutter, and a content area
|
|
67
|
+
with a real height — so a demo, a canvas or an embedded app can ask for `height: 100%` and get
|
|
68
|
+
it. There is a [demo page](/full-screen-demo/), and the navigation button works there: it
|
|
69
|
+
returns you to the normal layout rather than replacing the content with a full-screen nav, and
|
|
70
|
+
it **stays offered** so you can go back. Riding `compact` directly, as it used to, meant the
|
|
71
|
+
button vanished on its own click and stranded you in the normal layout.
|
|
72
|
+
|
|
73
|
+
Two inline styles were quietly breaking the promise. `.doc-content` receives both its
|
|
74
|
+
`max-width` and its `padding` inline from the doc-browser, so no stylesheet could override
|
|
75
|
+
either — a full-screen page rendered inset by 32px, in a box that collapsed to its content
|
|
76
|
+
height instead of filling. `padding` now routes through `--doc-content-padding`, the way the
|
|
77
|
+
measure already did.
|
|
78
|
+
|
|
79
|
+
Navigating away restores the nav, and a reader's override lasts until they navigate rather than
|
|
80
|
+
beyond it — **including Back**, so a full-screen page is full-screen whenever you arrive at it.
|
|
81
|
+
That is predictable, but it does mean leaving a page with the nav open and returning finds it
|
|
82
|
+
closed; whether Back should instead restore what you left is noted in `TODO.md` as a decision
|
|
83
|
+
rather than left to whatever fell out of the implementation. Both directions are mutation-verified, which caught a test that _looked_ like it
|
|
84
|
+
covered the second one and did not: it routed through a prose page, which reset the state as a
|
|
85
|
+
side effect.
|
|
86
|
+
|
|
87
|
+
Before hydration there is no sidenav at all, just static markup, so a stylesheet rule covers the
|
|
88
|
+
first paint. Those rules select `.doc-nav.doc-nav` — the class deliberately repeated — because
|
|
89
|
+
the pre-hydration layout further down the same sheet has identical specificity, and winning on
|
|
90
|
+
source order works right up until someone reorders the spec.
|
|
91
|
+
|
|
92
|
+
## 1.12.2
|
|
93
|
+
|
|
94
|
+
### `haltijaDev: 'tunnel'` — let an agent drive a page running in a headset (#104)
|
|
95
|
+
|
|
96
|
+
`haltijaDev: true` is localhost-gated, and that gate is exactly what stopped the feature
|
|
97
|
+
working where it is worth the most. Debugging in a VR headset is close to blind: no readable
|
|
98
|
+
console, no devtools, no way to inspect the DOM without taking it off — and taking it off is
|
|
99
|
+
the thing you cannot do, because the bug is usually _about_ being in it.
|
|
100
|
+
|
|
101
|
+
Set `haltijaDev: 'tunnel'` and the channel is also served **same-origin over the tunnel**.
|
|
102
|
+
Redeem an edit link on the device, load the page, and it shows up in `hj windows` like any
|
|
103
|
+
other — `hj eval`, `hj navigate`, `hj tree` all work against it, and `hj` never learns the
|
|
104
|
+
page is remote.
|
|
105
|
+
|
|
106
|
+
**Why it works at all, and why it needed no change from haltija.** The page **dials out**:
|
|
107
|
+
`component.js` reads `window.__haltija_config__.serverUrl` and falls back to localhost only
|
|
108
|
+
when that config is absent. So nothing has to reach _into_ a headset — the dev server sets
|
|
109
|
+
the config itself, serves the component from the page's own origin, and relays the WebSocket
|
|
110
|
+
to the local channel. The upstream `dev.js`/`inject.js` chain is skipped entirely; both files
|
|
111
|
+
carry their own localhost gates and all three of their hardcoded `https://localhost:8701`
|
|
112
|
+
URLs would resolve on the **headset** rather than on your machine.
|
|
113
|
+
|
|
114
|
+
**A separate opt-in, deliberately.** `true` means "an agent may drive the page on this
|
|
115
|
+
machine". Upgrading must never silently turn that into "wherever it is reachable", so
|
|
116
|
+
`'tunnel'` is its own value — and `HALTIJA_DEV=1` does **not** enable it, because that
|
|
117
|
+
variable is a convenience toggle and this is not a convenience.
|
|
118
|
+
|
|
119
|
+
**What authorizes it:** a live dev session cookie — the same credential that gates source
|
|
120
|
+
writes, via the same predicate (`mayDriveWithAgent` delegates to `mayWriteSource`, and a test
|
|
121
|
+
asserts they agree on every input). Driving a page with an agent is at least as powerful as
|
|
122
|
+
writing source, so it gets no weaker a gate. What authorizes is the **listener**, never the
|
|
123
|
+
peer address: a reverse tunnel counterfeits "local" by construction. Unauthorized requests
|
|
124
|
+
get **404**, not 403 — whether a project enabled the bridge is nobody's business but the
|
|
125
|
+
session holder's.
|
|
126
|
+
|
|
127
|
+
Verified end to end rather than by inspection: a real browser on the tunnel origin stores the
|
|
128
|
+
session, receives the loader, resolves `serverUrl` to its own origin (not `localhost:8701`),
|
|
129
|
+
attaches the widget, opens the relayed socket, appears in `hj windows`, and answers `hj eval`
|
|
130
|
+
and `hj navigate`.
|
|
131
|
+
|
|
132
|
+
### Dragging works on Firefox now — every draggable, not just table columns ([#107](https://github.com/tonioloewald/tosijs-ui/issues/107))
|
|
133
|
+
|
|
134
|
+
**The first drag on a freshly-loaded page did not work in Firefox.** The thing you grabbed
|
|
135
|
+
stayed put and random text highlighted instead; it only started behaving once you had clicked
|
|
136
|
+
somewhere — anywhere — to settle Firefox's selection state. Land on a page and drag a column
|
|
137
|
+
edge, which is an entirely ordinary thing to do, and the first attempt failed.
|
|
138
|
+
|
|
139
|
+
This affected **every** `trackDrag` consumer — `<tosi-sizer>`, `<tosi-float>`,
|
|
140
|
+
`<tosi-editable-rect>` and `<tosi-table>` column resizing — and had done since the drag tracker
|
|
141
|
+
landed in **v0.5.1**. Measured with no prior click: `<tosi-sizer>` did not move at all, a table
|
|
142
|
+
column applied one 12px step and froze. All three engines now behave identically (150 → 210 →
|
|
143
|
+
270 on the same gesture, and nothing selected).
|
|
144
|
+
|
|
145
|
+
Two parts, because either alone does nothing. `trackDrag` calls `preventDefault()` on the
|
|
146
|
+
initiating mousedown, which is narrow rather than blunt — it only ever runs once a component has
|
|
147
|
+
already decided a drag is starting, so ordinary clicks, focus and selection are untouched. And
|
|
148
|
+
`<tosi-sizer>`, `<tosi-float>` and `<tosi-editable-rect>` had to stop registering their
|
|
149
|
+
**mousedown** listeners as `passive`, because a passive listener makes `preventDefault` a silent
|
|
150
|
+
no-op — which is exactly why the table (whose listener was never passive) improved first while
|
|
151
|
+
the others stayed dead. Nothing is lost: `passive`'s practical value is silencing the console
|
|
152
|
+
warning about handlers that delay scrolling, and that warning only ever applied to touch and
|
|
153
|
+
wheel. `touchstart` stays passive.
|
|
154
|
+
|
|
155
|
+
It is Firefox behaving badly, and that is a diagnosis rather than an excuse — the workaround is
|
|
156
|
+
cheap and users do not care whose bug it is.
|
|
157
|
+
|
|
158
|
+
### Page metadata: `layout: full-width`, so a route can leave the reading column ([#105](https://github.com/tonioloewald/tosijs-ui/issues/105))
|
|
159
|
+
|
|
160
|
+
Prose wants a measure — 44em is roughly what people read comfortably, and it stays the default.
|
|
161
|
+
But a demo, a dashboard, a wide table or a canvas is _worse_ squeezed into a column, and having
|
|
162
|
+
to pick one habit for the whole site is what makes people build a second site. Now a page can
|
|
163
|
+
opt out, the same way it sets `pin` or `order`:
|
|
164
|
+
|
|
165
|
+
```markdown
|
|
166
|
+
<!--{ "layout": "full-width" }-->
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
or `layout: full-width` in YAML frontmatter. **This project's own README uses it.**
|
|
170
|
+
|
|
171
|
+
Stamped into the served HTML rather than applied on load, so the first paint is already right —
|
|
172
|
+
a layout applied by script would show the reading column and snap wide, the flicker
|
|
173
|
+
pre-rendering exists to prevent. Client-side navigation keeps it in step **in both directions**;
|
|
174
|
+
that second direction is the one a naive implementation forgets, and it has its own test
|
|
175
|
+
(mutation-verified: dropping the clear-on-navigate fails it, dropping the static stamp fails
|
|
176
|
+
three more including first paint).
|
|
177
|
+
|
|
178
|
+
**`layout: "full-screen"` is not implemented and is rejected with a message rather than
|
|
179
|
+
half-honoured.** It needs a collapsed state on `<tosi-side-nav>`: after hydration the nav
|
|
180
|
+
carries an inline `display`, and side-nav sets its width as an inline property, so a stylesheet
|
|
181
|
+
cannot override either without `!important` — and hiding side-nav itself hides the content too,
|
|
182
|
+
since it wraps both. A value that dropped the measure while leaving the chrome exactly where it
|
|
183
|
+
was would be reported as a bug, correctly. Tracked in `TODO.md`.
|
|
184
|
+
|
|
185
|
+
### `<tosi-table>`: a column drag no longer dies when `columns` is reassigned under it
|
|
186
|
+
|
|
187
|
+
The other half of the reporting app's "resize funkiness", and a different defect from #102's
|
|
188
|
+
torn grid — this one is not a mismatch at all. `resizeColumn` captured the `ColumnOptions`
|
|
189
|
+
object and mutated it for the whole drag, and that object stops being the table's the instant a
|
|
190
|
+
caller assigns a new `columns` array. Which the app does on a page-size change, and a page-size
|
|
191
|
+
change is a very likely thing to happen while someone is dragging a column edge. From then on
|
|
192
|
+
the drag wrote widths into an orphan: pointer moving, column not moving, nothing reported.
|
|
193
|
+
|
|
194
|
+
The drag now re-resolves the column by `prop` each step — ending cleanly if it is gone, and
|
|
195
|
+
rebasing on the new width if the object was merely replaced. Confirmed pre-existing rather than
|
|
196
|
+
introduced by 1.12.1: reverting that fix leaves the drag equally dead.
|
|
197
|
+
|
|
198
|
+
### Known: column resize does not work on Firefox at all ([#107](https://github.com/tonioloewald/tosijs-ui/issues/107))
|
|
199
|
+
|
|
200
|
+
Found while investigating the above, and it is the bigger problem. On Firefox a **plain** column
|
|
201
|
+
drag — no reassignment anywhere near it — applies one step and freezes: +60px reaches 162 and
|
|
202
|
+
+120px still reads 162, against 210 then 270 on chromium and webkit. A document-level capture
|
|
203
|
+
listener shows Firefox stops delivering `mousemove` to the page entirely after the first event,
|
|
204
|
+
so no callback can run.
|
|
205
|
+
|
|
206
|
+
The cause looks to be `trackDrag` listening on a full-screen overlay while Firefox keeps
|
|
207
|
+
implicit mouse capture on the element that received the `mousedown`. The fix is Pointer Events
|
|
208
|
+
with explicit capture, and `trackDrag` is shared by every draggable affordance — so it wants
|
|
209
|
+
doing deliberately, with the others re-tested on Firefox at the same time, rather than as a
|
|
210
|
+
footnote to this release. Ruled out first: it is not the DOM being rebuilt under the drag
|
|
211
|
+
(deferring renders changes nothing) and not #102 (reverting that changes nothing).
|
|
212
|
+
|
|
213
|
+
### `tosijs-release-notes`: the bump gate stops blocking on dev tooling
|
|
214
|
+
|
|
215
|
+
Two fixes, both hit while cutting this release — which is the only way anyone finds them.
|
|
216
|
+
|
|
217
|
+
**A dev-tooling path with a security surface now WARNS instead of blocking a patch.** Every
|
|
218
|
+
path on that list is development tooling — a dev server, a tunnel, a deploy script — and none
|
|
219
|
+
of it runs in an end user's browser as part of an adopter's app. Halting a release over it
|
|
220
|
+
spends the maintainer's attention on the release with the least at stake, and a guard that
|
|
221
|
+
cries wolf gets overridden or deleted, taking the case it was _right_ about with it (#79, a
|
|
222
|
+
loosened dev-server default shipped as a patch). The warning keeps that signal and stops
|
|
223
|
+
charging for it. A gate that genuinely should block would have to key on code an adopter
|
|
224
|
+
**ships**, which none of these are.
|
|
225
|
+
|
|
226
|
+
**The blocking branch offered advice it could not honour.** It said "explain in the notes why
|
|
227
|
+
this does not reach anyone" — but `bumpConcerns` never sees the notes, so nothing you wrote
|
|
228
|
+
could satisfy it, and the only way forward was to work that out by reading the tool's own
|
|
229
|
+
source. Same defect this project already fixed in the tunnel's 401 page. It now says "cut a
|
|
230
|
+
minor", which is the only thing that ever worked.
|
|
231
|
+
|
|
232
|
+
### Security-relevant paths that changed, and why this is still a patch
|
|
233
|
+
|
|
234
|
+
`dev-auth.ts` — the module that decides who may do privileged things — changed in this
|
|
235
|
+
release, and a patch touching it deserves a heading rather than a footnote.
|
|
236
|
+
|
|
237
|
+
**The change is purely additive, and that is checkable rather than asserted.** The diff against
|
|
238
|
+
1.12.1 removes **zero lines**, and `mayWriteSource` is **byte-identical** to 1.12.1. What was
|
|
239
|
+
added is one new exported function, `mayDriveWithAgent`, whose entire body delegates to
|
|
240
|
+
`mayWriteSource` — with a test asserting the two agree on every combination of `viaTunnel`,
|
|
241
|
+
`hasValidSession` and peer address, so they cannot drift apart later without that test going
|
|
242
|
+
red.
|
|
243
|
+
|
|
244
|
+
**Nothing reaches an existing adopter.** The new predicate is consulted only by the
|
|
245
|
+
`/__haltija/` routes, which exist only when `haltijaDev: 'tunnel'` is set. Every existing
|
|
246
|
+
authorization decision — source writes, link minting, session validation — is executed by the
|
|
247
|
+
same bytes as in 1.12.1. If you have not opted in, the reachable behaviour of this release is
|
|
248
|
+
identical to 1.12.1.
|
|
249
|
+
|
|
250
|
+
**What it would mean if you do opt in:** a live dev session cookie becomes sufficient to let an
|
|
251
|
+
agent drive your page over the tunnel, in addition to editing source. That is a real widening
|
|
252
|
+
of what a session can do, which is why it is a distinct config value and not implied by
|
|
253
|
+
`haltijaDev: true`.
|
|
254
|
+
|
|
255
|
+
**A patch, not a minor**, despite reading like a feature: it is a new optional _value_ on an
|
|
256
|
+
existing config field, plus routes that exist only under it. `haltijaDev: true` behaves exactly
|
|
257
|
+
as before and nothing is removed or renamed, so the contract a patch carries — it does not
|
|
258
|
+
break you — holds.
|
|
259
|
+
|
|
3
260
|
## 1.12.1
|
|
4
261
|
|
|
5
262
|
### The Playwright lane no longer fights itself
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tosijs-ui
|
|
2
2
|
|
|
3
|
-
<!--{ "pin": "top", "headTitle": "tosijs-ui — robust, standards-based web components", "description": "A small, fast library of standards-based web components (data tables, dialogs, forms, rich text, carousels, and more) built on tosijs — smaller and faster than a React app, with no virtual DOM.", "keywords": "web components, custom elements, tosijs, data table, dialog, form, rich text, no virtual dom" }-->
|
|
3
|
+
<!--{ "pin": "top", "layout": "full-width", "headTitle": "tosijs-ui — robust, standards-based web components", "description": "A small, fast library of standards-based web components (data tables, dialogs, forms, rich text, carousels, and more) built on tosijs — smaller and faster than a React app, with no virtual DOM.", "keywords": "web components, custom elements, tosijs, data table, dialog, form, rich text, no virtual dom" }-->
|
|
4
4
|
|
|
5
5
|
[ui.tosijs.net live demo](https://ui.tosijs.net) | [tosijs](https://tosijs.net) | [discord](https://discord.gg/ramJ9rgky5) | [github](https://github.com/tonioloewald/tosijs-ui#readme) | [npm](https://www.npmjs.com/package/tosijs-ui)
|
|
6
6
|
|
package/bin/release-notes.ts
CHANGED
|
@@ -159,9 +159,16 @@ if (has('check')) {
|
|
|
159
159
|
)
|
|
160
160
|
for (const e of c.evidence) console.error(` ${e}`)
|
|
161
161
|
console.error(
|
|
162
|
+
/*
|
|
163
|
+
Advice you can actually follow. The blocking branch used to offer "explain in the notes
|
|
164
|
+
why this does not reach anyone" — which `bumpConcerns` cannot honour, because it never
|
|
165
|
+
sees the notes. A gate whose stated remedy is unrunnable leaves you re-reading your own
|
|
166
|
+
tool to find out what it wants, which is the same defect this repo fixed in the tunnel's
|
|
167
|
+
401 page.
|
|
168
|
+
*/
|
|
162
169
|
blocking
|
|
163
|
-
? `\n Cut a minor
|
|
164
|
-
: `\n Check it is genuinely additive.\n`
|
|
170
|
+
? `\n Cut a minor — this is not something the notes can talk you out of.\n`
|
|
171
|
+
: `\n Check it is genuinely additive, and say what moved in the notes.\n`
|
|
165
172
|
)
|
|
166
173
|
}
|
|
167
174
|
|
package/dist/data-table.js
CHANGED
|
@@ -2149,9 +2149,29 @@ export class TosiTable extends WebComponent {
|
|
|
2149
2149
|
this.style.cursor = column !== undefined ? 'col-resize' : '';
|
|
2150
2150
|
};
|
|
2151
2151
|
resizeColumn = (event) => {
|
|
2152
|
-
const
|
|
2153
|
-
if (
|
|
2154
|
-
|
|
2152
|
+
const grabbed = this.getColumn(event);
|
|
2153
|
+
if (grabbed !== undefined) {
|
|
2154
|
+
/*
|
|
2155
|
+
The drag follows the column BY PROP, not by holding the object it started on.
|
|
2156
|
+
|
|
2157
|
+
A drag used to capture the `ColumnOptions` object and mutate it for its whole life. That
|
|
2158
|
+
object stops being the table's the moment a caller assigns a new `columns` array — which
|
|
2159
|
+
the reporting app does on a page-size change, and a page-size change is a very likely
|
|
2160
|
+
thing to happen while someone is dragging a column edge. From then on the drag wrote
|
|
2161
|
+
widths into an orphan: pointer moving, column not moving, nothing reported anywhere.
|
|
2162
|
+
|
|
2163
|
+
Each step re-resolves from the current set instead. If the column is gone the drag ends
|
|
2164
|
+
(its premise went with it); if the object was replaced but the prop survives, the drag
|
|
2165
|
+
continues, rebased on the new width — `origWidth + dx` against a width the caller has
|
|
2166
|
+
since changed would make the column jump by the difference.
|
|
2167
|
+
|
|
2168
|
+
(Firefox needed a separate fix before any of this was observable there — it turned the
|
|
2169
|
+
first drag on a fresh page into a text selection. See track-drag.ts, and #107.)
|
|
2170
|
+
*/
|
|
2171
|
+
const prop = grabbed.prop;
|
|
2172
|
+
let anchor = grabbed;
|
|
2173
|
+
let anchorWidth = Number(grabbed.width);
|
|
2174
|
+
let anchorDx = 0;
|
|
2155
2175
|
const isTouchEvent = event.touches !== undefined;
|
|
2156
2176
|
const touchIdentifier = isTouchEvent
|
|
2157
2177
|
? event.touches[0].identifier
|
|
@@ -2163,9 +2183,16 @@ export class TosiTable extends WebComponent {
|
|
|
2163
2183
|
if (touch === undefined) {
|
|
2164
2184
|
return true;
|
|
2165
2185
|
}
|
|
2166
|
-
const
|
|
2167
|
-
|
|
2168
|
-
|
|
2186
|
+
const live = this.columns.find((c) => c.prop === prop);
|
|
2187
|
+
if (!live)
|
|
2188
|
+
return true;
|
|
2189
|
+
if (live !== anchor) {
|
|
2190
|
+
anchor = live;
|
|
2191
|
+
anchorWidth = Number(live.width);
|
|
2192
|
+
anchorDx = dx;
|
|
2193
|
+
}
|
|
2194
|
+
const width = anchorWidth + (dx - anchorDx);
|
|
2195
|
+
live.width = width > this.minColumnWidth ? width : this.minColumnWidth;
|
|
2169
2196
|
this.setColumnWidths();
|
|
2170
2197
|
if (event.type === 'mouseup') {
|
|
2171
2198
|
return true;
|
package/dist/doc-browser.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface Doc {
|
|
|
22
22
|
path: string;
|
|
23
23
|
pin?: string;
|
|
24
24
|
hidden?: boolean;
|
|
25
|
+
/** `'full-width'` drops the reading measure; `'full-screen'` also puts the nav away. */
|
|
26
|
+
layout?: 'full-width' | 'full-screen';
|
|
25
27
|
testStatus?: 'passed' | 'failed' | 'pending';
|
|
26
28
|
bakes?: Array<[string, {
|
|
27
29
|
dialect: string;
|
package/dist/doc-browser.js
CHANGED
|
@@ -422,6 +422,16 @@ export function createDocBrowser(options) {
|
|
|
422
422
|
docs,
|
|
423
423
|
currentDoc,
|
|
424
424
|
compact: false,
|
|
425
|
+
/*
|
|
426
|
+
Whether to OFFER the navigation button — deliberately not the same thing as `compact`.
|
|
427
|
+
|
|
428
|
+
The button used to ride `compact` directly, which is right for a responsive page: wide
|
|
429
|
+
enough to show the nav beside the content, and there is nothing to toggle. But opening the
|
|
430
|
+
nav on a full-screen page leaves compact mode by design, and the button would vanish with
|
|
431
|
+
it — stranding the reader in the normal layout with no way back to full screen. So it is
|
|
432
|
+
offered whenever the layout is compact OR the page asked to be full-screen.
|
|
433
|
+
*/
|
|
434
|
+
navToggle: false,
|
|
425
435
|
},
|
|
426
436
|
});
|
|
427
437
|
const app = state[stateKey];
|
|
@@ -576,13 +586,58 @@ export function createDocBrowser(options) {
|
|
|
576
586
|
// sidenav')`, which returns the FIRST sidenav in the document (an OUTER instance
|
|
577
587
|
// when this one is an embedded, memory-routed browser).
|
|
578
588
|
let sidenav = null;
|
|
589
|
+
/*
|
|
590
|
+
`full-screen` uses the sidenav's OWN compact behaviour rather than a new state.
|
|
591
|
+
|
|
592
|
+
`<tosi-sidenav>` already does exactly what full-screen wants: compact mode shows the nav OR the
|
|
593
|
+
content, with `contentVisible` picking which. `alwaysCompact` keeps it in that mode at any
|
|
594
|
+
width, and `contentVisible` gives the page the whole box.
|
|
595
|
+
|
|
596
|
+
Remembered rather than applied directly, because the sidenav is not there yet when the first
|
|
597
|
+
doc renders — `sidenav` is captured from its own change event, which fires on a state CHANGE
|
|
598
|
+
and so may never fire at all on a normal-width page.
|
|
599
|
+
*/
|
|
600
|
+
let wantsFullScreen = false;
|
|
601
|
+
/*
|
|
602
|
+
What we last PUSHED onto the sidenav, so the user's own toggle is not overwritten.
|
|
603
|
+
|
|
604
|
+
The first version applied the layout on every call, and `applyFullScreen` is called from the
|
|
605
|
+
sidenav's change handler — so clicking "navigation" flipped `contentVisible`, the sidenav
|
|
606
|
+
changed state, the handler fired, and the layout was re-applied on top. The nav button looked
|
|
607
|
+
live and did nothing. Applying only on TRANSITION means a full-screen page sets itself up once
|
|
608
|
+
and then leaves the reader alone.
|
|
609
|
+
*/
|
|
610
|
+
let appliedFullScreen = null;
|
|
611
|
+
/** Offer the button when there is something to toggle: a compact layout, or a full-screen page. */
|
|
612
|
+
const refreshNavToggle = () => {
|
|
613
|
+
app.navToggle = (Boolean(sidenav?.compact) || wantsFullScreen);
|
|
614
|
+
};
|
|
615
|
+
const applyFullScreen = () => {
|
|
616
|
+
/*
|
|
617
|
+
Resolved from THIS browser's own container when the change event has not introduced it yet.
|
|
618
|
+
Scoped rather than `document.querySelector('tosi-sidenav')`, which returns the first in the
|
|
619
|
+
document — for a nested doc-browser, the HOST's.
|
|
620
|
+
*/
|
|
621
|
+
if (!sidenav && typeof container !== 'undefined' && container) {
|
|
622
|
+
sidenav = container.querySelector(TosiSidenav.tagName);
|
|
623
|
+
}
|
|
624
|
+
if (!sidenav)
|
|
625
|
+
return;
|
|
626
|
+
if (appliedFullScreen === wantsFullScreen)
|
|
627
|
+
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;
|
|
631
|
+
appliedFullScreen = wantsFullScreen;
|
|
632
|
+
refreshNavToggle();
|
|
633
|
+
};
|
|
579
634
|
const headerContent = [
|
|
580
635
|
button({
|
|
581
636
|
class: 'iconic',
|
|
582
637
|
style: { color: vars.linkColor },
|
|
583
638
|
title: 'navigation',
|
|
584
639
|
bind: {
|
|
585
|
-
value: app.
|
|
640
|
+
value: app.navToggle,
|
|
586
641
|
binding: {
|
|
587
642
|
toDOM(element, compact) {
|
|
588
643
|
element.style.display = compact ? '' : 'none';
|
|
@@ -593,8 +648,10 @@ export function createDocBrowser(options) {
|
|
|
593
648
|
},
|
|
594
649
|
},
|
|
595
650
|
onClick() {
|
|
651
|
+
// Read it, flip it. `<tosi-sidenav>` resolves what that means at this width; a
|
|
652
|
+
// button should not have to know, and this one used to.
|
|
596
653
|
if (sidenav)
|
|
597
|
-
sidenav.
|
|
654
|
+
sidenav.navVisible = !sidenav.navVisible;
|
|
598
655
|
},
|
|
599
656
|
}, icons.menu()),
|
|
600
657
|
span({ style: { flex: '0 0 10px' } }),
|
|
@@ -651,7 +708,13 @@ export function createDocBrowser(options) {
|
|
|
651
708
|
// doc-browser (no doc-system stylesheet) working.
|
|
652
709
|
maxWidth: 'var(--doc-content-max-width, 44em)',
|
|
653
710
|
margin: 'auto',
|
|
654
|
-
|
|
711
|
+
/*
|
|
712
|
+
Through a variable, for the same reason `maxWidth` is: this is an INLINE style, so a
|
|
713
|
+
stylesheet cannot override it, and `layout: "full-screen"` needs to. A full-screen page that
|
|
714
|
+
keeps a 1em gutter is not full screen — it is a demo inset by 32px, which is exactly what it
|
|
715
|
+
looked like.
|
|
716
|
+
*/
|
|
717
|
+
padding: 'var(--doc-content-padding, 0 1em)',
|
|
655
718
|
overflow: 'hidden',
|
|
656
719
|
});
|
|
657
720
|
// Adoption is zero-flash hydration of a statically pre-rendered page: it only
|
|
@@ -711,6 +774,58 @@ export function createDocBrowser(options) {
|
|
|
711
774
|
bakes: doc.bakes ? new Map(doc.bakes) : undefined,
|
|
712
775
|
});
|
|
713
776
|
}
|
|
777
|
+
/*
|
|
778
|
+
Keep the page's layout in step with the doc on SPA navigation.
|
|
779
|
+
|
|
780
|
+
The static generator stamps `data-layout` so the first paint is already right without JS;
|
|
781
|
+
this is the other half — navigating from a full-width page to a prose one must put the
|
|
782
|
+
reading column back, and vice versa. Written on every showDoc rather than only on change,
|
|
783
|
+
because the adopted first page also arrives here and the attribute it was served with must
|
|
784
|
+
survive rather than be assumed.
|
|
785
|
+
*/
|
|
786
|
+
/*
|
|
787
|
+
On the SAME element the static generator stamps — `<tosi-doc-system>` — not on
|
|
788
|
+
`docContent.parentElement`.
|
|
789
|
+
|
|
790
|
+
Those are not the same node, which cost a debugging round: hydration wraps the adopted
|
|
791
|
+
`<article class="doc-content">` in a div, so the parent is that div while the generator's
|
|
792
|
+
attribute sits on the custom element above it. Writing to the div left the real host still
|
|
793
|
+
reading `full-width`, so navigating from the wide page to a prose one silently stayed wide —
|
|
794
|
+
and the test that only checked the wide direction would have passed.
|
|
795
|
+
|
|
796
|
+
A memory-routed browser is EXCLUDED deliberately. That mode exists so an embedded or nested
|
|
797
|
+
browser cannot reach out and change the page hosting it, and `closest()` from inside one
|
|
798
|
+
finds the OUTER doc-system — the host's, not its own. Same reasoning as its history/location
|
|
799
|
+
ban; a nested doc-browser rewriting its host's layout is the same bug wearing a hat.
|
|
800
|
+
*/
|
|
801
|
+
if (!memoryRouting) {
|
|
802
|
+
const layoutHost = docContent.closest('tosi-doc-system');
|
|
803
|
+
if (layoutHost) {
|
|
804
|
+
if (doc.layout)
|
|
805
|
+
layoutHost.setAttribute('data-layout', doc.layout);
|
|
806
|
+
else
|
|
807
|
+
layoutHost.removeAttribute('data-layout');
|
|
808
|
+
}
|
|
809
|
+
/*
|
|
810
|
+
`full-screen` also collapses the nav, and that has to go through the COMPONENT rather than
|
|
811
|
+
CSS: `<tosi-sidenav>` writes its column widths as inline properties, so a stylesheet has
|
|
812
|
+
nothing to override. The pre-hydration rule in doc-system-styles.ts handles the static
|
|
813
|
+
markup, which has a plain `<nav>` and no sidenav yet; this handles the hydrated page. Two
|
|
814
|
+
rules for two genuinely different DOMs, not two fixes for one bug.
|
|
815
|
+
*/
|
|
816
|
+
wantsFullScreen = doc.layout === 'full-screen';
|
|
817
|
+
/*
|
|
818
|
+
Forced on every navigation. Without this, opening the nav on one full-screen page (which
|
|
819
|
+
turns `alwaysCompact` off) would leave the NEXT full-screen page not full-screen, because
|
|
820
|
+
the wish had not changed and the transition check would skip it. A reader's override is
|
|
821
|
+
meant to last until they navigate, not beyond it.
|
|
822
|
+
*/
|
|
823
|
+
appliedFullScreen = null;
|
|
824
|
+
applyFullScreen();
|
|
825
|
+
// applyFullScreen returns early when there is no sidenav yet; the button state still has
|
|
826
|
+
// to follow the doc, so this does not ride on that path succeeding.
|
|
827
|
+
refreshNavToggle();
|
|
828
|
+
}
|
|
714
829
|
rewriteContentLinks();
|
|
715
830
|
// Stamp each example with its source file (for the source↔doc map). doc.path
|
|
716
831
|
// is the extracted file (.md, or a source file with doc comments).
|
|
@@ -1216,6 +1331,9 @@ export function createDocBrowser(options) {
|
|
|
1216
1331
|
sidenav = event.target.closest(TosiSidenav.tagName);
|
|
1217
1332
|
if (sidenav)
|
|
1218
1333
|
app.compact = sidenav.compact;
|
|
1334
|
+
refreshNavToggle();
|
|
1335
|
+
// The sidenav has just introduced itself; give it the layout the doc asked for.
|
|
1336
|
+
applyFullScreen();
|
|
1219
1337
|
},
|
|
1220
1338
|
}, searchField, navContent, div({
|
|
1221
1339
|
style: {
|
|
@@ -98,6 +98,61 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
98
98
|
// below — if these two ever disagree, the page jumps on hydration.
|
|
99
99
|
_docContentMaxWidth: '44em',
|
|
100
100
|
},
|
|
101
|
+
/*
|
|
102
|
+
Per-page layout, driven by `layout:` in a doc's metadata (see `Doc.layout`).
|
|
103
|
+
|
|
104
|
+
Both are variable overrides rather than new layout rules, which is what keeps them cheap
|
|
105
|
+
and non-invasive: the measure is already `--doc-content-max-width`, so a page opts out by
|
|
106
|
+
changing one value. Nothing here runs for a corpus that never sets `layout`.
|
|
107
|
+
*/
|
|
108
|
+
'tosi-doc-system[data-layout="full-width"]': {
|
|
109
|
+
_docContentMaxWidth: 'none',
|
|
110
|
+
},
|
|
111
|
+
/*
|
|
112
|
+
`full-screen` — no measure, and no chrome.
|
|
113
|
+
|
|
114
|
+
These rules are for the PRE-HYDRATION page, whose markup is a plain `<nav class="doc-nav">`
|
|
115
|
+
with no inline styles, so hiding it here works and works on first paint. Once the bundle
|
|
116
|
+
loads, the nav becomes a slotted node inside `<tosi-sidenav>` carrying its own inline
|
|
117
|
+
`display`, and CSS can no longer touch it — the doc-browser sets `sidenav.navHidden`
|
|
118
|
+
instead. Different DOM before and after hydration, so genuinely two rules rather than a
|
|
119
|
+
belt-and-braces duplicate.
|
|
120
|
+
*/
|
|
121
|
+
'tosi-doc-system[data-layout="full-screen"]': {
|
|
122
|
+
_docContentMaxWidth: 'none',
|
|
123
|
+
},
|
|
124
|
+
'tosi-doc-system[data-layout="full-screen"] .doc-content': {
|
|
125
|
+
padding: '0',
|
|
126
|
+
/*
|
|
127
|
+
A definite height, so a child asking for `height: 100%` gets one.
|
|
128
|
+
|
|
129
|
+
"The content is the viewport" is the promise this layout makes, and a demo, a canvas or an
|
|
130
|
+
embedded app is the reason to use it — all of which want to FILL, not to sit in a tall
|
|
131
|
+
box. Without a height here `100%` on a child resolves against `auto` and collapses to its
|
|
132
|
+
content.
|
|
133
|
+
*/
|
|
134
|
+
height: '100%',
|
|
135
|
+
overflow: 'auto',
|
|
136
|
+
/*
|
|
137
|
+
The gutter goes too. `.doc-content`'s padding is an INLINE style set by the doc-browser,
|
|
138
|
+
so this is set through the variable it reads rather than declared here, where it would
|
|
139
|
+
simply lose.
|
|
140
|
+
*/
|
|
141
|
+
_docContentPadding: '0',
|
|
142
|
+
},
|
|
143
|
+
/*
|
|
144
|
+
`.doc-nav.doc-nav` — the class twice, on purpose.
|
|
145
|
+
|
|
146
|
+
`tosi-doc-system:not(:defined) .doc-nav` (the pre-hydration layout, further down this same
|
|
147
|
+
sheet) has IDENTICAL specificity to the single-class form, so which one wins is decided by
|
|
148
|
+
source order, and it sits later. Winning on position works until someone reorders the spec,
|
|
149
|
+
at which point the nav quietly comes back on full-screen pages with nothing to point at.
|
|
150
|
+
Repeating the class costs one selector token and settles it on specificity, wherever these
|
|
151
|
+
rules end up living.
|
|
152
|
+
*/
|
|
153
|
+
'tosi-doc-system[data-layout="full-screen"] > .doc-nav.doc-nav, tosi-doc-system[data-layout="full-screen"] > .doc-navbar.doc-navbar': {
|
|
154
|
+
display: 'none',
|
|
155
|
+
},
|
|
101
156
|
'.darkmode': {
|
|
102
157
|
...invertLuminance(colors),
|
|
103
158
|
/*
|
|
@@ -180,7 +235,7 @@ export function docSystemStyleSpec(theme = {}) {
|
|
|
180
235
|
display: 'block',
|
|
181
236
|
maxWidth: vars.docContentMaxWidth,
|
|
182
237
|
margin: 'auto',
|
|
183
|
-
padding: '0 1em',
|
|
238
|
+
padding: 'var(--doc-content-padding, 0 1em)',
|
|
184
239
|
// Must match doc-browser's inline style EXACTLY, `overflow` included: it
|
|
185
240
|
// establishes a block formatting context, which stops the <h1>'s top margin
|
|
186
241
|
// collapsing out of the box. Without it the static content sits 10px lower
|
|
@@ -221,6 +221,21 @@ Deliberately a small, named list rather than a clever heuristic: it has to be ob
|
|
|
221
221
|
release was flagged, and obvious how to add to it. Matched as substrings of repo-relative
|
|
222
222
|
paths.
|
|
223
223
|
*/
|
|
224
|
+
/*
|
|
225
|
+
Paths where a change is worth a second look before it ships as a patch.
|
|
226
|
+
|
|
227
|
+
These are all DEVELOPMENT tooling — a dev server, a tunnel, a deploy script. None of it runs in
|
|
228
|
+
an end user's browser as part of an adopter's app, which is why touching them WARNS rather than
|
|
229
|
+
blocks. The distinction is the point: a security-relevant change to code an adopter ships to
|
|
230
|
+
their users deserves a gate that stops the release, and a change to a tool a developer chooses
|
|
231
|
+
to run on their own machine deserves a sentence in the notes.
|
|
232
|
+
|
|
233
|
+
It blocked, briefly, and that was wrong in a way worth recording. A gate that halts a release
|
|
234
|
+
over dev-only tooling spends the maintainer's attention on the release with the least at stake,
|
|
235
|
+
and the reliable outcome of a guard that cries wolf is that it gets overridden or deleted —
|
|
236
|
+
taking the case it was RIGHT about (#79, a loosened dev-server default shipped as a patch) with
|
|
237
|
+
it. Warning keeps that signal and stops charging for it.
|
|
238
|
+
*/
|
|
224
239
|
export const SENSITIVE_PATHS = [
|
|
225
240
|
'dev-auth',
|
|
226
241
|
'build-lock',
|
|
@@ -264,8 +279,8 @@ export function bumpConcerns(opts) {
|
|
|
264
279
|
const sensitive = changedPaths.filter((p) => SENSITIVE_PATHS.some((s) => p.includes(s)));
|
|
265
280
|
if (sensitive.length) {
|
|
266
281
|
out.push({
|
|
267
|
-
level: '
|
|
268
|
-
reason: 'a
|
|
282
|
+
level: 'warn',
|
|
283
|
+
reason: 'a dev-tooling path with a security surface changed in a PATCH — say what moved, under its own heading, in a release people will read',
|
|
269
284
|
evidence: sensitive.slice(0, 8),
|
|
270
285
|
});
|
|
271
286
|
}
|
|
@@ -184,6 +184,23 @@ export declare function mayWriteSource(opts: {
|
|
|
184
184
|
/** does the request carry a live session cookie? */
|
|
185
185
|
hasValidSession: boolean;
|
|
186
186
|
}): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Who may attach the haltija dev channel — i.e. let an agent DRIVE this page.
|
|
189
|
+
*
|
|
190
|
+
* Deliberately the same rule as `mayWriteSource`, delegating rather than restating it, because
|
|
191
|
+
* driving a page with an agent is at least as powerful as writing source: an agent that can
|
|
192
|
+
* evaluate script in the page can read whatever the page can read and act as the logged-in
|
|
193
|
+
* user. A weaker gate here would quietly become the weakest link, and a *parallel* copy of the
|
|
194
|
+
* rule would be a second thing to keep in step — which is how the two drift.
|
|
195
|
+
*
|
|
196
|
+
* It is a separate NAME because the capability is separate: if the policies ever need to
|
|
197
|
+
* diverge, this is the one place to change, and every caller already says which it means.
|
|
198
|
+
*/
|
|
199
|
+
export declare function mayDriveWithAgent(opts: {
|
|
200
|
+
viaTunnel: boolean;
|
|
201
|
+
peer?: string | null;
|
|
202
|
+
hasValidSession: boolean;
|
|
203
|
+
}): boolean;
|
|
187
204
|
/**
|
|
188
205
|
* THE loopback test. dev-server re-exports this as `isLoopbackAddress`; it used to
|
|
189
206
|
* carry its own byte-identical copy.
|