sdocs 0.0.58 → 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
CHANGED
|
@@ -7,6 +7,32 @@ 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
|
+
|
|
19
|
+
## [0.0.59] - 2026-07-05
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Code blocks render on a dark stage.** Highlighted code — page fences and
|
|
24
|
+
the code panels — always uses the dark syntax theme, so blocks read as
|
|
25
|
+
code at a glance in both app themes (they previously rendered white-on-white
|
|
26
|
+
in light mode).
|
|
27
|
+
- **A page's body `#` title matches the entity title size** (24px/700) — it
|
|
28
|
+
is the page title, after all.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Every fence gets a themed block.** `bash`, `json`, `markdown`, `yaml`,
|
|
33
|
+
and `diff` fences highlight properly, and unknown languages fall back to a
|
|
34
|
+
plaintext shiki block instead of an unstyled bare `<pre>`.
|
|
35
|
+
|
|
10
36
|
## [0.0.58] - 2026-07-05
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -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}
|
|
@@ -162,7 +226,11 @@
|
|
|
162
226
|
margin: 1.6em 0 0.5em;
|
|
163
227
|
scroll-margin-top: 16px;
|
|
164
228
|
}
|
|
165
|
-
|
|
229
|
+
/* The body h1 serves as the page title — match .sdocs-view-title */
|
|
230
|
+
.sdocs-page-content :global(h1) {
|
|
231
|
+
font-size: 24px;
|
|
232
|
+
font-weight: 700;
|
|
233
|
+
}
|
|
166
234
|
.sdocs-page-content :global(h2) { font-size: 18px; }
|
|
167
235
|
.sdocs-page-content :global(h3) { font-size: 15px; }
|
|
168
236
|
.sdocs-page-content :global(h4),
|
|
@@ -368,8 +436,10 @@
|
|
|
368
436
|
cursor: pointer;
|
|
369
437
|
text-align: left;
|
|
370
438
|
width: 100%;
|
|
439
|
+
transition: color 0.15s;
|
|
371
440
|
}
|
|
372
|
-
.sdocs-toc-link:hover
|
|
441
|
+
.sdocs-toc-link:hover,
|
|
442
|
+
.sdocs-toc-link.is-active {
|
|
373
443
|
color: var(--color-action-500);
|
|
374
444
|
}
|
|
375
445
|
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** Highlight source code and return HTML
|
|
1
|
+
/** Highlight source code and return HTML. Unknown languages render as
|
|
2
|
+
* plaintext, so a fence is always a themed shiki block, never a bare pre. */
|
|
2
3
|
export declare function highlight(code: string, lang?: string): Promise<string>;
|
|
3
4
|
/** Dispose the highlighter (for cleanup) */
|
|
4
5
|
export declare function disposeHighlighter(): Promise<void>;
|
|
@@ -5,16 +5,21 @@ async function getHighlighter() {
|
|
|
5
5
|
if (!highlighterPromise) {
|
|
6
6
|
highlighterPromise = createHighlighter({
|
|
7
7
|
themes: ['github-light', 'github-dark'],
|
|
8
|
-
langs: [
|
|
8
|
+
langs: [
|
|
9
|
+
'svelte', 'typescript', 'javascript', 'css', 'html',
|
|
10
|
+
'bash', 'json', 'markdown', 'yaml', 'diff',
|
|
11
|
+
],
|
|
9
12
|
});
|
|
10
13
|
}
|
|
11
14
|
return highlighterPromise;
|
|
12
15
|
}
|
|
13
|
-
/** Highlight source code and return HTML
|
|
16
|
+
/** Highlight source code and return HTML. Unknown languages render as
|
|
17
|
+
* plaintext, so a fence is always a themed shiki block, never a bare pre. */
|
|
14
18
|
export async function highlight(code, lang = 'svelte') {
|
|
15
19
|
const highlighter = await getHighlighter();
|
|
20
|
+
const resolved = highlighter.getLoadedLanguages().includes(lang) ? lang : 'text';
|
|
16
21
|
return highlighter.codeToHtml(code, {
|
|
17
|
-
lang,
|
|
22
|
+
lang: resolved,
|
|
18
23
|
themes: {
|
|
19
24
|
light: 'github-light',
|
|
20
25
|
dark: 'github-dark',
|
package/dist/ui/styles/sdocs.css
CHANGED
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
text-decoration: none;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Highlighted code blocks always render shiki's dark theme — a dark stage
|
|
32
|
+
* for code in both app themes (the inline light-theme colors lose to the
|
|
33
|
+
* dual-theme --shiki-dark variables). */
|
|
34
|
+
.sdocs-app pre.shiki,
|
|
35
|
+
.sdocs-app pre.shiki span {
|
|
36
|
+
color: var(--shiki-dark) !important;
|
|
37
|
+
background-color: var(--shiki-dark-bg) !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
/* Type chips and value literals, color-coded by kind (shared by doc tables) */
|
|
32
41
|
.sdocs-app code[class*='sdocs-type-'] {
|
|
33
42
|
font-family: var(--mono);
|