sdocs 0.0.59 → 0.0.60
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/CHANGELOG.md +9 -0
- package/dist/explorer/views/PageView.svelte +67 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.60] - 2026-07-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The page table of contents highlights the current section.** As the
|
|
15
|
+
page scrolls, the "On this page" entry for the section in view lights up;
|
|
16
|
+
clicking an entry highlights it immediately and holds it through the
|
|
17
|
+
smooth scroll.
|
|
18
|
+
|
|
10
19
|
## [0.0.59] - 2026-07-05
|
|
11
20
|
|
|
12
21
|
### Changed
|
|
@@ -37,7 +37,69 @@
|
|
|
37
37
|
};
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
// Scrollspy: the TOC highlights the section currently in view. The active
|
|
41
|
+
// heading is the last one above the threshold line near the viewport top.
|
|
42
|
+
// The scroll listener is capturing because the scrolling element is an
|
|
43
|
+
// ancestor container, not the window.
|
|
44
|
+
const SPY_OFFSET = 120;
|
|
45
|
+
let activeId = $state('');
|
|
46
|
+
let spyLockUntil = 0;
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
if (!container || toc.length === 0) return;
|
|
50
|
+
void PageComponent; // re-run once the page body has mounted
|
|
51
|
+
const ids = toc.map((h) => h.id);
|
|
52
|
+
let frame = 0;
|
|
53
|
+
const update = () => {
|
|
54
|
+
frame = 0;
|
|
55
|
+
if (performance.now() < spyLockUntil) return;
|
|
56
|
+
// At the bottom the last sections can never reach the threshold
|
|
57
|
+
// line — snap to the final entry so it is reachable at all.
|
|
58
|
+
let scroller: HTMLElement | null = container ?? null;
|
|
59
|
+
while (scroller) {
|
|
60
|
+
const o = getComputedStyle(scroller).overflowY;
|
|
61
|
+
if (
|
|
62
|
+
scroller.scrollHeight > scroller.clientHeight + 1 &&
|
|
63
|
+
(o === 'auto' || o === 'scroll')
|
|
64
|
+
) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
scroller = scroller.parentElement;
|
|
68
|
+
}
|
|
69
|
+
const atBottom = scroller
|
|
70
|
+
? scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2
|
|
71
|
+
: window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
72
|
+
if (atBottom) {
|
|
73
|
+
activeId = ids[ids.length - 1];
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let current = ids[0] ?? '';
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const el = container?.querySelector(`#${CSS.escape(id)}`);
|
|
79
|
+
if (!el) continue;
|
|
80
|
+
if (el.getBoundingClientRect().top > SPY_OFFSET) break;
|
|
81
|
+
current = id;
|
|
82
|
+
}
|
|
83
|
+
activeId = current;
|
|
84
|
+
};
|
|
85
|
+
const onScroll = () => {
|
|
86
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
87
|
+
};
|
|
88
|
+
update();
|
|
89
|
+
document.addEventListener('scroll', onScroll, { capture: true, passive: true });
|
|
90
|
+
window.addEventListener('resize', onScroll);
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener('scroll', onScroll, { capture: true });
|
|
93
|
+
window.removeEventListener('resize', onScroll);
|
|
94
|
+
if (frame) cancelAnimationFrame(frame);
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
40
98
|
function scrollToHeading(id: string) {
|
|
99
|
+
// Highlight the target right away and hold it while the smooth scroll
|
|
100
|
+
// passes intermediate sections.
|
|
101
|
+
activeId = id;
|
|
102
|
+
spyLockUntil = performance.now() + 800;
|
|
41
103
|
container?.querySelector(`#${CSS.escape(id)}`)?.scrollIntoView({ behavior: 'smooth' });
|
|
42
104
|
}
|
|
43
105
|
|
|
@@ -102,6 +164,8 @@
|
|
|
102
164
|
<li class="sdocs-toc-item" style:padding-left="{(heading.level - 2) * 12}px">
|
|
103
165
|
<button
|
|
104
166
|
class="sdocs-toc-link"
|
|
167
|
+
class:is-active={heading.id === activeId}
|
|
168
|
+
aria-current={heading.id === activeId ? 'true' : undefined}
|
|
105
169
|
onclick={() => scrollToHeading(heading.id)}
|
|
106
170
|
>
|
|
107
171
|
{heading.text}
|
|
@@ -372,8 +436,10 @@
|
|
|
372
436
|
cursor: pointer;
|
|
373
437
|
text-align: left;
|
|
374
438
|
width: 100%;
|
|
439
|
+
transition: color 0.15s;
|
|
375
440
|
}
|
|
376
|
-
.sdocs-toc-link:hover
|
|
441
|
+
.sdocs-toc-link:hover,
|
|
442
|
+
.sdocs-toc-link.is-active {
|
|
377
443
|
color: var(--color-action-500);
|
|
378
444
|
}
|
|
379
445
|
</style>
|