@fluid-theme/core 0.4.3 → 2.0.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.
- package/AGENTS.md +6 -29
- package/CHANGELOG.md +543 -2
- package/CLAUDE.md +193 -14
- package/GEMINI.md +6 -45
- package/README.md +17 -8
- package/assets/js/compare.js +77 -0
- package/assets/js/fit-text.js +103 -0
- package/assets/js/lightbox.js +153 -0
- package/assets/js/scroll-reveal.js +72 -0
- package/assets/js/toc-build.mjs +86 -0
- package/assets/js/toc.js +384 -0
- package/dist/fluid-1x.min.css +4 -0
- package/dist/fluid-fonts.min.css +2 -0
- package/dist/fluid-legacy-1x.css +3 -0
- package/dist/fluid-nav.min.css +3 -0
- package/dist/fluid-toc.min.css +3 -0
- package/dist/fluid-zero.min.css +3 -0
- package/dist/fluid.min.css +3 -2
- package/dist/fonts/OFL-newsreader.txt +93 -0
- package/dist/fonts/OFL-schibsted-grotesk.txt +93 -0
- package/dist/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/dist/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/dist/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
- package/fluid-agent-kit/README.md +1 -0
- package/fluid-agent-kit/skills/accessibility.md +3 -1
- package/fluid-agent-kit/skills/css-conventions.md +22 -5
- package/fluid-agent-kit/skills/image-optimization.md +2 -0
- package/fluid-agent-kit/skills/preset-composition.md +108 -0
- package/package.json +29 -3
- package/src/animations/scroll-reveal.css +75 -193
- package/src/components/accordion.css +6 -47
- package/src/components/alert.css +47 -88
- package/src/components/avatar.css +4 -20
- package/src/components/badge.css +25 -48
- package/src/components/button.css +69 -175
- package/src/components/card.css +146 -481
- package/src/components/chip.css +46 -12
- package/src/components/compare.css +234 -0
- package/src/components/dropdown.css +10 -67
- package/src/components/empty-state.css +4 -0
- package/src/components/form.css +113 -81
- package/src/components/gallery.css +252 -0
- package/src/components/hero.css +203 -88
- package/src/components/indicator.css +23 -117
- package/src/components/loading.css +7 -29
- package/src/components/modal.css +12 -65
- package/src/components/nav-modes.css +261 -0
- package/src/components/nav.css +324 -392
- package/src/components/search-input.css +4 -2
- package/src/components/step-indicator.css +26 -27
- package/src/components/table.css +64 -171
- package/src/components/toc.css +254 -0
- package/src/components/tooltip.css +5 -46
- package/src/core/base.css +393 -547
- package/src/core/reset.css +17 -5
- package/src/core/tokens.css +426 -2276
- package/src/fluid.css +258 -17
- package/src/fonts/OFL-newsreader.txt +93 -0
- package/src/fonts/OFL-schibsted-grotesk.txt +93 -0
- package/src/fonts/fluid-fonts.css +55 -0
- package/src/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
- package/src/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/src/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
- package/src/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
- package/src/layouts/positioning.css +6 -3
- package/src/legacy-1x.css +61 -0
- package/src/sections/cta.css +33 -101
- package/src/sections/features.css +42 -219
- package/src/sections/footer.css +53 -134
- package/src/sections/stats.css +40 -153
- package/src/sections/team.css +24 -113
- package/src/sections/testimonials.css +34 -148
- package/src/zero/base.css +166 -0
- package/src/zero/tokens.css +64 -0
- package/src/zero.css +20 -0
- package/src/components/scroll-progress.css +0 -104
- package/src/components/settings-panel.css +0 -603
- package/src/components/themed-images.css +0 -229
- package/src/components/version-badge.css +0 -70
package/assets/js/toc.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOC: scroll spy and the dock for .toc (spec _docs/plans/base-v2.md §13)
|
|
3
|
+
*
|
|
4
|
+
* Progressive enhancement. The list is plain HTML written by the server
|
|
5
|
+
* (assets/js/toc-build.mjs), so it works and reads well with no JS. This
|
|
6
|
+
* script adds:
|
|
7
|
+
* - the spy: aria-current="location" on the link of the section being read:
|
|
8
|
+
* the last heading at or above the activation line. The line sits at 20%
|
|
9
|
+
* of the viewport and moves down to 95% over the last screen of scroll,
|
|
10
|
+
* so short sections at the end of a page each get their turn. Above the
|
|
11
|
+
* first heading, none. After a TOC click or a hash change the target is
|
|
12
|
+
* the current one until the reader next scrolls: a wheel, touch or
|
|
13
|
+
* scrolling key at once, and any other scroll (scrollbar, Tab, find in
|
|
14
|
+
* page, a screen reader, script) once the page has moved a quarter of a
|
|
15
|
+
* screen from where the jump settled;
|
|
16
|
+
* - the mode: data-toc-mode="dock" when data-toc has "dock", the viewport
|
|
17
|
+
* is at least 32rem tall (a zoomed or short screen gets the inline list)
|
|
18
|
+
* and either no "rail" or a viewport under 70rem; "rail" or "inline"
|
|
19
|
+
* otherwise. The fold is opened in rail and dock modes; a rail that falls
|
|
20
|
+
* back to inline on a narrow screen is folded closed;
|
|
21
|
+
* - the dock: shown (data-toc-visible) while .article__body is on screen,
|
|
22
|
+
* and whenever a link in it has keyboard focus (CSS). Row one centres
|
|
23
|
+
* the current section, row two its current subsection. Centring scrolls
|
|
24
|
+
* only the rows, never the page; it jumps under reduced motion (OS
|
|
25
|
+
* setting or data-motion="reduced"). On a phone each neighbour of the
|
|
26
|
+
* centred section is capped to the room beside it (--_toc-side), so it
|
|
27
|
+
* ends in an ellipsis on screen. A link focused by Tab is centred; a
|
|
28
|
+
* link pressed with a pointer is not (moving it under the pointer would
|
|
29
|
+
* lose the click). The dock's height is written to --_toc-dock-size on
|
|
30
|
+
* <html> for the page's scroll padding;
|
|
31
|
+
* - the rail: when its list is taller than the rail, the active entry is
|
|
32
|
+
* kept in view (the rail scrolls, never the page).
|
|
33
|
+
* - data-toc-build on an empty nav builds the list from the h2 and h3
|
|
34
|
+
* inside data-toc-source (a selector; default: the nav's <article>,
|
|
35
|
+
* else <main>), with the rules of
|
|
36
|
+
* toc-build.mjs: data-toc-skip leaves a heading out, data-toc-label
|
|
37
|
+
* gives a shorter entry, a missing id is made from the text,
|
|
38
|
+
* data-toc-depth="2" drops H3s, fewer than data-toc-min H2s (default 3)
|
|
39
|
+
* hides the nav, data-toc-label-text renames "On this page".
|
|
40
|
+
* Server-built lists are preferred.
|
|
41
|
+
*
|
|
42
|
+
* Clicks are native hash links: the URL, history and focus behave as usual.
|
|
43
|
+
*
|
|
44
|
+
* Usage:
|
|
45
|
+
* <link rel="stylesheet" href="fluid-toc.min.css">
|
|
46
|
+
* <script src="assets/js/toc.js" defer></script>
|
|
47
|
+
*
|
|
48
|
+
* Dynamic content: call window.fluidToc() after injecting a TOC or headings
|
|
49
|
+
* (idempotent; safe to call repeatedly).
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
(function () {
|
|
53
|
+
'use strict';
|
|
54
|
+
|
|
55
|
+
var WIDE = '(min-width: 70rem)';
|
|
56
|
+
var TALL = '(min-height: 32rem)';
|
|
57
|
+
var navs = [];
|
|
58
|
+
var queued = false;
|
|
59
|
+
// Where a TOC jump settled (scrollY), or null while it is still moving.
|
|
60
|
+
var pinY = null, settling = 0;
|
|
61
|
+
|
|
62
|
+
/* ---------- Builder (keep identical to toc-build.mjs) ---------- */
|
|
63
|
+
|
|
64
|
+
function esc(s) {
|
|
65
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// github-slugger's rule (what Astro uses): lower case, drop punctuation and
|
|
69
|
+
// symbols, spaces to hyphens; a repeat gets -1, -2…
|
|
70
|
+
function slug(text, used) {
|
|
71
|
+
var base = String(text).toLowerCase().replace(/[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu, '').replace(/ /g, '-');
|
|
72
|
+
var id = base, n = 0;
|
|
73
|
+
while (used[id]) id = base + '-' + (++n);
|
|
74
|
+
used[id] = true;
|
|
75
|
+
return id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tocBody(headings, opts) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
var depth = opts.depth || 3, min = opts.min == null ? 3 : opts.min;
|
|
81
|
+
var labelId = opts.labelId || 'toc-label', label = opts.label || 'On this page';
|
|
82
|
+
var tree = [], last = null, skipping = false;
|
|
83
|
+
headings.forEach(function (h) {
|
|
84
|
+
if (h.depth === 2) {
|
|
85
|
+
skipping = Boolean(h.skip);
|
|
86
|
+
if (skipping) return;
|
|
87
|
+
last = { h: h, kids: [] };
|
|
88
|
+
tree.push(last);
|
|
89
|
+
} else if (h.depth === 3 && depth >= 3 && last && !skipping && !h.skip) {
|
|
90
|
+
last.kids.push({ h: h });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
if (tree.length < min) return '';
|
|
94
|
+
function item(n) {
|
|
95
|
+
var a = '<a href="#' + esc(encodeURIComponent(n.h.id)) + '">' + esc(n.h.label || n.h.text) + '</a>';
|
|
96
|
+
return '<li>' + a + (n.kids && n.kids.length ? '<ol>' + n.kids.map(item).join('') + '</ol>' : '') + '</li>';
|
|
97
|
+
}
|
|
98
|
+
return '<details class="toc__fold" open><summary class="toc__label" id="' + esc(labelId) + '">' +
|
|
99
|
+
esc(label) + '</summary><ol>' + tree.map(item).join('') + '</ol></details>';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Headings as toc-build.mjs takes them, read from the page.
|
|
103
|
+
function readHeadings(root) {
|
|
104
|
+
var used = {};
|
|
105
|
+
Array.prototype.forEach.call(document.querySelectorAll('[id]'), function (el) { used[el.id] = true; });
|
|
106
|
+
return Array.prototype.map.call(root.querySelectorAll('h2, h3'), function (el) {
|
|
107
|
+
var text = el.textContent.replace(/\s+/g, ' ').trim();
|
|
108
|
+
if (!el.id) el.id = slug(text, used);
|
|
109
|
+
return {
|
|
110
|
+
depth: el.tagName === 'H2' ? 2 : 3,
|
|
111
|
+
id: el.id,
|
|
112
|
+
text: text,
|
|
113
|
+
label: el.getAttribute('data-toc-label') || undefined,
|
|
114
|
+
skip: el.hasAttribute('data-toc-skip') || undefined
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function build(nav) {
|
|
120
|
+
var src = nav.getAttribute('data-toc-source');
|
|
121
|
+
var root = (src ? document.querySelector(src) : nav.closest('article') || document.querySelector('main')) || document.body;
|
|
122
|
+
var labelId = nav.getAttribute('aria-labelledby') || 'toc-label';
|
|
123
|
+
var label = nav.getAttribute('data-toc-label-text');
|
|
124
|
+
var html = tocBody(readHeadings(root), {
|
|
125
|
+
depth: Number(nav.getAttribute('data-toc-depth')) || 3,
|
|
126
|
+
min: nav.hasAttribute('data-toc-min') ? Number(nav.getAttribute('data-toc-min')) : 3,
|
|
127
|
+
labelId: labelId,
|
|
128
|
+
label: label
|
|
129
|
+
});
|
|
130
|
+
nav.innerHTML = html;
|
|
131
|
+
nav.hidden = !html;
|
|
132
|
+
if (html) nav.setAttribute('aria-labelledby', labelId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ---------- Spy and dock ---------- */
|
|
136
|
+
|
|
137
|
+
function reduced(nav) {
|
|
138
|
+
return matchMedia('(prefers-reduced-motion: reduce)').matches || Boolean(nav.closest('[data-motion="reduced"]'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Scroll a row (never the page) so el's centre meets the row's centre.
|
|
142
|
+
function centre(row, el, nav, instant) {
|
|
143
|
+
if (!row || !el || !row.clientWidth) return;
|
|
144
|
+
var r = row.getBoundingClientRect(), e = el.getBoundingClientRect();
|
|
145
|
+
var dx = (e.left + e.width / 2) - (r.left + r.width / 2);
|
|
146
|
+
if (Math.abs(dx) < 1) return;
|
|
147
|
+
row.scrollBy({ left: dx, behavior: instant || reduced(nav) ? 'instant' : 'smooth' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function centreRows(t, instant) {
|
|
151
|
+
if (t.mode !== 'dock') return;
|
|
152
|
+
var cur = t.active >= 0 ? t.links[t.active] : null;
|
|
153
|
+
var rowOne = t.nav.querySelector('.toc__fold > ol');
|
|
154
|
+
var top = cur ? cur.closest('.toc__fold > ol > li') : rowOne && rowOne.firstElementChild;
|
|
155
|
+
if (!top) return;
|
|
156
|
+
// The room either side of the centred entry, less the 1.5rem fade: a
|
|
157
|
+
// neighbour capped to it (CSS, phones) ends in its ellipsis on screen.
|
|
158
|
+
var fade = parseFloat(getComputedStyle(document.documentElement).fontSize) * 1.5;
|
|
159
|
+
t.nav.style.setProperty('--_toc-side', Math.max(0, (rowOne.clientWidth - top.firstElementChild.offsetWidth) / 2 - fade) + 'px');
|
|
160
|
+
centre(rowOne, top.firstElementChild, t.nav, instant);
|
|
161
|
+
var rowTwo = top.querySelector(':scope > ol');
|
|
162
|
+
if (rowTwo && cur) centre(rowTwo, cur.parentElement.parentElement === rowTwo ? cur : rowTwo.firstElementChild.firstElementChild, t.nav, instant);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Where the spy says the reader is: an index into t.heads, or -1. Reads only.
|
|
166
|
+
function activeIndex(t) {
|
|
167
|
+
if (t.pin != null) return t.pin;
|
|
168
|
+
var h = innerHeight, left = document.documentElement.scrollHeight - (scrollY + h);
|
|
169
|
+
// Measured in the space the dock leaves, so the line never sits under it.
|
|
170
|
+
var dock = t.mode === 'dock' && t.nav.hasAttribute('data-toc-visible') ? t.nav.offsetHeight : 0;
|
|
171
|
+
var line = (h - dock) * (0.2 + 0.75 * Math.max(0, 1 - left / h));
|
|
172
|
+
var active = -1;
|
|
173
|
+
for (var i = 0; i < t.heads.length; i++) {
|
|
174
|
+
if (t.heads[i].getBoundingClientRect().top <= line) active = i; else break;
|
|
175
|
+
}
|
|
176
|
+
return active;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Writes only; true when the active link changed.
|
|
180
|
+
function apply(t, active) {
|
|
181
|
+
if (!t.links[active]) active = -1; // a stale pin after the headings changed
|
|
182
|
+
if (active === t.active) return false;
|
|
183
|
+
if (t.active >= 0 && t.links[t.active]) t.links[t.active].removeAttribute('aria-current');
|
|
184
|
+
if (active >= 0) t.links[active].setAttribute('aria-current', 'location');
|
|
185
|
+
t.active = active;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The rail keeps its active entry in view when the list is taller than it.
|
|
190
|
+
function railKeep(t) {
|
|
191
|
+
if (t.mode !== 'rail' || t.active < 0 || t.nav.scrollHeight <= t.nav.clientHeight) return;
|
|
192
|
+
var n = t.nav.getBoundingClientRect(), a = t.links[t.active].getBoundingClientRect();
|
|
193
|
+
var d = a.top < n.top ? a.top - n.top - 8 : a.bottom > n.bottom ? a.bottom - n.bottom + 8 : 0;
|
|
194
|
+
if (d) t.nav.scrollBy({ top: d, behavior: 'instant' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function dockSize(t) {
|
|
198
|
+
var root = document.documentElement.style;
|
|
199
|
+
if (t.mode === 'dock') root.setProperty('--_toc-dock-size', t.nav.offsetHeight + 'px');
|
|
200
|
+
else if (!navs.some(function (o) { return o.mode === 'dock'; })) root.removeProperty('--_toc-dock-size');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Every nav is read first, then every nav is written; the centring reads
|
|
204
|
+
// again in the next frame. No layout is forced between a write and a read.
|
|
205
|
+
function spyAll() {
|
|
206
|
+
var next = navs.map(activeIndex);
|
|
207
|
+
var changed = navs.filter(function (t, k) { return apply(t, next[k]); });
|
|
208
|
+
if (changed.length) {
|
|
209
|
+
requestAnimationFrame(function () {
|
|
210
|
+
changed.forEach(function (t) { centreRows(t, false); railKeep(t); });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function setMode(t) {
|
|
216
|
+
var words = (t.nav.getAttribute('data-toc') || '').split(/\s+/);
|
|
217
|
+
var rail = words.indexOf('rail') > -1, wide = matchMedia(WIDE).matches;
|
|
218
|
+
var dock = words.indexOf('dock') > -1 && matchMedia(TALL).matches;
|
|
219
|
+
var mode = dock && (!rail || !wide) ? 'dock' : rail && wide ? 'rail' : 'inline';
|
|
220
|
+
if (mode === t.mode) return;
|
|
221
|
+
t.mode = mode;
|
|
222
|
+
// Switch without the dock's slide, so a page never loads with the bar
|
|
223
|
+
// gliding away; data-toc-instant turns the transition off for two frames.
|
|
224
|
+
t.nav.setAttribute('data-toc-instant', '');
|
|
225
|
+
t.nav.setAttribute('data-toc-mode', mode);
|
|
226
|
+
var fold = t.nav.querySelector('.toc__fold');
|
|
227
|
+
if (fold) {
|
|
228
|
+
if (mode !== 'inline') fold.open = true;
|
|
229
|
+
else if (rail) fold.open = false;
|
|
230
|
+
}
|
|
231
|
+
requestAnimationFrame(function () {
|
|
232
|
+
centreRows(t, true);
|
|
233
|
+
dockSize(t);
|
|
234
|
+
requestAnimationFrame(function () { t.nav.removeAttribute('data-toc-instant'); });
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function onScroll() {
|
|
239
|
+
// Any scroll that moves a quarter of a screen from where a jump settled
|
|
240
|
+
// releases the pin, however it was made (scrollbar, Tab, find, script).
|
|
241
|
+
if (pinY != null && Math.abs(scrollY - pinY) > innerHeight / 4) unpin({ type: 'scroll' });
|
|
242
|
+
if (queued) return;
|
|
243
|
+
queued = true;
|
|
244
|
+
requestAnimationFrame(function () {
|
|
245
|
+
queued = false;
|
|
246
|
+
spyAll();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function onResize() {
|
|
251
|
+
navs.forEach(setMode);
|
|
252
|
+
onScroll();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function idOf(hash) {
|
|
256
|
+
var raw = String(hash).replace(/^#/, '');
|
|
257
|
+
try { return decodeURIComponent(raw); } catch (e) { return raw; }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// A TOC click or a hash change: the target is current until the reader scrolls.
|
|
261
|
+
function pin(id) {
|
|
262
|
+
navs.forEach(function (t) {
|
|
263
|
+
var i = -1;
|
|
264
|
+
for (var k = 0; k < t.heads.length; k++) if (t.heads[k].id === id) { i = k; break; }
|
|
265
|
+
t.pin = i >= 0 ? i : null;
|
|
266
|
+
});
|
|
267
|
+
settle();
|
|
268
|
+
onScroll();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Record where the jump lands: the first scrollY that holds still for
|
|
272
|
+
// three frames (a smooth scroll moves every frame), at most ~5s.
|
|
273
|
+
function settle() {
|
|
274
|
+
pinY = null;
|
|
275
|
+
var run = ++settling, last = scrollY, quiet = 0, frames = 0;
|
|
276
|
+
(function tick() {
|
|
277
|
+
requestAnimationFrame(function () {
|
|
278
|
+
if (run !== settling) return;
|
|
279
|
+
if (Math.abs(scrollY - last) < 1) quiet++; else { quiet = 0; last = scrollY; }
|
|
280
|
+
if (quiet >= 3 || ++frames > 300) pinY = scrollY; else tick();
|
|
281
|
+
});
|
|
282
|
+
})();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function unpin(e) {
|
|
286
|
+
if (e.type === 'keydown' && !/^(ArrowUp|ArrowDown|PageUp|PageDown|Home|End| )$/.test(e.key)) return;
|
|
287
|
+
var any = false;
|
|
288
|
+
navs.forEach(function (t) { if (t.pin != null) { t.pin = null; any = true; } });
|
|
289
|
+
pinY = null;
|
|
290
|
+
settling++;
|
|
291
|
+
if (any) onScroll();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function init(nav) {
|
|
295
|
+
if (nav.hasAttribute('data-toc-build') && !nav.querySelector('a')) build(nav);
|
|
296
|
+
var t = nav._fluidToc;
|
|
297
|
+
if (!t) {
|
|
298
|
+
t = nav._fluidToc = { nav: nav, active: -1, mode: null, pin: null };
|
|
299
|
+
navs.push(t);
|
|
300
|
+
// A link reached by Tab in the dock is centred, so it is never under
|
|
301
|
+
// the fade; a row-one link also brings its row two to the start. A
|
|
302
|
+
// pointer press focuses a link too (Chrome), but is skipped.
|
|
303
|
+
nav.addEventListener('focusin', function (e) {
|
|
304
|
+
if (t.mode !== 'dock' || !e.target.matches('a')) return;
|
|
305
|
+
var keyboard = true;
|
|
306
|
+
try { keyboard = e.target.matches(':focus-visible'); } catch (err) { /* old browser: assume keyboard */ }
|
|
307
|
+
if (!keyboard) return;
|
|
308
|
+
centre(e.target.closest('ol'), e.target, nav, true);
|
|
309
|
+
var sub = e.target.nextElementSibling;
|
|
310
|
+
if (sub && sub.tagName === 'OL' && sub.firstElementChild) centre(sub, sub.firstElementChild.firstElementChild, nav, true);
|
|
311
|
+
});
|
|
312
|
+
nav.addEventListener('click', function (e) {
|
|
313
|
+
var a = e.target.closest && e.target.closest('a[href^="#"]');
|
|
314
|
+
if (a) pin(idOf(a.hash));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// The dock shows while the article body is on screen. Re-observe if the
|
|
318
|
+
// body changed (the page swapped it out).
|
|
319
|
+
var body = nav.closest('.article__body') || nav.parentElement;
|
|
320
|
+
if (t.body !== body) {
|
|
321
|
+
if (t.io) t.io.disconnect();
|
|
322
|
+
t.body = body;
|
|
323
|
+
if ('IntersectionObserver' in window) {
|
|
324
|
+
t.io = new IntersectionObserver(function (entries) {
|
|
325
|
+
var on = entries[entries.length - 1].isIntersecting;
|
|
326
|
+
nav.toggleAttribute('data-toc-visible', on);
|
|
327
|
+
if (on) centreRows(t, true);
|
|
328
|
+
});
|
|
329
|
+
t.io.observe(body);
|
|
330
|
+
} else {
|
|
331
|
+
nav.setAttribute('data-toc-visible', '');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// The nav's own links decide the headings it follows.
|
|
335
|
+
t.links = [];
|
|
336
|
+
t.heads = [];
|
|
337
|
+
Array.prototype.forEach.call(nav.querySelectorAll('a[href^="#"]'), function (a) {
|
|
338
|
+
var h = document.getElementById(idOf(a.hash));
|
|
339
|
+
if (h) { t.links.push(a); t.heads.push(h); }
|
|
340
|
+
});
|
|
341
|
+
t.active = -1;
|
|
342
|
+
t.pin = null; // fluidToc() pins location.hash again after init
|
|
343
|
+
Array.prototype.forEach.call(nav.querySelectorAll('[aria-current]'), function (a) { a.removeAttribute('aria-current'); });
|
|
344
|
+
t.mode = null;
|
|
345
|
+
setMode(t);
|
|
346
|
+
apply(t, activeIndex(t));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
var listening = false;
|
|
350
|
+
|
|
351
|
+
function fluidToc() {
|
|
352
|
+
// Drop navs that left the page (an SPA or view-transition swap), with
|
|
353
|
+
// their observers, so detached headings are never read again.
|
|
354
|
+
navs = navs.filter(function (t) {
|
|
355
|
+
if (t.nav.isConnected) return true;
|
|
356
|
+
if (t.io) t.io.disconnect();
|
|
357
|
+
delete t.nav._fluidToc;
|
|
358
|
+
return false;
|
|
359
|
+
});
|
|
360
|
+
Array.prototype.forEach.call(document.querySelectorAll('nav.toc'), init);
|
|
361
|
+
if (!listening) {
|
|
362
|
+
listening = true;
|
|
363
|
+
addEventListener('scroll', onScroll, { passive: true });
|
|
364
|
+
addEventListener('resize', onResize, { passive: true });
|
|
365
|
+
addEventListener('hashchange', function () { pin(idOf(location.hash)); });
|
|
366
|
+
['wheel', 'touchstart', 'keydown'].forEach(function (ev) { addEventListener(ev, unpin, { passive: true }); });
|
|
367
|
+
[WIDE, TALL].forEach(function (q) {
|
|
368
|
+
var mq = matchMedia(q);
|
|
369
|
+
if (mq.addEventListener) mq.addEventListener('change', onResize);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (location.hash) pin(idOf(location.hash));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
window.fluidToc = fluidToc;
|
|
376
|
+
// Exposed for toc-check.mjs, which proves this builder and toc-build.mjs agree.
|
|
377
|
+
window.fluidToc.html = tocBody;
|
|
378
|
+
|
|
379
|
+
if (document.readyState === 'loading') {
|
|
380
|
+
document.addEventListener('DOMContentLoaded', fluidToc);
|
|
381
|
+
} else {
|
|
382
|
+
fluidToc();
|
|
383
|
+
}
|
|
384
|
+
})();
|