ovellum 0.2.0 → 0.2.2

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.
@@ -16,22 +16,45 @@
16
16
  }
17
17
  }
18
18
 
19
+ function syncThemeColor(theme) {
20
+ var meta = document.getElementById('ov-theme-color');
21
+ if (!meta) return;
22
+ var effective = theme;
23
+ if (theme === 'auto') {
24
+ effective = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
25
+ }
26
+ var next = meta.getAttribute(effective === 'dark' ? 'data-dark' : 'data-light');
27
+ if (next) meta.setAttribute('content', next);
28
+ }
29
+
19
30
  function apply(theme) {
20
31
  document.documentElement.setAttribute('data-theme', theme);
32
+ syncThemeColor(theme);
21
33
  try {
22
34
  localStorage.setItem(STORAGE_KEY, theme);
23
35
  } catch (_) {}
24
36
  }
25
37
 
26
- // Theme toggle
27
- var btn = document.querySelector('[data-ov-theme-toggle]');
28
- if (btn) {
38
+ // Theme toggle — there may be more than one instance (desktop cluster +
39
+ // mobile sheet). Wire every button; they share state via <html data-theme>.
40
+ document.querySelectorAll('[data-ov-theme-toggle]').forEach(function (btn) {
29
41
  btn.addEventListener('click', function () {
30
42
  var current = readStored();
31
43
  var next = ORDER[(ORDER.indexOf(current) + 1) % ORDER.length];
32
44
  apply(next);
33
45
  });
34
- }
46
+ });
47
+
48
+ // When the user is on 'auto' and flips their OS theme, retune the
49
+ // meta theme-color so Safari's URL bar follows the OS change.
50
+ try {
51
+ var mq = window.matchMedia('(prefers-color-scheme: dark)');
52
+ var onMqChange = function () {
53
+ if (readStored() === 'auto') syncThemeColor('auto');
54
+ };
55
+ if (mq.addEventListener) mq.addEventListener('change', onMqChange);
56
+ else if (mq.addListener) mq.addListener(onMqChange);
57
+ } catch (_) {}
35
58
 
36
59
  // Mobile menu
37
60
  var menuBtn = document.querySelector('[data-ov-menu-toggle]');
@@ -142,4 +165,92 @@
142
165
  });
143
166
  pre.appendChild(btn);
144
167
  });
168
+
169
+ // ToC scroll-spy
170
+ //
171
+ // Highlights the right-rail "On this page" link for whichever h2/h3 is
172
+ // currently the most recently-passed-the-top-of-the-viewport. Uses one
173
+ // IntersectionObserver with a tall negative rootMargin so the "active"
174
+ // line activates a moment AFTER the heading scrolls past the topbar,
175
+ // giving you visual feedback that you're inside that section's content
176
+ // — not the moment the heading first appears at the very bottom of
177
+ // the window.
178
+ (function tocSpy() {
179
+ var toc = document.querySelector('.ov-toc');
180
+ if (!toc) return;
181
+ var prose = document.querySelector('.ov-prose');
182
+ if (!prose) return;
183
+ var tocLinks = {};
184
+ toc.querySelectorAll('a[href^="#"]').forEach(function (a) {
185
+ tocLinks[decodeURIComponent(a.getAttribute('href').slice(1))] = a;
186
+ });
187
+ var ids = Object.keys(tocLinks);
188
+ if (ids.length === 0) return;
189
+
190
+ var headings = ids
191
+ .map(function (id) {
192
+ return document.getElementById(id);
193
+ })
194
+ .filter(Boolean);
195
+ if (headings.length === 0) return;
196
+
197
+ var visible = new Set();
198
+
199
+ function setActive(id) {
200
+ ids.forEach(function (i) {
201
+ var a = tocLinks[i];
202
+ if (i === id) a.classList.add('is-current');
203
+ else a.classList.remove('is-current');
204
+ });
205
+ }
206
+
207
+ function recompute() {
208
+ // Pick the LAST heading whose top is above the topbar offset — that's
209
+ // "the section you're reading". Falls back to the first heading when
210
+ // the viewport is still above all of them.
211
+ var top = 96; // ≈ topbar + a touch of breathing room
212
+ var current = headings[0];
213
+ for (var i = 0; i < headings.length; i++) {
214
+ var h = headings[i];
215
+ var rect = h.getBoundingClientRect();
216
+ if (rect.top - top <= 0) current = h;
217
+ else break;
218
+ }
219
+ setActive(current.id);
220
+ }
221
+
222
+ if ('IntersectionObserver' in window) {
223
+ var io = new IntersectionObserver(
224
+ function (entries) {
225
+ entries.forEach(function (e) {
226
+ if (e.isIntersecting) visible.add(e.target.id);
227
+ else visible.delete(e.target.id);
228
+ });
229
+ recompute();
230
+ },
231
+ { rootMargin: '-96px 0px -60% 0px', threshold: 0 },
232
+ );
233
+ headings.forEach(function (h) {
234
+ io.observe(h);
235
+ });
236
+ }
237
+
238
+ // Even with IO, recompute on scroll so a fast user-scroll doesn't
239
+ // leave the indicator stranded between intersect callbacks.
240
+ var ticking = false;
241
+ window.addEventListener(
242
+ 'scroll',
243
+ function () {
244
+ if (ticking) return;
245
+ ticking = true;
246
+ window.requestAnimationFrame(function () {
247
+ recompute();
248
+ ticking = false;
249
+ });
250
+ },
251
+ { passive: true },
252
+ );
253
+
254
+ recompute();
255
+ })();
145
256
  })();