studio-engine 0.111.1.rc1 → 0.111.2.rc1
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 +31 -0
- data/app/assets/javascripts/studio/sticky_table_header.js +78 -16
- data/app/javascript/studio/pinned_stack.js +25 -0
- data/app/views/layouts/studio/_head.html.erb +5 -2
- data/app/views/studio/profiles/_identity_mini.html.erb +4 -4
- data/app/views/style/index.html.erb +5 -2
- data/lib/studio/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8808038af9e2419c0af0177895740230d150828f028ae40be6824c877624b557
|
|
4
|
+
data.tar.gz: ca450685c591c1414842097435599e658e5a0a68cbf9b05f1405d7c338d35a88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae13f4a634c55627b37809d8fb01b39c853a8bec472374aaf5afd30f3f57d84ca99b2d07a92132b8e8ee4ddf28e563ef2fe62befea927262830fb4e7f2065efa
|
|
7
|
+
data.tar.gz: d900c3a5fcb6aae1d4339fe0a74b078cc9876c11f810f751112e0b2018fbaf54f5205363d8b61d78474c502b501b4f110f9c9eea78e2faaa426c19e92c0bdf1b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.111.2 — 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The sticky table header's clone sits on the pinned bar.** It placed its
|
|
12
|
+
`position: fixed` header row at `--nav-h`, re-read only on a scroll, a resize
|
|
13
|
+
or a header resize. On a scroll-timeline header the compact height is
|
|
14
|
+
published only once the nav-collapse controller starts, which resizes
|
|
15
|
+
nothing, so a page loaded at a restored scroll position kept the clone at the
|
|
16
|
+
expanded height (Turf Monster `/contests`: 96px under a 50px bar, rows showing
|
|
17
|
+
in the gap) until the first scroll. On a phone the root's `--nav-h` also
|
|
18
|
+
lagged the collapse, so the clone sat off the bar through a flick. The clone
|
|
19
|
+
now reads the stack's live bottom (`window.studioPinnedStack.read()`, closing
|
|
20
|
+
the expanded header's travel on the timeline as `data-pin-edge` does), falls
|
|
21
|
+
back to `--pin-stack-bottom`, and never reads `--nav-h`. It re-places on
|
|
22
|
+
`studio:pinned-stack-refresh`, on every stack publish, and synchronously when
|
|
23
|
+
any `data-pin` layer resizes.
|
|
24
|
+
- **The style guide's section nav sticks under the whole pinned stack**
|
|
25
|
+
(`--pin-stack-bottom`), not at the header's height, which put it under any
|
|
26
|
+
chrome stacked above the header.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`studio:pinned-stack-published`.** `studio/pinned_stack` fires it on the
|
|
31
|
+
document after a publish that moved the stack, with the reading it just made
|
|
32
|
+
(`{ bottom, timeline, travel }`) as `detail`, so a script that positions off
|
|
33
|
+
the stack re-places in the same frame. `docs/NAVBAR_SETUP.md`, *Reading the
|
|
34
|
+
Pinned Stack*.
|
|
35
|
+
- **`layouts/studio/_head` takes a `sticky_table_headers:` local** that answers
|
|
36
|
+
for one render (nil defers to `config.sticky_table_headers`).
|
|
37
|
+
|
|
7
38
|
## 0.111.1 — 2026-10-16
|
|
8
39
|
|
|
9
40
|
### Added
|
|
@@ -4,18 +4,69 @@
|
|
|
4
4
|
const MANUAL_TABLE_SELECTOR = "table[data-sticky-table-header]";
|
|
5
5
|
const AUTO_TABLE_SELECTOR = "table";
|
|
6
6
|
const SKIP_CONTAINER_SELECTOR = "[data-sticky-table-skip]";
|
|
7
|
+
// What studio/nav_collapse fires when a scroll-timeline header starts,
|
|
8
|
+
// re-measures its band, or stops: the header's PINNED height changed without
|
|
9
|
+
// its box resizing, so no ResizeObserver and no scroll hears of it.
|
|
10
|
+
const PINNED_STACK_REFRESH = "studio:pinned-stack-refresh";
|
|
11
|
+
// What studio/pinned_stack fires after every publish that moved the stack.
|
|
12
|
+
const PINNED_STACK_PUBLISHED = "studio:pinned-stack-published";
|
|
7
13
|
|
|
8
14
|
let stickyTables = [];
|
|
9
15
|
let scheduled = false;
|
|
10
16
|
let bootScheduled = false;
|
|
11
|
-
let
|
|
17
|
+
let pinResizeObserver = null;
|
|
12
18
|
let tableMutationObserver = null;
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
// WHERE THE CLONE STARTS: THE PINNED STACK'S BOTTOM EDGE, read live.
|
|
21
|
+
//
|
|
22
|
+
// This used to read --nav-h off the root, and that was wrong twice. --nav-h
|
|
23
|
+
// is the header's HEIGHT, not its bottom edge, so chrome stacked above the
|
|
24
|
+
// header put the clone that much too high. And a root custom property is a
|
|
25
|
+
// SNAPSHOT this script re-read only on scroll, resize or a header resize: on
|
|
26
|
+
// a scroll-timeline header the compact height is published only once the
|
|
27
|
+
// nav-collapse controller starts, which resizes nothing, so a page loaded at
|
|
28
|
+
// a restored scroll position kept the clone at the expanded height (Turf
|
|
29
|
+
// Monster /contests: 96px against a 50px bar, rows showing in the gap) until
|
|
30
|
+
// the first scroll.
|
|
31
|
+
//
|
|
32
|
+
// window.studioPinnedStack.read() measures on call, the same reading the
|
|
33
|
+
// publisher makes. On a scroll-timeline header it reports where the stack
|
|
34
|
+
// sits once PINNED; at the top of the page the expanded header still reaches
|
|
35
|
+
// `travel` further down, and closes that gap as the page scrolls, exactly as
|
|
36
|
+
// engine.css's data-pin-edge does for a fixed reader in CSS. A host that never
|
|
37
|
+
// started the publisher falls back to --pin-stack-bottom, then to 0: a clone
|
|
38
|
+
// at the top of the viewport rather than none.
|
|
39
|
+
function pinnedEdge(reading) {
|
|
40
|
+
const r = reading || window.studioPinnedStack?.read?.();
|
|
41
|
+
if (!r) {
|
|
42
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue("--pin-stack-bottom");
|
|
43
|
+
return Number.parseFloat(raw) || 0;
|
|
44
|
+
}
|
|
45
|
+
const bottom = Number(r.bottom) || 0;
|
|
46
|
+
if (!r.timeline) return bottom;
|
|
47
|
+
return bottom + Math.max(0, (Number(r.travel) || 0) - Math.max(0, window.scrollY));
|
|
48
|
+
}
|
|
15
49
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
50
|
+
// Where the clone goes for one table, as a pure function of what was measured:
|
|
51
|
+
// shown once the real header has scrolled up to the edge, and pushed up by the
|
|
52
|
+
// table's own bottom so it leaves with the table.
|
|
53
|
+
function placement({ edge, headerTop, tableBottom, cloneHeight }) {
|
|
54
|
+
const active = headerTop <= edge && tableBottom > edge;
|
|
55
|
+
return { active, top: active ? Math.min(edge, tableBottom - cloneHeight) : null };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.StudioStickyTableHeaders = { loaded: true, pinnedEdge, placement };
|
|
59
|
+
|
|
60
|
+
// One pass over every table, reading the edge ONCE. Called from a frame
|
|
61
|
+
// callback (scroll, resize, a stack event) and SYNCHRONOUSLY from the
|
|
62
|
+
// ResizeObserver, which runs after layout and before paint: a script-mode
|
|
63
|
+
// header changes height every frame of its collapse (studio/nav_collapse
|
|
64
|
+
// writes --nav-p from its own frame callback, after ours), and a frame-late
|
|
65
|
+
// clone shows rows through the gap for that frame.
|
|
66
|
+
function updateAll(reading) {
|
|
67
|
+
if (!stickyTables.length) return;
|
|
68
|
+
const edge = pinnedEdge(reading);
|
|
69
|
+
stickyTables.slice().forEach((stickyTable) => stickyTable.update(edge));
|
|
19
70
|
}
|
|
20
71
|
|
|
21
72
|
function scheduleUpdate() {
|
|
@@ -23,7 +74,7 @@
|
|
|
23
74
|
scheduled = true;
|
|
24
75
|
requestAnimationFrame(() => {
|
|
25
76
|
scheduled = false;
|
|
26
|
-
|
|
77
|
+
updateAll();
|
|
27
78
|
});
|
|
28
79
|
}
|
|
29
80
|
|
|
@@ -80,19 +131,20 @@
|
|
|
80
131
|
});
|
|
81
132
|
}
|
|
82
133
|
|
|
83
|
-
update() {
|
|
134
|
+
update(edge = pinnedEdge()) {
|
|
84
135
|
if (!document.body.contains(this.table) || !this.table.tHead || !this.scroller) {
|
|
85
136
|
this.destroy();
|
|
86
137
|
stickyTables = stickyTables.filter((stickyTable) => stickyTable !== this);
|
|
87
138
|
return;
|
|
88
139
|
}
|
|
89
140
|
|
|
90
|
-
const offset = navOffset();
|
|
91
141
|
const headerRect = this.table.tHead.getBoundingClientRect();
|
|
92
142
|
const tableRect = this.table.getBoundingClientRect();
|
|
93
143
|
const scrollerRect = this.scroller.getBoundingClientRect();
|
|
94
144
|
const cloneHeight = headerRect.height || this.cloneShell.getBoundingClientRect().height;
|
|
95
|
-
const active
|
|
145
|
+
const { active, top } = placement({
|
|
146
|
+
edge, headerTop: headerRect.top, tableBottom: tableRect.bottom, cloneHeight
|
|
147
|
+
});
|
|
96
148
|
|
|
97
149
|
if (!active) {
|
|
98
150
|
this.cloneShell.style.display = "none";
|
|
@@ -102,7 +154,6 @@
|
|
|
102
154
|
|
|
103
155
|
this.syncColumnWidths();
|
|
104
156
|
|
|
105
|
-
const top = Math.min(offset, tableRect.bottom - cloneHeight);
|
|
106
157
|
const translateX = tableRect.left - scrollerRect.left;
|
|
107
158
|
|
|
108
159
|
this.cloneShell.style.display = "block";
|
|
@@ -119,12 +170,17 @@
|
|
|
119
170
|
function bootStickyTables() {
|
|
120
171
|
startTableObserver();
|
|
121
172
|
|
|
122
|
-
|
|
173
|
+
// EVERY LAYER OF THE PINNED STACK, not the header alone: a bar above the
|
|
174
|
+
// header that grows or goes moves the edge too. Re-observed on every boot.
|
|
175
|
+
if (pinResizeObserver) pinResizeObserver.disconnect();
|
|
176
|
+
pinResizeObserver = null;
|
|
123
177
|
if (window.ResizeObserver) {
|
|
178
|
+
const layers = new Set(document.querySelectorAll("[data-pin]"));
|
|
124
179
|
const header = document.querySelector("header");
|
|
125
|
-
if (header)
|
|
126
|
-
|
|
127
|
-
|
|
180
|
+
if (header) layers.add(header);
|
|
181
|
+
if (layers.size) {
|
|
182
|
+
pinResizeObserver = new ResizeObserver(() => updateAll());
|
|
183
|
+
layers.forEach((layer) => pinResizeObserver.observe(layer));
|
|
128
184
|
}
|
|
129
185
|
}
|
|
130
186
|
|
|
@@ -187,13 +243,19 @@
|
|
|
187
243
|
document.addEventListener("turbo:load", bootStickyTables);
|
|
188
244
|
document.addEventListener("DOMContentLoaded", bootStickyTables);
|
|
189
245
|
document.addEventListener("turbo:before-cache", () => {
|
|
190
|
-
if (
|
|
246
|
+
if (pinResizeObserver) pinResizeObserver.disconnect();
|
|
191
247
|
if (tableMutationObserver) tableMutationObserver.disconnect();
|
|
192
|
-
|
|
248
|
+
pinResizeObserver = null;
|
|
193
249
|
tableMutationObserver = null;
|
|
194
250
|
stickyTables.forEach((stickyTable) => stickyTable.destroy());
|
|
195
251
|
stickyTables = [];
|
|
196
252
|
});
|
|
253
|
+
// A STACK REPUBLISH RE-PLACES THE CLONE. The refresh is the timeline header
|
|
254
|
+
// changing its pinned height (on start, which is the restored-scroll case);
|
|
255
|
+
// the publish carries the reading the publisher just made, so the clone moves
|
|
256
|
+
// in the same frame and with the same number.
|
|
257
|
+
document.addEventListener(PINNED_STACK_REFRESH, scheduleUpdate);
|
|
258
|
+
document.addEventListener(PINNED_STACK_PUBLISHED, (event) => updateAll(event.detail || undefined));
|
|
197
259
|
window.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
198
260
|
window.addEventListener("resize", scheduleUpdate, { passive: true });
|
|
199
261
|
})();
|
|
@@ -232,6 +232,23 @@ if (typeof window !== 'undefined') {
|
|
|
232
232
|
// without its box resizing, which the ResizeObserver cannot see.
|
|
233
233
|
export var REFRESH_EVENT = 'studio:pinned-stack-refresh';
|
|
234
234
|
|
|
235
|
+
// What this publisher fires on the document after a publish that MOVED the
|
|
236
|
+
// stack: its bottom, whether the header is on the scroll timeline, or its
|
|
237
|
+
// travel. The detail is the reading it just made, { bottom, timeline, travel },
|
|
238
|
+
// the same shape window.studioPinnedStack.read() gives. It is for a SCRIPT that
|
|
239
|
+
// positions off the stack (studio/sticky_table_header's clone): a value written
|
|
240
|
+
// to the root reaches CSS on its own, but a script has to be told, and without
|
|
241
|
+
// this it heard only of a scroll, a resize or a box it observed itself.
|
|
242
|
+
// Fired synchronously from inside the publish, so a listener that places
|
|
243
|
+
// something lands it in the same frame. Not fired when nothing moved, so a
|
|
244
|
+
// scroll frame on the timeline (where the stack never moves) fires nothing.
|
|
245
|
+
export var PUBLISHED_EVENT = 'studio:pinned-stack-published';
|
|
246
|
+
|
|
247
|
+
// The key a publish is compared on, so an unchanged stack fires nothing.
|
|
248
|
+
export function publishedKey(reading) {
|
|
249
|
+
return reading.stack + '|' + (reading.timeline ? 1 : 0) + '|' + reading.travel;
|
|
250
|
+
}
|
|
251
|
+
|
|
235
252
|
var started = false
|
|
236
253
|
|
|
237
254
|
// THE READERS OF THE STACK, found rather than declared.
|
|
@@ -477,11 +494,19 @@ export function startPinnedStack() {
|
|
|
477
494
|
}
|
|
478
495
|
}
|
|
479
496
|
|
|
497
|
+
var lastPublished = null;
|
|
480
498
|
function publishAll() {
|
|
481
499
|
var reading = readStack();
|
|
482
500
|
var pins = reading.pins;
|
|
483
501
|
write(pins, reading.stack, reading.timeline, reading.travel);
|
|
484
502
|
syncObserver(pins);
|
|
503
|
+
var key = publishedKey(reading);
|
|
504
|
+
if (key !== lastPublished) {
|
|
505
|
+
lastPublished = key;
|
|
506
|
+
document.dispatchEvent(new CustomEvent(PUBLISHED_EVENT, {
|
|
507
|
+
detail: { bottom: reading.stack, timeline: reading.timeline, travel: reading.travel }
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
485
510
|
}
|
|
486
511
|
|
|
487
512
|
// Observe whatever is pinned RIGHT NOW. observe() on an already-observed node
|
|
@@ -180,7 +180,10 @@
|
|
|
180
180
|
|
|
181
181
|
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
|
182
182
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
183
|
-
|
|
183
|
+
<%# config.sticky_table_headers, unless the caller answers for this render with
|
|
184
|
+
the sticky_table_headers local (true or false; nil defers to the config). %>
|
|
185
|
+
<% sticky_table_headers = local_assigns[:sticky_table_headers].nil? ? Studio.sticky_table_headers : local_assigns[:sticky_table_headers] %>
|
|
186
|
+
<% if sticky_table_headers %>
|
|
184
187
|
<%= stylesheet_link_tag "studio/sticky_table_header", "data-turbo-track": "reload" %>
|
|
185
188
|
<% end %>
|
|
186
189
|
<%= javascript_importmap_tags %>
|
|
@@ -253,6 +256,6 @@
|
|
|
253
256
|
day, 2026-08-15). This removes a real source of run-to-run variance; it does NOT
|
|
254
257
|
claim to be that flake's cause, and the remaining suspect there is untouched. %>
|
|
255
258
|
<%= javascript_include_tag "studio/alpine", defer: true, "data-turbo-track": "reload" %>
|
|
256
|
-
<% if
|
|
259
|
+
<% if sticky_table_headers %>
|
|
257
260
|
<%= javascript_include_tag "studio/sticky_table_header", defer: true, "data-turbo-track": "reload" %>
|
|
258
261
|
<% end %>
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
push every row down by 56px whether or not it is showing. Fixed takes it out
|
|
10
10
|
of flow: nothing moves, and the bar simply is not there until it is.
|
|
11
11
|
|
|
12
|
-
Pinned to `var(--nav-h, 0px)`
|
|
13
|
-
/
|
|
14
|
-
app that publishes nothing gets a bar at the top of the
|
|
15
|
-
a broken one.
|
|
12
|
+
Pinned to the header's bottom edge, `var(--nav-bottom, var(--nav-h, 0px))`
|
|
13
|
+
(studio/profiles/_identity_styles says why the edge and not the height). The
|
|
14
|
+
0px fallback means an app that publishes nothing gets a bar at the top of the
|
|
15
|
+
viewport rather than a broken one.
|
|
16
16
|
|
|
17
17
|
THE IDENTITY HALF IS aria-hidden, and only that half. It duplicates content
|
|
18
18
|
that is already on the page and already reachable: the full card above is the
|
|
@@ -66,13 +66,16 @@
|
|
|
66
66
|
</p>
|
|
67
67
|
</header>
|
|
68
68
|
|
|
69
|
-
<%# Section nav — sticky under the
|
|
69
|
+
<%# Section nav — sticky under the whole pinned stack (--pin-stack-bottom; then
|
|
70
|
+
--nav-bottom and --nav-h for a host on an older publisher; then 0). Not --nav-h first: that
|
|
71
|
+
is the header's HEIGHT, and chrome stacked above the header would sit over
|
|
72
|
+
the nav by its own height. A pill per
|
|
70
73
|
section; the row scrolls horizontally on narrow screens, its edges softened
|
|
71
74
|
by the .fade-edge-x primitive. Order leads with Theme (the color foundation).
|
|
72
75
|
It sits at --z-sticky, above --z-raised, so the live hold button's portal
|
|
73
76
|
bubbles scroll under it rather than over it. %>
|
|
74
77
|
<nav class="sticky -mx-4 px-4 py-3 bg-surface border-y border-subtle"
|
|
75
|
-
style="top: var(--nav-h, 0px); z-index: var(--z-sticky, 30)" aria-label="Style guide sections">
|
|
78
|
+
style="top: var(--pin-stack-bottom, var(--nav-bottom, var(--nav-h, 0px))); z-index: var(--z-sticky, 30)" aria-label="Style guide sections">
|
|
76
79
|
<div class="fade-edge-x overflow-x-auto">
|
|
77
80
|
<div class="flex items-center gap-2 min-w-max">
|
|
78
81
|
<% sections.each do |anchor, label| %>
|
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.111.
|
|
4
|
+
version: 0.111.2.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|