studio-engine 0.72.2 → 0.73.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 +55 -0
- data/app/views/layouts/studio/_head.html.erb +261 -152
- data/app/views/solana_sessions/phantom_callback.html.erb +62 -0
- 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: 60486d6f2711eb8d61d258fa6305f05cfd2d85910858aacc1506dee587f3c114
|
|
4
|
+
data.tar.gz: beb3d3a69e3c01fca35a1196e37065b56b9b563696e2d7d3c6d15d5718440fd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c668f68df4d51e460a58a3bfb6b8608acb943b9ab75558b7676c9ed79489219e686249e45c20593ec97c70401ed34c9b8ce78fafb927201912c1d0e87012fd1e
|
|
7
|
+
data.tar.gz: f9f13b69c48bd71d7ad127e1229245a7079dd0155dcfd731fe3044b25fcd439217e6dd61ca3f33b4c73b065cd8d5629dacd500218ac1b27cd02270f528f78e3a
|
data/CHANGELOG.md
CHANGED
|
@@ -185,6 +185,61 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
185
185
|
|
|
186
186
|
### Changed
|
|
187
187
|
|
|
188
|
+
- **The pinned stack composes itself, and publishes in the frame the change
|
|
189
|
+
happened.** `data-pin` layers now also publish `--pin-stack-bottom` (the bottom
|
|
190
|
+
of the whole stack) and `--pin-<name>-top` (the bottom of everything above that
|
|
191
|
+
layer), and the publisher writes them from inside its ResizeObserver callback
|
|
192
|
+
rather than deferring to `requestAnimationFrame`.
|
|
193
|
+
|
|
194
|
+
**THE DEFECT.** A frame runs `rAF -> style/layout -> ResizeObserver -> paint`,
|
|
195
|
+
so a write made in the frame callback lands at the top of the NEXT frame. Every
|
|
196
|
+
frame in which a pinned layer appeared or disappeared therefore painted with the
|
|
197
|
+
previous frame's number. Measured in isolation by toggling a layer's `display`
|
|
198
|
+
and sampling what the changing frame paints: rAF-deferred wrong on 8/8 changing
|
|
199
|
+
frames, synchronous wrong on 0/8. On mcritchie-studio's production
|
|
200
|
+
`/deployments` it showed as the board's swim-lane headers slamming 99px and back
|
|
201
|
+
on a page nobody was touching — parked at a fixed scroll offset with document
|
|
202
|
+
height, row position and nav height all constant, the header's `top` went
|
|
203
|
+
152 -> 53 -> 152 -> 53px in 240ms behind two Turbo broadcasts.
|
|
204
|
+
|
|
205
|
+
**AND CONSUMERS NO LONGER COMPOSE THE STACK.** `top: max(var(--pin-nav-bottom),
|
|
206
|
+
var(--pin-apps-bottom))` fails twice: it does not scale, because a fourth layer
|
|
207
|
+
means editing every consumer that ever wanted to sit under the stack; and it is
|
|
208
|
+
not sound, because a `max()` over two custom properties is only meaningful if
|
|
209
|
+
they were written in the same frame. `--pin-stack-bottom` is one value that
|
|
210
|
+
cannot disagree with itself. A layer that is ITSELF in the stack reads
|
|
211
|
+
`--pin-<name>-top` instead, which excludes its own edge — positioning off the
|
|
212
|
+
stack bottom would make it chase itself down the page. Stacking order is
|
|
213
|
+
document order, with `data-pin-order` overriding it for a layer whose DOM
|
|
214
|
+
position does not match where it sits on screen.
|
|
215
|
+
|
|
216
|
+
**THE REGISTRY IS DERIVED PER PUBLISH, never cached.** A Turbo Stream replaces a
|
|
217
|
+
pinned layer's node without a `turbo:load`, and a held reference then points at
|
|
218
|
+
the DETACHED predecessor, whose rect is all zeros — indistinguishable from a
|
|
219
|
+
layer that is legitimately hidden. Re-querying makes the stale reference
|
|
220
|
+
impossible rather than merely unlikely.
|
|
221
|
+
|
|
222
|
+
**THE COST, re-measured**, because the rule this reverses was itself justified by
|
|
223
|
+
a measurement (turf-monster, 6x CPU throttle, frames over 20ms 13/24 through the
|
|
224
|
+
collapse ramp vs 0/24 once the observer was coalesced). What was expensive there
|
|
225
|
+
was read-write thrash, not the observer: the publisher now takes one read pass
|
|
226
|
+
and one write pass per invocation, and in an RO callback layout is already clean,
|
|
227
|
+
so those reads force nothing. On the engine lab at the same 6x throttle, ~700
|
|
228
|
+
frames of ramp each way — `RO through rAF` median 8.3ms / p90 10.0ms / 0 frames
|
|
229
|
+
over 20ms; `RO synchronous` median 8.4-8.5ms / p90 11.0ms / 3-6 frames over 20ms;
|
|
230
|
+
ZERO frames over 32ms on either, so neither drops one.
|
|
231
|
+
|
|
232
|
+
**BACK-COMPAT.** `--nav-h`, `--nav-bottom` and `--pin-<name>-h` /
|
|
233
|
+
`--pin-<name>-bottom` publish exactly as before, from the same measurements, so
|
|
234
|
+
the 26 consumers across turf-monster, rolio and mcritchie-studio need no change
|
|
235
|
+
and no floor bump. A structural change (a Turbo patch, a fresh document, a layer
|
|
236
|
+
joining or leaving) now forces a full write, because the unchanged-write skip is
|
|
237
|
+
only safe while nothing else touches these properties — the lab caught
|
|
238
|
+
`--nav-h` staying empty through a re-scan on exactly the host-owned-header path
|
|
239
|
+
both live consumers use. The lane contract moves 131 -> 132
|
|
240
|
+
(`config/e2e_lane.yml`), re-derived with the lister.
|
|
241
|
+
|
|
242
|
+
|
|
188
243
|
- **`Gemfile.lock` resolves solana-studio 0.5.7, and a gate now keeps it there.** The lock had sat on **0.5.3 for four patch releases** while BOTH consumers shipped 0.5.7 (turf-monster `~> 0.5.3`, mcritchie-studio `~> 0.5`). Nothing was red and nothing could have been: engine CI installs with `bundler-cache: true`, so it resolves from the lock and never fresh — the drift does not self-correct and never surfaces as flakiness. It matters because `test/views/style_web3_specimens_test.rb` exists to prove "the style guide renders the REAL gem cards" and reads them off whatever the LOCK resolved; four versions behind, that guard certifies a card no consumer receives. It still passes — only its MEANING changes. MEASURED on this span, the gem's whole `app/` tree was byte-identical 0.5.3 → 0.5.7 (only `CHANGELOG.md`, `README.md` and `version.rb` differ), so this instance cost nothing, which is exactly why it went four releases unnoticed.
|
|
189
244
|
- **The constraint stays `>= 0.5.3`, deliberately.** 0.5.3 is a real FLOOR (0.5.2 shipped the credential partial without `solana_studio/modals/_wallet_connect`, which the web3 capability gate requires). Above it this engine claims no ceiling: it is the BASE half of the base/bolt-on split, and a pessimistic `~> 0.5.3` here would be NARROWER than mcritchie-studio's own `~> 0.5` — a dev-only dependency constraining a resolution it does not own. The defect was the LOCK, not the pin, so the fix is a gate rather than a tighter pin.
|
|
190
245
|
- **`bin/gem-drift-check`** — fails when this engine's lock resolves a tracked gem OLDER than a consumer's, wired into `consumer-ci.yml` after the consumer bundle install. That lane is the only place two repos' lockfiles exist at once (engine at `studio/`, consumer beside it); `test/lib/consumer_ci_shard_contract_test.rb` records the same constraint for its own cross-repo contract. Direction is ONE-WAY on purpose: engine behind FAILS, engine level or ahead passes (the engine is the producer and may test an unreleased gem), and a consumer bundling no tracked gem is a SKIP, not a failure — `mcritchie_industries` is the base half working as designed. Stdlib-only, and it names its one-command remedy (`bundle update solana-studio`) in the failure.
|
|
@@ -182,186 +182,295 @@
|
|
|
182
182
|
body { overflow-anchor: none; }
|
|
183
183
|
</style>
|
|
184
184
|
<script>
|
|
185
|
-
// studio-engine:
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// re-attaches after Turbo nav.
|
|
185
|
+
// studio-engine: THE PINNED STACK. Publishes the live geometry of every layer
|
|
186
|
+
// of pinned page chrome as CSS custom properties, so anything fixed or sticky
|
|
187
|
+
// positions off it in CSS with no hardcoded px and no JS of its own.
|
|
189
188
|
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
189
|
+
// A layer joins by carrying `data-pin="<name>"`. Nothing else is required:
|
|
190
|
+
// no registration call, no declared stacking order, no consumer edit.
|
|
192
191
|
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
192
|
+
// WHAT A CONSUMER READS — prefer the first two; they are the reason this is a
|
|
193
|
+
// primitive rather than an arithmetic helper:
|
|
194
|
+
//
|
|
195
|
+
// --pin-stack-bottom the bottom of the WHOLE stack. What something sitting
|
|
196
|
+
// BENEATH all the pinned chrome positions off.
|
|
197
|
+
// --pin-<name>-top the bottom of everything ABOVE that layer. What a layer
|
|
198
|
+
// that is ITSELF in the stack positions off.
|
|
199
|
+
// --pin-<name>-h that layer's HEIGHT.
|
|
200
|
+
// --pin-<name>-bottom that layer's own BOTTOM EDGE, in viewport coordinates.
|
|
201
|
+
// --nav-h / --nav-bottom the <header>'s height and bottom edge, under their
|
|
202
|
+
// legacy names, from the same measurement.
|
|
203
|
+
//
|
|
204
|
+
// DO NOT COMPOSE THE STACK IN A CONSUMER. `top: max(var(--pin-nav-bottom),
|
|
205
|
+
// var(--pin-apps-bottom))` is what this replaced, and it fails twice: it does
|
|
206
|
+
// not scale, because a fourth layer means editing every consumer that ever
|
|
207
|
+
// wanted to sit under the stack; and it is not sound, because a max() over two
|
|
208
|
+
// custom properties is only meaningful if they were written in the same frame.
|
|
209
|
+
// --pin-stack-bottom is one value that cannot disagree with itself.
|
|
210
|
+
//
|
|
211
|
+
// --nav-h AND --nav-bottom ARE NOT THE SAME NUMBER. They are equal only when
|
|
212
|
+
// the header sits at the top of the viewport, which is why one was long
|
|
213
|
+
// mistaken for the other. An app that stacks chrome ABOVE the header — an
|
|
214
|
+
// environment banner rendered before the navbar, as mcritchie-industries does —
|
|
215
|
+
// pushes the header down, and a `fixed` overlay positioned at --nav-h then
|
|
216
|
+
// rides UP over the header by exactly the height of that chrome. Use --nav-h to
|
|
217
|
+
// SIZE something as tall as the header; use --nav-bottom to START something
|
|
218
|
+
// underneath it. Same distinction for --pin-<name>-h vs --pin-<name>-bottom.
|
|
200
219
|
(function () {
|
|
201
220
|
if (!window.ResizeObserver) return;
|
|
202
221
|
var ro = null;
|
|
203
222
|
var queued = false;
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
223
|
+
// WHAT WAS LAST PUBLISHED, per layer name: { h, bottom, top }. It does two
|
|
224
|
+
// jobs. It is the record of which names this publisher has written, so a
|
|
225
|
+
// DEPARTED layer can be cleared. And it is what lets an UNCHANGED value skip
|
|
226
|
+
// its write — these are INHERITED custom properties on documentElement, so
|
|
227
|
+
// even a write of the same number invalidates style for the whole document,
|
|
228
|
+
// and a scroll frame touches this four times per layer.
|
|
229
|
+
var last = {};
|
|
230
|
+
var lastStack = null;
|
|
231
|
+
// SKIPPING AN UNCHANGED WRITE IS ONLY SAFE WHILE NOTHING ELSE TOUCHES THESE
|
|
232
|
+
// PROPERTIES. On a scroll frame that holds, and skipping is most of why this
|
|
233
|
+
// is cheap. On a STRUCTURAL change it does not: a layer arriving or leaving,
|
|
234
|
+
// a Turbo patch, a fresh document — anything there may have cleared a
|
|
235
|
+
// property out from under the cache, and a skip would then never write it
|
|
236
|
+
// again. So a structural change forces a full write, once.
|
|
237
|
+
var forceWrite = true;
|
|
238
|
+
// The nodes currently under observation, held only to know what to unobserve
|
|
239
|
+
// when one departs. The REGISTRY ITSELF is never cached; see readPins().
|
|
240
|
+
var observed = [];
|
|
241
|
+
|
|
242
|
+
// THE REGISTRY IS DERIVED, NEVER CACHED — and that is a correctness rule.
|
|
209
243
|
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
244
|
+
// This used to build a `pins` array once and hold each element by reference
|
|
245
|
+
// until something re-registered it. A Turbo Stream replaces a pinned layer's
|
|
246
|
+
// node WITHOUT a turbo:load, so between the patch and the re-scan the held
|
|
247
|
+
// reference pointed at the DETACHED predecessor — and a detached node's rect
|
|
248
|
+
// is all zeros, which is indistinguishable from a layer that is legitimately
|
|
249
|
+
// hidden. Measured on mcritchie-studio's /deployments: --pin-apps-bottom
|
|
250
|
+
// published 0px while the live strip stood at display:block, bottom 152px,
|
|
251
|
+
// and the board's lane headers slammed 99px up and back on every broadcast.
|
|
218
252
|
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
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) {
|
|
232
|
-
var style = document.documentElement.style;
|
|
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');
|
|
238
|
-
}
|
|
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');
|
|
243
|
-
}
|
|
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
|
-
}
|
|
251
|
-
function schedule() {
|
|
252
|
-
if (queued || !pins.length) return;
|
|
253
|
-
queued = true;
|
|
254
|
-
window.requestAnimationFrame(function () { queued = false; publishAll(); });
|
|
255
|
-
}
|
|
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 = {};
|
|
253
|
+
// Re-querying costs one querySelectorAll over a handful of nodes per publish.
|
|
254
|
+
// That is cheaper than the class of bug it removes, because it makes a stale
|
|
255
|
+
// reference IMPOSSIBLE rather than merely unlikely: there is no window between
|
|
256
|
+
// a DOM patch and a re-scan in which this can be pointed at the wrong node.
|
|
257
|
+
function readPins() {
|
|
268
258
|
var header = document.querySelector('header');
|
|
269
|
-
var headerPinned = false;
|
|
270
259
|
var nodes = document.querySelectorAll('[data-pin]');
|
|
260
|
+
var list = [];
|
|
261
|
+
var headerPinned = false;
|
|
271
262
|
for (var i = 0; i < nodes.length; i++) {
|
|
272
263
|
var name = nodes[i].getAttribute('data-pin');
|
|
273
264
|
// An unnamed pin would write `--pin--h`, a valid property name and a
|
|
274
265
|
// silent nonsense one. Skip it rather than publish it.
|
|
275
266
|
if (!name) continue;
|
|
276
|
-
seen[name] = true;
|
|
277
267
|
if (nodes[i] === header) headerPinned = true;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
268
|
+
list.push(measure(nodes[i], name, nodes[i] === header));
|
|
269
|
+
}
|
|
270
|
+
// THE HOST-OWNED HEADER, and the back-compat promise that depends on it.
|
|
271
|
+
// A header that does not carry [data-pin] is not in the registry, so the
|
|
272
|
+
// legacy --nav-h / --nav-bottom — which studio/_sidebar_panel, turf's
|
|
273
|
+
// contest board and mcritchie-studio's heartbeat CSS all read — would
|
|
274
|
+
// never publish at all. Both live consumers own their header, so it is
|
|
275
|
+
// adopted as a pin even when it does not ask to be.
|
|
276
|
+
if (header && !headerPinned) list.push(measure(header, 'nav', true));
|
|
277
|
+
return list;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ONE READ PER ELEMENT PER PUBLISH, and every read before every write.
|
|
281
|
+
// A read after a write to the root forces a fresh layout, which is the
|
|
282
|
+
// thrash this publisher exists without. Measured by review at 6x CPU
|
|
283
|
+
// throttle over 175 frames: frames past 20ms 62 vs 42, median 17.7 vs
|
|
284
|
+
// 14.9ms, p90 34.2 vs 26.0ms; at 10x, 84/180 vs 4/180.
|
|
285
|
+
function measure(el, name, legacy) {
|
|
286
|
+
// A hidden layer needs no special case: display:none gives offsetHeight 0
|
|
287
|
+
// and an all-zero rect, so it measures 0 and drops out of the stack on its
|
|
288
|
+
// own. That is what lets a layer come and go without anyone declaring a
|
|
289
|
+
// stacking order, and it is load-bearing rather than incidental.
|
|
290
|
+
var r = el.getBoundingClientRect();
|
|
291
|
+
return { el: el, name: name, legacy: legacy, h: el.offsetHeight, bottom: Math.max(0, r.bottom) };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// THE STACK, COMPOSED HERE RATHER THAN BY EVERY CONSUMER.
|
|
295
|
+
//
|
|
296
|
+
// Consumers used to write `top: max(var(--pin-nav-bottom), var(--pin-apps-bottom))`
|
|
297
|
+
// — every layer enumerated, in every consumer. That is two failures at once.
|
|
298
|
+
// It does not SCALE: a fourth pinned layer means editing every consumer that
|
|
299
|
+
// ever wanted to sit under the stack. And it is not SOUND: max() over two
|
|
300
|
+
// independently-written custom properties is only meaningful if they were
|
|
301
|
+
// written in the same frame, and any skew between them is expressed instantly
|
|
302
|
+
// as layout. Composing here means a consumer reads ONE value that cannot
|
|
303
|
+
// disagree with itself.
|
|
304
|
+
//
|
|
305
|
+
// --pin-stack-bottom the bottom of the WHOLE pinned stack. What something
|
|
306
|
+
// sitting beneath all the chrome positions off.
|
|
307
|
+
// --pin-<name>-top the bottom of everything ABOVE that layer. What a
|
|
308
|
+
// layer that is ITSELF in the stack positions off —
|
|
309
|
+
// --pin-stack-bottom would include the layer's own
|
|
310
|
+
// edge, so a layer positioned off it would chase itself
|
|
311
|
+
// down the page.
|
|
312
|
+
//
|
|
313
|
+
// ORDER is document order, which is the order the layers were declared and
|
|
314
|
+
// therefore the order they read in. `data-pin-order` overrides it for a layer
|
|
315
|
+
// whose DOM position does not match where it sits on screen; it is a plain
|
|
316
|
+
// number, lower is higher up, and it needs to exist on only the layer that
|
|
317
|
+
// disagrees rather than on all of them.
|
|
318
|
+
function order(p) {
|
|
319
|
+
var raw = parseFloat(p.el.getAttribute('data-pin-order'));
|
|
320
|
+
return isNaN(raw) ? null : raw;
|
|
321
|
+
}
|
|
322
|
+
function composed(pins) {
|
|
323
|
+
var i, j, stack = 0;
|
|
324
|
+
for (i = 0; i < pins.length; i++) {
|
|
325
|
+
if (pins[i].bottom > stack) stack = pins[i].bottom;
|
|
326
|
+
var above = 0;
|
|
327
|
+
for (j = 0; j < pins.length; j++) {
|
|
328
|
+
if (i === j) continue;
|
|
329
|
+
var oi = order(pins[i]), oj = order(pins[j]);
|
|
330
|
+
// Both ordered: compare the numbers. Otherwise fall back to document
|
|
331
|
+
// order, which querySelectorAll already returns them in.
|
|
332
|
+
var jIsAbove = (oi !== null && oj !== null) ? oj < oi : j < i;
|
|
333
|
+
if (jIsAbove && pins[j].bottom > above) above = pins[j].bottom;
|
|
334
|
+
}
|
|
335
|
+
pins[i].top = above;
|
|
336
|
+
}
|
|
337
|
+
return stack;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function write(pins, stack) {
|
|
341
|
+
var style = document.documentElement.style;
|
|
342
|
+
var seen = {};
|
|
343
|
+
var i, p, prev;
|
|
344
|
+
// A ROSTER CHANGE IS STRUCTURAL TOO, and it is not always announced by an
|
|
345
|
+
// event: an x-show that adds the node, a layer rendered by something other
|
|
346
|
+
// than a stream. Compare the names against what was published last.
|
|
347
|
+
var force = forceWrite;
|
|
348
|
+
forceWrite = false;
|
|
349
|
+
if (!force) {
|
|
350
|
+
for (i = 0; i < pins.length; i++) { if (!last.hasOwnProperty(pins[i].name)) { force = true; break; } }
|
|
288
351
|
}
|
|
352
|
+
for (i = 0; i < pins.length; i++) {
|
|
353
|
+
p = pins[i];
|
|
354
|
+
seen[p.name] = true;
|
|
355
|
+
prev = last[p.name] || {};
|
|
356
|
+
if (force || p.h !== prev.h) {
|
|
357
|
+
style.setProperty('--pin-' + p.name + '-h', p.h + 'px');
|
|
358
|
+
if (p.legacy) style.setProperty('--nav-h', p.h + 'px');
|
|
359
|
+
}
|
|
360
|
+
if (force || p.bottom !== prev.bottom) {
|
|
361
|
+
style.setProperty('--pin-' + p.name + '-bottom', p.bottom + 'px');
|
|
362
|
+
if (p.legacy) style.setProperty('--nav-bottom', p.bottom + 'px');
|
|
363
|
+
}
|
|
364
|
+
if (force || p.top !== prev.top) style.setProperty('--pin-' + p.name + '-top', p.top + 'px');
|
|
365
|
+
last[p.name] = { h: p.h, bottom: p.bottom, top: p.top };
|
|
366
|
+
}
|
|
367
|
+
if (force || stack !== lastStack) {
|
|
368
|
+
style.setProperty('--pin-stack-bottom', stack + 'px');
|
|
369
|
+
lastStack = stack;
|
|
370
|
+
}
|
|
371
|
+
|
|
289
372
|
// 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
|
|
291
|
-
//
|
|
292
|
-
// last published value stands forever
|
|
373
|
+
// both keep the node, so it measures 0 and drops out of the stack on its
|
|
374
|
+
// own. REMOVAL does not — nothing is left to measure, and without this the
|
|
375
|
+
// last published value stands forever while the consumer sits at the height
|
|
293
376
|
// 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
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
377
|
+
// --pin-apps-bottom at 300px against a real stack bottom of 160px.
|
|
378
|
+
for (var name in last) {
|
|
379
|
+
if (last.hasOwnProperty(name) && !seen[name]) {
|
|
380
|
+
style.removeProperty('--pin-' + name + '-h');
|
|
381
|
+
style.removeProperty('--pin-' + name + '-bottom');
|
|
382
|
+
style.removeProperty('--pin-' + name + '-top');
|
|
383
|
+
delete last[name];
|
|
301
384
|
}
|
|
302
385
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function publishAll() {
|
|
389
|
+
var pins = readPins();
|
|
390
|
+
write(pins, composed(pins));
|
|
391
|
+
syncObserver(pins);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Observe whatever is pinned RIGHT NOW. observe() on an already-observed node
|
|
395
|
+
// is a no-op, so this only ever adds the new; unobserve() drops the departed
|
|
396
|
+
// so a replaced node cannot keep waking us from outside the document.
|
|
397
|
+
function syncObserver(pins) {
|
|
398
|
+
if (!pins.length) {
|
|
399
|
+
if (ro) { ro.disconnect(); ro = null; observed = []; }
|
|
400
|
+
return;
|
|
334
401
|
}
|
|
402
|
+
if (!ro) ro = new ResizeObserver(onResize);
|
|
403
|
+
var i, next = [];
|
|
404
|
+
for (i = 0; i < pins.length; i++) { next.push(pins[i].el); ro.observe(pins[i].el); }
|
|
405
|
+
for (i = 0; i < observed.length; i++) {
|
|
406
|
+
if (next.indexOf(observed[i]) === -1) ro.unobserve(observed[i]);
|
|
407
|
+
}
|
|
408
|
+
observed = next;
|
|
409
|
+
}
|
|
335
410
|
|
|
336
|
-
|
|
411
|
+
// PUBLISH IN THE ResizeObserver CALLBACK, SYNCHRONOUSLY. This is the whole
|
|
412
|
+
// fix, and it is a statement about WHEN a frame does its work rather than
|
|
413
|
+
// about what this code computes.
|
|
414
|
+
//
|
|
415
|
+
// A frame runs: rAF callbacks -> style/layout -> ResizeObserver callbacks ->
|
|
416
|
+
// paint. This publisher used to be woken by the observer and then DEFER the
|
|
417
|
+
// write to requestAnimationFrame, which lands at the top of the NEXT frame —
|
|
418
|
+
// so the frame in which a layer actually appeared or disappeared painted with
|
|
419
|
+
// the previous frame's number, every time. The RO callback runs after layout
|
|
420
|
+
// and before paint, which is exactly the last moment a write can still land
|
|
421
|
+
// in the frame that caused it.
|
|
422
|
+
//
|
|
423
|
+
// A/B measured in isolation, toggling a pinned layer's display and sampling
|
|
424
|
+
// what the changing frame paints: rAF-deferred wrong on 8/8 changing frames,
|
|
425
|
+
// RO-synchronous wrong on 0/8.
|
|
426
|
+
//
|
|
427
|
+
// This cannot loop. The values written here move CONSUMERS, and a consumer is
|
|
428
|
+
// not a pin — nothing this writes resizes anything it observes. A pinned layer
|
|
429
|
+
// that positions off --pin-<name>-top MOVES without RESIZING, and the observer
|
|
430
|
+
// fires on size, so it settles in one pass rather than ringing.
|
|
431
|
+
// The one way a SYNCHRONOUS publish could misbehave: a consumer that both
|
|
432
|
+
// reads a published value and is itself a pin, sized off what it reads
|
|
433
|
+
// (`height: calc(100vh - var(--pin-apps-top))`). That resizes an observed
|
|
434
|
+
// node from inside the observer's own callback, and left alone the browser
|
|
435
|
+
// reports "ResizeObserver loop completed with undelivered notifications" and
|
|
436
|
+
// drops the frame. So re-entry is counted, and past a couple of settling
|
|
437
|
+
// passes this hands the rest to the frame callback: the pathological page
|
|
438
|
+
// degrades to the OLD one-frame-late behaviour instead of wedging, and the
|
|
439
|
+
// ordinary page never reaches the branch. Reset on the next frame, so the
|
|
440
|
+
// count measures one frame's settling rather than the session's.
|
|
441
|
+
var depth = 0, resetQueued = false;
|
|
442
|
+
function onResize() {
|
|
443
|
+
if (depth >= 3) { schedule(); return; }
|
|
444
|
+
depth++;
|
|
445
|
+
if (!resetQueued) {
|
|
446
|
+
resetQueued = true;
|
|
447
|
+
window.requestAnimationFrame(function () { resetQueued = false; depth = 0; });
|
|
448
|
+
}
|
|
337
449
|
publishAll();
|
|
338
450
|
}
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// only reachable from that branch before, so pins on a headerless layout were
|
|
349
|
-
// permanently dead.
|
|
350
|
-
function attach() {
|
|
351
|
-
if (ro) ro.disconnect();
|
|
352
|
-
registerPins();
|
|
353
|
-
if (!pins.length && ro) { ro.disconnect(); ro = null; }
|
|
451
|
+
|
|
452
|
+
// SCROLL STILL NEEDS THE FRAME CALLBACK. A scroll changes a sticky header's
|
|
453
|
+
// bottom EDGE without changing its size, so the observer never sees it and
|
|
454
|
+
// there is nothing to be synchronous with — rAF is the right clock, and it
|
|
455
|
+
// coalesces the burst iOS momentum fires far above 60Hz.
|
|
456
|
+
function schedule() {
|
|
457
|
+
if (queued) return;
|
|
458
|
+
queued = true;
|
|
459
|
+
window.requestAnimationFrame(function () { queued = false; publishAll(); });
|
|
354
460
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
//
|
|
461
|
+
|
|
462
|
+
function invalidate() { forceWrite = true; publishAll(); }
|
|
463
|
+
document.addEventListener('DOMContentLoaded', invalidate);
|
|
464
|
+
document.addEventListener('turbo:load', invalidate);
|
|
465
|
+
// A Turbo Stream replaces nodes WITHOUT a turbo:load. The registry is derived
|
|
466
|
+
// per publish, so a stale reference is no longer possible either way — but the
|
|
467
|
+
// OBSERVER still has to be moved onto the incoming node, and the stack has to
|
|
468
|
+
// be recomposed for a layer that arrived or left. Both happen in publishAll.
|
|
360
469
|
document.addEventListener('turbo:before-stream-render', function () {
|
|
361
|
-
window.requestAnimationFrame(
|
|
470
|
+
window.requestAnimationFrame(invalidate);
|
|
362
471
|
});
|
|
363
|
-
// Registered ONCE at script eval, not inside
|
|
364
|
-
// re-
|
|
472
|
+
// Registered ONCE at script eval, not inside a re-scan, so a Turbo nav
|
|
473
|
+
// re-publishes instead of stacking another listener per visit.
|
|
365
474
|
window.addEventListener('scroll', schedule, { passive: true });
|
|
366
475
|
window.addEventListener('resize', schedule, { passive: true });
|
|
367
476
|
})();
|
|
@@ -137,6 +137,68 @@
|
|
|
137
137
|
dbg(' ' + key, truncate(val, 40));
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
// === THE REDIRECT TRANSPORT'S RETURN LEG ==============================
|
|
141
|
+
//
|
|
142
|
+
// TRIED FIRST, AND EVERYTHING BELOW IS LEFT EXACTLY AS IT WAS. The legacy
|
|
143
|
+
// block that follows handles ONE step — Phantom's signIn — with keys named
|
|
144
|
+
// phantom_dl_*, and it is what production mobile sign-in runs on TODAY. It is
|
|
145
|
+
// not touched, moved, or reworded here: a consumer that has not yet adopted
|
|
146
|
+
// the new transport must behave identically after this change, and the only
|
|
147
|
+
// honest way to promise that is to leave its code alone.
|
|
148
|
+
//
|
|
149
|
+
// GUARDED THE WAY THIS CODEBASE GUARDS EVERY OPTIONAL CAPABILITY: a host that
|
|
150
|
+
// does not load solana_studio/wallet_ops.js + wallet_journal.js has no
|
|
151
|
+
// SolanaStudio.walletOps, so this branch never runs and the legacy path is
|
|
152
|
+
// the whole program. An absent capability must not default to the permissive
|
|
153
|
+
// branch — the same rule the wallet picker's canDeepLink learned.
|
|
154
|
+
//
|
|
155
|
+
// WHY THE PENDING-JOURNAL CHECK IS PART OF THE GUARD, not just the globals:
|
|
156
|
+
// a host may load the new scripts while a signIn started under the OLD keys
|
|
157
|
+
// is still in flight — a user mid-round-trip across a deploy. Requiring an
|
|
158
|
+
// actual wallet_dl journal means that user finishes on the path they started,
|
|
159
|
+
// rather than being handed to a resumer with nothing to resume.
|
|
160
|
+
//
|
|
161
|
+
// WHAT THIS BRANCH DOES NOT DO: decrypt, verify, or know what the user was
|
|
162
|
+
// trying to accomplish. walletOps owns the step machine and the registered
|
|
163
|
+
// intent owns the outcome. This is a seam, and keeping it a seam is what lets
|
|
164
|
+
// the same callback serve sign-in and a contest entry without learning either.
|
|
165
|
+
var studio = window.SolanaStudio;
|
|
166
|
+
var hasResumer = !!(studio && studio.walletOps && typeof studio.walletOps.resume === 'function' &&
|
|
167
|
+
studio.walletJournal && typeof studio.walletJournal.peek === 'function');
|
|
168
|
+
|
|
169
|
+
if (hasResumer && studio.walletJournal.peek()) {
|
|
170
|
+
dbg('Transport', 'wallet_dl journal present — resuming through walletOps');
|
|
171
|
+
|
|
172
|
+
studio.walletOps.resume(params, {
|
|
173
|
+
navigate: function(url) { window.location.href = url; }
|
|
174
|
+
}).then(function(result) {
|
|
175
|
+
// SUSPENDED means resume already navigated to the next hop; this document
|
|
176
|
+
// is on its way out and must not also redirect, or the two race.
|
|
177
|
+
if (result && result.suspended) {
|
|
178
|
+
dbg('Transport', 'advanced to the next hop');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (result && result.done) {
|
|
182
|
+
dbg('Transport', 'intent complete');
|
|
183
|
+
// The registered intent owns where to land — it knows what the user was
|
|
184
|
+
// doing. Falling back to '/' rather than guessing a wallet-specific
|
|
185
|
+
// destination, which this file has no business knowing.
|
|
186
|
+
window.location.href = (result.value && result.value.redirect) || '/';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// No pending request is a STATE, not a failure: a stale bookmark, a second
|
|
190
|
+
// tab, a reload after the journal was consumed. peek() said otherwise a
|
|
191
|
+
// moment ago, so this is a race, and the honest answer is to start over.
|
|
192
|
+
showError('No pending wallet request. Redirecting to login...');
|
|
193
|
+
}, function(err) {
|
|
194
|
+
// A user rejection arrives here with .rejected set — the wallet's own
|
|
195
|
+
// words, already mapped. Anything else is a genuine fault.
|
|
196
|
+
dbg('Transport ERROR', (err && err.message) || String(err));
|
|
197
|
+
showError((err && err.message) || 'Wallet request failed');
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
140
202
|
// Check for Phantom error
|
|
141
203
|
if (params.get('errorCode')) {
|
|
142
204
|
dbg('ERROR', 'errorCode=' + params.get('errorCode'));
|
data/lib/studio/version.rb
CHANGED