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
- .sdocs-page-content :global(h1) { font-size: 22px; }
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: ['svelte', 'typescript', 'javascript', 'css', 'html'],
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',
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",