studio-engine 0.64.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/app/views/layouts/_navbar.html.erb +5 -1
- data/app/views/layouts/studio/_head.html.erb +147 -53
- data/lib/studio/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8914b74430f105820bbc5a41f44d6c573ff88492bcd882534f769fa2c2a11608
|
|
4
|
+
data.tar.gz: f3d1ffbec8dcb09ef354d21ab3786d2d5b9b0590a665ac95a949e7282668a66b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d52952922f19f864f97fd09297d8711c246ba54415bd40d60682569cbdfe0d51ccf7fb7673e7d921a361ab3657b35871f4999aad0baa80374dd3098cd3c267b2
|
|
7
|
+
data.tar.gz: 82afd75d6a4e57bfbe3708b59c1c1e7eec188cf2bcfd5921e3eeb50e006090d1f0839e5fc280be8ee60c8d6dc999bb07bb148e7e2220b7d061559048e14b6225
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,69 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`data-pin` — the pinned stack publishes itself.** `--nav-h` / `--nav-bottom`
|
|
10
|
+
answer for one element. Everything else that pins has been re-deriving the
|
|
11
|
+
same geometry by hand: on `mcritchie-studio`'s `/deployments` the app strip
|
|
12
|
+
positions itself with `:style="{ top: offset + 'px' }"`, and the lane headers
|
|
13
|
+
compute *"site header height + strip height"* in Alpine behind **their own**
|
|
14
|
+
`ResizeObserver` on `.vt-pinned-header` — while `--nav-bottom`, which already
|
|
15
|
+
answers the first half, goes unused there. Same shape as the four copies of
|
|
16
|
+
the navbar collapse, one layer up.
|
|
17
|
+
|
|
18
|
+
Any element carrying `data-pin="<name>"` now publishes `--pin-<name>-h` and
|
|
19
|
+
`--pin-<name>-bottom`, from **one shared `ResizeObserver`**, through the same
|
|
20
|
+
rAF coalescer and the same no-op-write skip the header already uses — and one
|
|
21
|
+
measurement pass for the whole frame, every read taken before any write.
|
|
22
|
+
|
|
23
|
+
That last part was wrong in the first cut of this change and review caught it:
|
|
24
|
+
the frame published the header (writing two inherited properties on
|
|
25
|
+
`documentElement`) and *then* read the pins, so the reads-before-writes order
|
|
26
|
+
held inside each function and broke across them. Measured at 6× CPU throttle
|
|
27
|
+
over 175 frames, pin loop on vs off: frames past 20ms **62 vs 42**, p90 **34.2
|
|
28
|
+
vs 26.0ms**; at 10×, **84/180 vs 4/180**. The header is now a pin like any
|
|
29
|
+
other — measured once, writing `--nav-h` / `--nav-bottom` from that same
|
|
30
|
+
reading rather than a second one. A consumer composes layers in pure
|
|
31
|
+
CSS:
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
top: max(var(--pin-nav-bottom, 0px), var(--pin-apps-bottom, 0px));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**`max()` rather than a declared stacking order.** A hidden layer measures 0
|
|
38
|
+
and drops out, so nothing has to say which layer sits above which, and
|
|
39
|
+
reordering a partial cannot break the arithmetic.
|
|
40
|
+
|
|
41
|
+
**A host-owned header still publishes the legacy pair.** The registry is built
|
|
42
|
+
from `[data-pin]`, so a header that does not carry the attribute would never be
|
|
43
|
+
measured and `--nav-h` / `--nav-bottom` would never publish — and **both live
|
|
44
|
+
consumers own their header** (`turf-monster` `layouts/_navbar`,
|
|
45
|
+
`mcritchie-studio` `layouts/application`), neither carrying `data-pin`. Review
|
|
46
|
+
measured the gear drawer **82px** out of place and the contest board **114px**,
|
|
47
|
+
with neither tracking the collapse; both apps pin a two-segment `~>` under 1.0,
|
|
48
|
+
so it would have landed on their next bundle update with no floor bump. The
|
|
49
|
+
header is adopted as the legacy pin whether or not it asks to be.
|
|
50
|
+
|
|
51
|
+
**A departed layer has its properties removed.** `display:none` and `x-show`
|
|
52
|
+
keep the node, so it measures 0 and drops out of a consumer's `max()` on its
|
|
53
|
+
own; a REMOVED node cannot be measured by anything, and its last value would
|
|
54
|
+
stand forever — review measured a removed 300px strip holding the property at
|
|
55
|
+
300px against a real stack bottom of 160px. The registry remembers what it
|
|
56
|
+
published and clears whatever is no longer in the document.
|
|
57
|
+
|
|
58
|
+
The registry is **rebuilt from the document**, on attach and after
|
|
59
|
+
`turbo:before-stream-render` — a Turbo Stream replaces nodes without a
|
|
60
|
+
`turbo:load`, which is precisely the stale-node bug the board documents
|
|
61
|
+
against its own version.
|
|
62
|
+
|
|
63
|
+
`layouts/_navbar` carries `data-pin="nav"`, so every consuming app gets
|
|
64
|
+
`--pin-nav-bottom` for free. **`--nav-h` and `--nav-bottom` keep publishing
|
|
65
|
+
unchanged** — `studio/_sidebar_panel` and turf-monster's `scroll-margin-top`
|
|
66
|
+
read them, and `nav_offset_contract_test` still pins both to their exact
|
|
67
|
+
sources. This is additive.
|
|
68
|
+
|
|
69
|
+
|
|
7
70
|
### Changed
|
|
8
71
|
|
|
9
72
|
- **The navbar collapse primitive is broadcastable, rate-limited, and cheap per
|
|
@@ -46,7 +46,11 @@
|
|
|
46
46
|
so it cannot move content under the reader.
|
|
47
47
|
Preview keeps nav-shell (the calc()s have to resolve) but no x-data — the
|
|
48
48
|
/navbar review page drives --nav-p from its own Scrolled toggle. %>
|
|
49
|
-
|
|
49
|
+
<%# data-pin names this header as a layer of the pinned stack, so
|
|
50
|
+
layouts/studio/_head publishes --pin-nav-h and --pin-nav-bottom for it and
|
|
51
|
+
anything below can position off it in pure CSS. The legacy --nav-h /
|
|
52
|
+
--nav-bottom keep publishing unchanged; this is additive. %>
|
|
53
|
+
<header data-pin="nav" <%= 'x-data="navCollapse()"'.html_safe unless is_preview %>
|
|
50
54
|
<%# top-0, a STATIC value, and never a custom property. Any bars render as
|
|
51
55
|
this header's sibling in normal flow (studio/banners/_stack), so they
|
|
52
56
|
already occupy their own height above it and there is nothing to
|
|
@@ -200,74 +200,168 @@
|
|
|
200
200
|
(function () {
|
|
201
201
|
if (!window.ResizeObserver) return;
|
|
202
202
|
var ro = null;
|
|
203
|
-
var current = null;
|
|
204
203
|
var queued = false;
|
|
205
|
-
var
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
var pins = [];
|
|
205
|
+
// The names this publisher has written, so a DEPARTED layer can be cleared.
|
|
206
|
+
var published = {};
|
|
207
|
+
|
|
208
|
+
// ONE MEASUREMENT PER ELEMENT PER FRAME, and every read before every write.
|
|
209
|
+
//
|
|
210
|
+
// publish() and publishPin() each kept reads-before-writes INTERNALLY, and
|
|
211
|
+
// that was not enough: the frame ran publish(current) — which WRITES two
|
|
212
|
+
// inherited custom properties on documentElement — and then the pin loop,
|
|
213
|
+
// whose first act is a getBoundingClientRect(). A read after a write to the
|
|
214
|
+
// root forces a fresh layout, so the thrash this file removed came straight
|
|
215
|
+
// back one function boundary over. Measured by review at 6x CPU throttle
|
|
216
|
+
// over 175 frames, pin loop on vs off: frames past 20ms 62 vs 42, median
|
|
217
|
+
// 17.7 vs 14.9ms, p90 34.2 vs 26.0ms; at 10x, 84/180 vs 4/180.
|
|
218
|
+
//
|
|
219
|
+
// The header was also measured TWICE — once as `current`, once as pin
|
|
220
|
+
// `nav` — for values proven identical across 40 frames. It is a pin like
|
|
221
|
+
// any other now; the only thing special about it is that it ALSO writes the
|
|
222
|
+
// legacy --nav-h / --nav-bottom that studio/_sidebar_panel and
|
|
223
|
+
// turf-monster's scroll-margin-top already read.
|
|
224
|
+
function readPin(pin) {
|
|
225
|
+
return {
|
|
226
|
+
pin: pin,
|
|
227
|
+
h: pin.el.offsetHeight,
|
|
228
|
+
bottom: Math.max(0, pin.el.getBoundingClientRect().bottom)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function writeReading(r) {
|
|
208
232
|
var style = document.documentElement.style;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// BOTH READS BEFORE EITHER WRITE. Writing a custom property on
|
|
215
|
-
// documentElement invalidates style, so a read taken after one write
|
|
216
|
-
// forces a fresh layout — the thrash this whole change exists to remove.
|
|
217
|
-
var h = header.offsetHeight;
|
|
218
|
-
// Scroll-dependent where --nav-h is not: chrome above the header scrolls
|
|
219
|
-
// away while an overlay is open (nothing here locks body scroll), so this
|
|
220
|
-
// is republished on scroll. Clamped at 0 — once the sticky header is
|
|
221
|
-
// pinned at top:0 the two values converge, which is why a consumer whose
|
|
222
|
-
// header already starts at the viewport top sees no change at all.
|
|
223
|
-
var bottom = Math.max(0, header.getBoundingClientRect().bottom);
|
|
224
|
-
if (h !== lastH) {
|
|
225
|
-
lastH = h;
|
|
226
|
-
style.setProperty('--nav-h', h + 'px');
|
|
233
|
+
var pin = r.pin;
|
|
234
|
+
if (r.h !== pin.lastH) {
|
|
235
|
+
pin.lastH = r.h;
|
|
236
|
+
style.setProperty('--pin-' + pin.name + '-h', r.h + 'px');
|
|
237
|
+
if (pin.legacy) style.setProperty('--nav-h', r.h + 'px');
|
|
227
238
|
}
|
|
228
|
-
if (bottom !== lastBottom) {
|
|
229
|
-
lastBottom = bottom;
|
|
230
|
-
style.setProperty('--
|
|
239
|
+
if (r.bottom !== pin.lastBottom) {
|
|
240
|
+
pin.lastBottom = r.bottom;
|
|
241
|
+
style.setProperty('--pin-' + pin.name + '-bottom', r.bottom + 'px');
|
|
242
|
+
if (pin.legacy) style.setProperty('--nav-bottom', r.bottom + 'px');
|
|
231
243
|
}
|
|
232
244
|
}
|
|
245
|
+
function publishAll() {
|
|
246
|
+
var readings = [];
|
|
247
|
+
var i;
|
|
248
|
+
for (i = 0; i < pins.length; i++) readings.push(readPin(pins[i]));
|
|
249
|
+
for (i = 0; i < readings.length; i++) writeReading(readings[i]);
|
|
250
|
+
}
|
|
233
251
|
function schedule() {
|
|
234
|
-
if (queued || !
|
|
252
|
+
if (queued || !pins.length) return;
|
|
235
253
|
queued = true;
|
|
236
|
-
window.requestAnimationFrame(function () { queued = false;
|
|
254
|
+
window.requestAnimationFrame(function () { queued = false; publishAll(); });
|
|
237
255
|
}
|
|
238
|
-
|
|
256
|
+
// ONE construction site. Two of them let attach() replace the observer that
|
|
257
|
+
// registerPins() had already put the pins on, silently dropping every live
|
|
258
|
+
// observation — caught by test_pins_share_one_observer_and_one_frame, which
|
|
259
|
+
// counts them for exactly that reason.
|
|
260
|
+
function ensureObserver() {
|
|
261
|
+
if (!ro) ro = new ResizeObserver(schedule);
|
|
262
|
+
return ro;
|
|
263
|
+
}
|
|
264
|
+
function registerPins() {
|
|
265
|
+
ensureObserver();
|
|
266
|
+
pins = [];
|
|
267
|
+
var seen = {};
|
|
239
268
|
var header = document.querySelector('header');
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// would
|
|
245
|
-
//
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
269
|
+
var headerPinned = false;
|
|
270
|
+
var nodes = document.querySelectorAll('[data-pin]');
|
|
271
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
272
|
+
var name = nodes[i].getAttribute('data-pin');
|
|
273
|
+
// An unnamed pin would write `--pin--h`, a valid property name and a
|
|
274
|
+
// silent nonsense one. Skip it rather than publish it.
|
|
275
|
+
if (!name) continue;
|
|
276
|
+
seen[name] = true;
|
|
277
|
+
if (nodes[i] === header) headerPinned = true;
|
|
278
|
+
pins.push({
|
|
279
|
+
el: nodes[i],
|
|
280
|
+
name: name,
|
|
281
|
+
// The header keeps publishing --nav-h / --nav-bottom, from the SAME
|
|
282
|
+
// measurement, so it is never measured twice.
|
|
283
|
+
legacy: nodes[i] === header,
|
|
284
|
+
lastH: null,
|
|
285
|
+
lastBottom: null
|
|
286
|
+
});
|
|
287
|
+
ro.observe(nodes[i]);
|
|
288
|
+
}
|
|
289
|
+
// CLEAR WHAT LEFT. A layer goes away three ways: display:none and x-show
|
|
290
|
+
// both keep the node, so it measures 0 and drops out of a consumer's
|
|
291
|
+
// max() on its own. REMOVAL does not — nothing is left to measure, the
|
|
292
|
+
// last published value stands forever, and a consumer sits at the height
|
|
293
|
+
// of a strip that is gone. Review measured a removed 300px strip holding
|
|
294
|
+
// --pin-apps-bottom at 300px against a real stack bottom of 160px: a
|
|
295
|
+
// permanent 140px error, and a direct contradiction of this primitive's
|
|
296
|
+
// headline promise.
|
|
297
|
+
for (var name2 in published) {
|
|
298
|
+
if (published.hasOwnProperty(name2) && !seen[name2]) {
|
|
299
|
+
document.documentElement.style.removeProperty('--pin-' + name2 + '-h');
|
|
300
|
+
document.documentElement.style.removeProperty('--pin-' + name2 + '-bottom');
|
|
301
|
+
}
|
|
249
302
|
}
|
|
250
|
-
|
|
303
|
+
// THE HOST-OWNED HEADER, and the back-compat promise that depends on it.
|
|
304
|
+
//
|
|
305
|
+
// The registry is built from [data-pin], so a header that does not carry
|
|
306
|
+
// the attribute is not in it — and --nav-h / --nav-bottom, which are
|
|
307
|
+
// written from the legacy pin's reading, would never publish at all.
|
|
308
|
+
// THIS ENGINE'S navbar carries data-pin, so every test here and the whole
|
|
309
|
+
// e2e lab publish fine; the broken path exists only in a consumer that
|
|
310
|
+
// owns its header, which is BOTH live consumers:
|
|
311
|
+
//
|
|
312
|
+
// turf-monster layouts/_navbar.html.erb (data-navbar-root)
|
|
313
|
+
// mcritchie-studio layouts/application.html.erb (inline)
|
|
314
|
+
//
|
|
315
|
+
// Review measured it against turf's real header and its real sidebar
|
|
316
|
+
// declaration: --nav-h and --nav-bottom unset, the gear drawer 82px out
|
|
317
|
+
// of place, the contest board 114px, and neither tracking the collapse.
|
|
318
|
+
// Both apps pin a two-segment ~> under 1.0, so it would have landed on
|
|
319
|
+
// their next bundle update with no floor bump to warn anyone.
|
|
320
|
+
//
|
|
321
|
+
// So the header is adopted as a pin even when it does not ask to be. It
|
|
322
|
+
// still publishes --pin-nav-* under that name, which is what an app gets
|
|
323
|
+
// for free the moment it wants the general contract.
|
|
324
|
+
if (header && !headerPinned) {
|
|
325
|
+
pins.push({
|
|
326
|
+
el: header,
|
|
327
|
+
name: 'nav',
|
|
328
|
+
legacy: true,
|
|
329
|
+
lastH: null,
|
|
330
|
+
lastBottom: null
|
|
331
|
+
});
|
|
332
|
+
seen.nav = true;
|
|
333
|
+
ensureObserver().observe(header);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
published = seen;
|
|
337
|
+
publishAll();
|
|
338
|
+
}
|
|
339
|
+
// ATTACH IS NOW JUST A RE-SCAN. The header used to be tracked separately as
|
|
340
|
+
// `current`, with its own observe and its own publish; it is a pin named
|
|
341
|
+
// "nav" like any other, so registerPins() covers it. What survives is the
|
|
342
|
+
// release promise: a page with no pinned chrome at all disconnects the
|
|
343
|
+
// observer rather than leaving it on detached nodes, and the properties keep
|
|
344
|
+
// their last value rather than being driven to 0 by an all-zero rect.
|
|
345
|
+
//
|
|
346
|
+
// A page that never has a <header> is served too — the observer is built by
|
|
347
|
+
// ensureObserver() from registerPins(), not from a has-header branch. It was
|
|
348
|
+
// only reachable from that branch before, so pins on a headerless layout were
|
|
349
|
+
// permanently dead.
|
|
350
|
+
function attach() {
|
|
251
351
|
if (ro) ro.disconnect();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// during a 300ms transition. navCollapse made it resize on EVERY scroll
|
|
255
|
-
// frame, and each direct publish forced layout and then wrote two
|
|
256
|
-
// inherited custom properties on documentElement. Measured in
|
|
257
|
-
// turf-monster at 6x CPU throttle: frames over 20ms were 13/24 THROUGH
|
|
258
|
-
// the collapse ramp versus 0/24 past it, median 26ms versus 8ms.
|
|
259
|
-
// Ablating this observer alone took it to 0/24 and median 13ms — it cost
|
|
260
|
-
// about 2.6x the reflow it was reacting to. schedule() already coalesces
|
|
261
|
-
// to one publish per frame and is what the scroll and resize listeners
|
|
262
|
-
// use; the ResizeObserver was the one path that bypassed it.
|
|
263
|
-
ro = new ResizeObserver(schedule);
|
|
264
|
-
ro.observe(header);
|
|
265
|
-
publish(header);
|
|
352
|
+
registerPins();
|
|
353
|
+
if (!pins.length && ro) { ro.disconnect(); ro = null; }
|
|
266
354
|
}
|
|
267
355
|
document.addEventListener('DOMContentLoaded', attach);
|
|
268
356
|
document.addEventListener('turbo:load', attach);
|
|
357
|
+
// A Turbo Stream replaces nodes WITHOUT a turbo:load, which is how a
|
|
358
|
+
// captured pin goes stale. Re-register on the next frame, once the stream
|
|
359
|
+
// has actually patched the DOM.
|
|
360
|
+
document.addEventListener('turbo:before-stream-render', function () {
|
|
361
|
+
window.requestAnimationFrame(registerPins);
|
|
362
|
+
});
|
|
269
363
|
// Registered ONCE at script eval, not inside attach(), so a Turbo nav
|
|
270
|
-
// re-
|
|
364
|
+
// re-scans instead of stacking another listener per visit.
|
|
271
365
|
window.addEventListener('scroll', schedule, { passive: true });
|
|
272
366
|
window.addEventListener('resize', schedule, { passive: true });
|
|
273
367
|
})();
|
data/lib/studio/version.rb
CHANGED