svelte-docsmith 0.7.0 → 0.9.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.
Files changed (61) hide show
  1. package/dist/buildtime/changelog.d.ts +20 -0
  2. package/dist/buildtime/changelog.js +74 -0
  3. package/dist/buildtime/fence-meta.d.ts +20 -0
  4. package/dist/buildtime/fence-meta.js +48 -0
  5. package/dist/buildtime/markdown-layout.svelte.d.ts +3 -0
  6. package/dist/buildtime/preprocess.d.ts +13 -0
  7. package/dist/buildtime/preprocess.js +77 -17
  8. package/dist/buildtime/twoslash.d.ts +26 -0
  9. package/dist/buildtime/twoslash.js +40 -0
  10. package/dist/buildtime/vite/collect.js +31 -3
  11. package/dist/buildtime/vite/git.d.ts +7 -0
  12. package/dist/buildtime/vite/git.js +32 -0
  13. package/dist/buildtime/vite/index.d.ts +13 -0
  14. package/dist/buildtime/vite/index.js +16 -0
  15. package/dist/buildtime/vite/releases.d.ts +12 -0
  16. package/dist/buildtime/vite/releases.js +57 -0
  17. package/dist/components/changelog/changelog-entry.svelte +90 -0
  18. package/dist/components/changelog/changelog-entry.svelte.d.ts +7 -0
  19. package/dist/components/changelog/changelog.svelte +56 -0
  20. package/dist/components/changelog/changelog.svelte.d.ts +14 -0
  21. package/dist/components/chrome/announcement-bar.svelte +188 -0
  22. package/dist/components/chrome/announcement-bar.svelte.d.ts +7 -0
  23. package/dist/components/docs/mermaid.svelte +213 -0
  24. package/dist/components/docs/mermaid.svelte.d.ts +6 -0
  25. package/dist/components/docs/tabs-sync.svelte.d.ts +10 -0
  26. package/dist/components/docs/tabs-sync.svelte.js +59 -0
  27. package/dist/components/docs/tabs.svelte +42 -6
  28. package/dist/components/docs/tabs.svelte.d.ts +6 -0
  29. package/dist/components/landing/action.svelte +50 -0
  30. package/dist/components/landing/action.svelte.d.ts +13 -0
  31. package/dist/components/landing/cta.svelte +52 -0
  32. package/dist/components/landing/cta.svelte.d.ts +13 -0
  33. package/dist/components/landing/feature-grid.svelte +51 -0
  34. package/dist/components/landing/feature-grid.svelte.d.ts +19 -0
  35. package/dist/components/landing/feature.svelte +33 -0
  36. package/dist/components/landing/feature.svelte.d.ts +11 -0
  37. package/dist/components/landing/hero.svelte +70 -0
  38. package/dist/components/landing/hero.svelte.d.ts +19 -0
  39. package/dist/components/layouts/docs-header.svelte +36 -3
  40. package/dist/components/layouts/docs-shell.svelte +14 -8
  41. package/dist/components/layouts/docs-sidebar.svelte +52 -18
  42. package/dist/components/layouts/docs-sidebar.svelte.d.ts +1 -1
  43. package/dist/components/markdown/pre.svelte +238 -8
  44. package/dist/components/markdown/pre.svelte.d.ts +6 -0
  45. package/dist/core/changelog.d.ts +31 -0
  46. package/dist/core/changelog.js +7 -0
  47. package/dist/core/config.d.ts +21 -0
  48. package/dist/core/config.js +19 -0
  49. package/dist/core/content.d.ts +6 -1
  50. package/dist/core/index.d.ts +2 -2
  51. package/dist/core/index.js +1 -1
  52. package/dist/core/nav.d.ts +23 -6
  53. package/dist/core/nav.js +94 -15
  54. package/dist/fallbacks/changelog.d.ts +10 -0
  55. package/dist/fallbacks/changelog.js +3 -0
  56. package/dist/generate/feed.d.ts +35 -0
  57. package/dist/generate/feed.js +80 -0
  58. package/dist/index.d.ts +8 -0
  59. package/dist/index.js +9 -0
  60. package/dist/theme.css +32 -0
  61. package/package.json +21 -2
@@ -6,18 +6,102 @@
6
6
 
7
7
  const clipboard = useClipboard();
8
8
 
9
- const { children }: { children: Snippet } = $props();
9
+ const {
10
+ title,
11
+ lineNumbers = false,
12
+ startLine = 1,
13
+ children
14
+ }: {
15
+ /** Filename or caption, from the fence's `title=` metadata. */
16
+ title?: string;
17
+ /** Number the lines, from `showLineNumbers`. */
18
+ lineNumbers?: boolean;
19
+ /** First line's number, from `startLine=`. */
20
+ startLine?: number;
21
+ children: Snippet;
22
+ } = $props();
23
+
24
+ /**
25
+ * Twoslash popups are absolutely positioned inside the code block, which is a
26
+ * scroll container, so anything reaching past its edges is clipped: a hover
27
+ * near the last line shows a sliver of the type and nothing else. CSS can't
28
+ * fix this, because a horizontally scrolling element must also clip
29
+ * vertically. So on hover the popup is promoted to fixed positioning, whose
30
+ * containing block is the viewport rather than the scroller.
31
+ *
32
+ * Without JavaScript the popup still opens on hover, clipped as before, so
33
+ * this only ever improves on the CSS-only behaviour.
34
+ */
35
+ function unclipPopups(node: HTMLElement) {
36
+ const GAP = 6;
37
+
38
+ const place = (event: Event) => {
39
+ const target = event.target as HTMLElement | null;
40
+ const trigger = target?.closest?.('.twoslash-hover') as HTMLElement | null;
41
+ if (!trigger) return;
42
+ const popup = trigger.querySelector('.twoslash-popup-container') as HTMLElement | null;
43
+ if (!popup) return;
44
+
45
+ // Measure while visible but before committing a position, so the popup's
46
+ // own width decides whether it flips to stay on screen.
47
+ popup.style.position = 'fixed';
48
+ popup.style.left = '0px';
49
+ popup.style.top = '0px';
50
+ const rect = trigger.getBoundingClientRect();
51
+ const { width, height } = popup.getBoundingClientRect();
52
+
53
+ const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
54
+ // Below the token unless that would run off the bottom, then above it.
55
+ const below = rect.bottom + GAP;
56
+ const top = below + height > window.innerHeight - 8 ? rect.top - height - GAP : below;
57
+
58
+ popup.style.left = `${left}px`;
59
+ popup.style.top = `${Math.max(8, top)}px`;
60
+ };
61
+
62
+ const reset = (event: Event) => {
63
+ const trigger = (event.target as HTMLElement | null)?.closest?.(
64
+ '.twoslash-hover'
65
+ ) as HTMLElement | null;
66
+ const popup = trigger?.querySelector('.twoslash-popup-container') as HTMLElement | null;
67
+ if (popup) popup.removeAttribute('style');
68
+ };
69
+
70
+ node.addEventListener('mouseover', place);
71
+ node.addEventListener('mouseout', reset);
72
+ return {
73
+ destroy() {
74
+ node.removeEventListener('mouseover', place);
75
+ node.removeEventListener('mouseout', reset);
76
+ }
77
+ };
78
+ }
10
79
  </script>
11
80
 
12
- <div class="rounded-md overflow-hidden bg-muted mt-2 shadow-md text-sm relative">
81
+ <div class="rounded-md overflow-hidden bg-muted mt-2 shadow-md text-sm relative" use:unclipPopups>
82
+ {#if title}
83
+ <!-- The filename bar doubles as the copy button's row, so the button stops
84
+ floating over the first line of code when a title is present. -->
85
+ <div class="docsmith-code-title">
86
+ <span class="docsmith-code-title-text">{title}</span>
87
+ <CopyButton copied={clipboard.copied} onclick={() => clipboard.copy()} class="bg-muted" />
88
+ </div>
89
+ {/if}
90
+
13
91
  <ScrollArea orientation="both" class="relative">
14
- <CopyButton
15
- copied={clipboard.copied}
16
- onclick={() => clipboard.copy()}
17
- class="bg-muted absolute top-2 right-2 z-10"
18
- />
92
+ {#if !title}
93
+ <CopyButton
94
+ copied={clipboard.copied}
95
+ onclick={() => clipboard.copy()}
96
+ class="bg-muted absolute top-2 right-2 z-10"
97
+ />
98
+ {/if}
19
99
 
20
- <pre class="not-prose shiki max-h-[32rem] flex shrink" use:clipboard.readText>
100
+ <pre
101
+ class="not-prose shiki max-h-[32rem] flex shrink"
102
+ class:docsmith-line-numbers={lineNumbers}
103
+ style={lineNumbers && startLine !== 1 ? `--docsmith-line-start: ${startLine - 1}` : undefined}
104
+ use:clipboard.readText>
21
105
  {@render children()}
22
106
  </pre>
23
107
  </ScrollArea>
@@ -44,6 +128,64 @@
44
128
  padding-inline: 1rem;
45
129
  }
46
130
 
131
+ /* --- Filename bar --- */
132
+ .docsmith-code-title {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ gap: 0.75rem;
137
+ padding: 0.4rem 0.5rem 0.4rem 1rem;
138
+ border-bottom: 1px solid var(--border);
139
+ background: color-mix(in oklch, var(--muted) 60%, var(--background));
140
+ }
141
+ .docsmith-code-title-text {
142
+ font-family: var(--font-mono, ui-monospace, monospace);
143
+ font-size: 0.78rem;
144
+ color: var(--muted-foreground);
145
+ overflow-wrap: anywhere;
146
+ }
147
+
148
+ /* --- Line numbers ---
149
+ A CSS counter on Shiki's existing per-line spans, so numbering adds no DOM
150
+ and stays out of the text the copy button reads. `--docsmith-line-start`
151
+ offsets the counter for a snippet lifted out of a larger file. */
152
+ :global(pre.docsmith-line-numbers code) {
153
+ counter-reset: docsmith-line var(--docsmith-line-start, 0);
154
+ }
155
+ :global(pre.docsmith-line-numbers span.line) {
156
+ counter-increment: docsmith-line;
157
+ position: relative;
158
+ padding-inline-start: 3.2rem;
159
+ }
160
+ :global(pre.docsmith-line-numbers span.line::before) {
161
+ content: counter(docsmith-line);
162
+ position: absolute;
163
+ left: 0;
164
+ width: 2.4rem;
165
+ text-align: right;
166
+ color: color-mix(in oklch, var(--muted-foreground) 65%, transparent);
167
+ /* Numbers are decoration, not content: keep them out of selections and
168
+ off the clipboard. */
169
+ user-select: none;
170
+ }
171
+ /* The diff gutter glyph would collide with the number, so shift it over. */
172
+ :global(pre.docsmith-line-numbers span.line.diff::before) {
173
+ content: counter(docsmith-line);
174
+ }
175
+ :global(pre.docsmith-line-numbers span.line.diff::after) {
176
+ position: absolute;
177
+ left: 2.7rem;
178
+ font-weight: 600;
179
+ }
180
+ :global(pre.docsmith-line-numbers span.line.diff.add::after) {
181
+ content: '+';
182
+ color: oklch(0.6 0.15 150);
183
+ }
184
+ :global(pre.docsmith-line-numbers span.line.diff.remove::after) {
185
+ content: '−';
186
+ color: oklch(0.58 0.19 25);
187
+ }
188
+
47
189
  /* Two tiers of annotation. EMPHASIS (highlight, word) tints with the theme's
48
190
  --primary, so it reads as the brand pointing at a line and recolors with
49
191
  the active theme. SEMANTIC (diff, error, warning) uses fixed OKLCH hues so
@@ -125,6 +267,94 @@
125
267
  }
126
268
  }
127
269
 
270
+ /* --- Twoslash hovers ---
271
+ Restyled from @shikijs/twoslash's rich theme onto our tokens so the popup
272
+ belongs to the site in both colour schemes. Underline marks a token as
273
+ inspectable; the popup is a hidden sibling revealed on hover. `unclipPopups`
274
+ then promotes it to fixed positioning so the code block's scrolling does not
275
+ clip it — see the comment there. */
276
+ :global(pre .twoslash-hover) {
277
+ position: relative;
278
+ border-bottom: 1px dotted color-mix(in oklch, var(--muted-foreground) 55%, transparent);
279
+ }
280
+ :global(pre .twoslash-popup-container) {
281
+ position: absolute;
282
+ z-index: 40;
283
+ display: none;
284
+ left: 0;
285
+ top: 1.6em;
286
+ /* Shrink to the type's own width so a short signature isn't a wide slab;
287
+ the script measures this before choosing a position. */
288
+ width: max-content;
289
+ max-width: min(32rem, 78vw);
290
+ padding: 0.5rem 0.7rem;
291
+ border: 1px solid var(--border);
292
+ border-radius: var(--radius, 0.5rem);
293
+ background: var(--popover, var(--card));
294
+ color: var(--popover-foreground, var(--foreground));
295
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.18);
296
+ white-space: pre-wrap;
297
+ text-align: left;
298
+ font-size: 0.85em;
299
+ line-height: 1.5;
300
+ }
301
+ :global(pre .twoslash-hover:hover .twoslash-popup-container) {
302
+ display: block;
303
+ }
304
+ /* The signature keeps Shiki's own colours and monospace. */
305
+ :global(pre .twoslash-popup-code) {
306
+ display: block;
307
+ font-family: var(--font-mono, ui-monospace, monospace);
308
+ white-space: pre-wrap;
309
+ }
310
+
311
+ /* Prose, by contrast, is prose. Inside a <pre> it would otherwise inherit
312
+ monospace and whatever token colour the hovered span happened to carry,
313
+ which reads as more code rather than an explanation of it. */
314
+ :global(pre .twoslash-popup-docs) {
315
+ /* The dark-theme swap repaints every span inside `.shiki` with
316
+ `--shiki-dark`, which these spans inherit from whichever token was
317
+ hovered — so docs took on the colour of the word above them. Redefining
318
+ the variable here resolves it to prose colours instead of fighting an
319
+ `!important` rule. */
320
+ --shiki-dark: var(--muted-foreground);
321
+ margin-top: 0.5rem;
322
+ padding-top: 0.5rem;
323
+ border-top: 1px solid var(--border);
324
+ font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
325
+ font-size: 0.92em;
326
+ line-height: 1.55;
327
+ color: var(--muted-foreground);
328
+ white-space: normal;
329
+ }
330
+
331
+ /* Each @param is its own line: the tags are inline spans, so without this
332
+ they run together into one paragraph. */
333
+ :global(pre .twoslash-popup-docs-tags) {
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: 0.35rem;
337
+ }
338
+ :global(pre .twoslash-popup-docs-tag) {
339
+ display: block;
340
+ }
341
+ :global(pre .twoslash-popup-docs-tag-value) {
342
+ --shiki-dark: var(--muted-foreground);
343
+ color: var(--muted-foreground);
344
+ }
345
+ /* And the tag name needs a gap, or it reads as "@paramcallbackfn". */
346
+ :global(pre .twoslash-popup-docs-tag-name) {
347
+ --shiki-dark: var(--primary);
348
+ margin-right: 0.4em;
349
+ font-family: var(--font-mono, ui-monospace, monospace);
350
+ font-size: 0.92em;
351
+ color: var(--primary);
352
+ }
353
+ /* A snippet that deliberately shows an error (// @errors:) marks the line. */
354
+ :global(pre .twoslash-error) {
355
+ background: color-mix(in oklch, oklch(0.62 0.2 25) 14%, transparent);
356
+ }
357
+
128
358
  /* --- Word highlight (// [!code word:name]): a primary-tinted inline pill --- */
129
359
  :global(pre .highlighted-word) {
130
360
  border-radius: 0.3rem;
@@ -1,5 +1,11 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
+ /** Filename or caption, from the fence's `title=` metadata. */
4
+ title?: string;
5
+ /** Number the lines, from `showLineNumbers`. */
6
+ lineNumbers?: boolean;
7
+ /** First line's number, from `startLine=`. */
8
+ startLine?: number;
3
9
  children: Snippet;
4
10
  };
5
11
  declare const Pre: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The record types for the generated changelog index. The `docsmith()` Vite
3
+ * plugin parses your package's `CHANGELOG.md` at build time and emits these as
4
+ * the `svelte-docsmith/changelog` virtual module, so every release becomes a
5
+ * page and a feed entry without being written twice.
6
+ */
7
+ /** One group of changes within a release, e.g. "Minor Changes". */
8
+ export type ChangelogGroup = {
9
+ /** The group heading as written, e.g. `Minor Changes` or `Patch Changes`. */
10
+ kind: string;
11
+ /**
12
+ * Each entry's body as HTML, rendered from markdown at build time with the
13
+ * changeset's commit hash removed. HTML rather than markdown so a consuming
14
+ * site needs no runtime renderer.
15
+ */
16
+ items: string[];
17
+ };
18
+ /** One released version. */
19
+ export type ChangelogRelease = {
20
+ /** Version string as it appears in the changelog, e.g. `0.8.0`. */
21
+ version: string;
22
+ /** Release date (ISO) from the git history, when it can be determined. */
23
+ date?: string;
24
+ groups: ChangelogGroup[];
25
+ /**
26
+ * A hand-written page for this release, when `src/routes/changelog/<version>/`
27
+ * exists. The generated entry is the fallback, so a release that deserves a
28
+ * proper write-up can have one without the rest going undocumented.
29
+ */
30
+ path?: string;
31
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The record types for the generated changelog index. The `docsmith()` Vite
3
+ * plugin parses your package's `CHANGELOG.md` at build time and emits these as
4
+ * the `svelte-docsmith/changelog` virtual module, so every release becomes a
5
+ * page and a feed entry without being written twice.
6
+ */
7
+ export {};
@@ -14,6 +14,25 @@ export type DocsmithFooterColumn = {
14
14
  title: string;
15
15
  links: DocsmithLink[];
16
16
  };
17
+ /** A thin announcement bar shown at the top of every page. */
18
+ export type DocsmithAnnouncement = {
19
+ /** The message text. */
20
+ text: string;
21
+ /** Optional leading pill label, e.g. `'New'`, `'Beta'`, or a version. */
22
+ tag?: string;
23
+ /** Optional link the bar points to. */
24
+ href?: string;
25
+ /** Open the link in a new tab with `rel="noopener"`. */
26
+ external?: boolean;
27
+ /**
28
+ * Dismissal key. A dismissed bar stays dismissed until this value changes, so
29
+ * bump it (or edit `text`) when you post a new announcement. Defaults to the
30
+ * text itself.
31
+ */
32
+ id?: string;
33
+ /** Let readers dismiss the bar. Defaults to `true`. */
34
+ dismissible?: boolean;
35
+ };
17
36
  export type DocsmithConfig = {
18
37
  /** Site title, shown in the header/sidebar and in the `<title>` suffix. */
19
38
  title: string;
@@ -42,6 +61,8 @@ export type DocsmithConfig = {
42
61
  version?: string;
43
62
  /** Optional logo image src; falls back to the built-in book mark. */
44
63
  logo?: string;
64
+ /** Optional announcement bar shown at the top of every page. */
65
+ announcement?: DocsmithAnnouncement;
45
66
  /** Top-level header navigation links. */
46
67
  nav?: DocsmithLink[];
47
68
  /** Footer content, driven by data. */
@@ -23,6 +23,25 @@ export function defineConfig(config) {
23
23
  throw new Error(`[svelte-docsmith] config.${key} must be a string when set.`);
24
24
  }
25
25
  }
26
+ if (config.announcement !== undefined) {
27
+ const a = config.announcement;
28
+ if (typeof a !== 'object' || a === null) {
29
+ throw new Error('[svelte-docsmith] config.announcement must be an object ({ text, href?, id?, external?, dismissible? }).');
30
+ }
31
+ if (typeof a.text !== 'string' || a.text.trim() === '') {
32
+ throw new Error('[svelte-docsmith] config.announcement.text is required and must be a string.');
33
+ }
34
+ for (const key of ['tag', 'href', 'id']) {
35
+ if (a[key] !== undefined && typeof a[key] !== 'string') {
36
+ throw new Error(`[svelte-docsmith] config.announcement.${key} must be a string when set.`);
37
+ }
38
+ }
39
+ for (const key of ['external', 'dismissible']) {
40
+ if (a[key] !== undefined && typeof a[key] !== 'boolean') {
41
+ throw new Error(`[svelte-docsmith] config.announcement.${key} must be a boolean when set.`);
42
+ }
43
+ }
44
+ }
26
45
  if (config.nav !== undefined) {
27
46
  if (!Array.isArray(config.nav)) {
28
47
  throw new Error('[svelte-docsmith] config.nav must be an array of { label, href } links.');
@@ -13,7 +13,12 @@
13
13
  export type DocsContentItem = {
14
14
  title: string;
15
15
  path: string;
16
- section?: string;
16
+ /**
17
+ * Sidebar group. A string places the page in a single top-level group; an
18
+ * array names a nested group path (e.g. `['Guides', 'Advanced']`), building
19
+ * collapsible subsections. Omitted pages fall under "Docs".
20
+ */
21
+ section?: string | string[];
17
22
  order?: number;
18
23
  description?: string;
19
24
  /** Source file path relative to the app cwd, for the "Edit this page" link. */
@@ -1,3 +1,3 @@
1
- export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn } from './config.js';
1
+ export { defineConfig, type DocsmithConfig, type DocsmithLink, type DocsmithFooterColumn, type DocsmithAnnouncement } from './config.js';
2
2
  export type { DocsContentItem, SearchDoc, LlmsDoc } from './content.js';
3
- export { navFromContent, type NavItem, type NavGroup } from './nav.js';
3
+ export { navFromContent, flattenNav, navTrail, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
@@ -3,4 +3,4 @@
3
3
  // several of these (e.g. the shell) import from here; the public surface is
4
4
  // re-exported through `src/lib/index.ts`.
5
5
  export { defineConfig } from './config.js';
6
- export { navFromContent } from './nav.js';
6
+ export { navFromContent, flattenNav, navTrail, isNavGroup } from './nav.js';
@@ -1,17 +1,34 @@
1
1
  import type { DocsContentItem } from './content.js';
2
- /** A single sidebar link. */
2
+ /** A single sidebar link (a leaf). */
3
3
  export type NavItem = {
4
4
  title: string;
5
5
  url: string;
6
6
  };
7
- /** A titled group of sidebar links. */
7
+ /** A titled group of sidebar entries, which may themselves be nested groups. */
8
8
  export type NavGroup = {
9
9
  title: string;
10
- items: NavItem[];
10
+ items: NavNode[];
11
11
  };
12
+ /** A sidebar entry: either a link or a nested group. */
13
+ export type NavNode = NavItem | NavGroup;
14
+ /** Narrow a {@link NavNode} to a {@link NavGroup} (an entry with children). */
15
+ export declare function isNavGroup(node: NavNode): node is NavGroup;
12
16
  /**
13
- * Derive sidebar nav from a content collection: group by `section`, order by
14
- * `order` within a group, and order groups by the smallest `order` they
15
- * contain. Entries without a `section` fall under "Docs".
17
+ * Derive sidebar nav from a content collection. Each page's `section` (a string
18
+ * or a nested path) places it in the tree; within every level, entries order by
19
+ * `order` (then title), and a group inherits the smallest `order` of its
20
+ * descendants so groups sort by their earliest child. Top-level entries are
21
+ * always groups; pages without a section fall under "Docs".
16
22
  */
17
23
  export declare function navFromContent(content: DocsContentItem[]): NavGroup[];
24
+ /**
25
+ * Flatten the nav tree to its leaf links in sidebar reading order (depth-first),
26
+ * for the prev/next pager.
27
+ */
28
+ export declare function flattenNav(nodes: NavNode[]): NavItem[];
29
+ /**
30
+ * The trail of group titles leading to the page at `url` (top group first,
31
+ * excluding the page itself), or `undefined` if the page is not in the tree.
32
+ * Drives the breadcrumb trail.
33
+ */
34
+ export declare function navTrail(nodes: NavNode[], url: string): string[] | undefined;
package/dist/core/nav.js CHANGED
@@ -1,22 +1,101 @@
1
+ /** Narrow a {@link NavNode} to a {@link NavGroup} (an entry with children). */
2
+ export function isNavGroup(node) {
3
+ return 'items' in node;
4
+ }
5
+ /**
6
+ * Normalize a page's `section` to a group path. A string is a single level; an
7
+ * array is a nested path; blank or missing sections fall under "Docs". Empty
8
+ * strings inside an array are dropped so a stray `['Guides', '']` stays sane.
9
+ */
10
+ function sectionPath(section) {
11
+ const segs = Array.isArray(section) ? section : section ? [section] : [];
12
+ const clean = segs.filter((s) => typeof s === 'string' && s.trim() !== '');
13
+ return clean.length ? clean : ['Docs'];
14
+ }
15
+ function makeBuilder(title) {
16
+ return { title, order: Infinity, groups: new Map(), leaves: [] };
17
+ }
18
+ /**
19
+ * Emit a group's children (nested groups and leaf links) as one list, sorted by
20
+ * `order` and then title. The title tiebreak keeps the output deterministic when
21
+ * pages share an order (e.g. all default to 0), instead of depending on the
22
+ * filesystem scan order.
23
+ */
24
+ function emitItems(node) {
25
+ const entries = [];
26
+ for (const leaf of node.leaves) {
27
+ entries.push({
28
+ order: leaf.order,
29
+ title: leaf.title,
30
+ node: { title: leaf.title, url: leaf.url }
31
+ });
32
+ }
33
+ for (const group of node.groups.values()) {
34
+ entries.push({
35
+ order: group.order,
36
+ title: group.title,
37
+ node: { title: group.title, items: emitItems(group) }
38
+ });
39
+ }
40
+ entries.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
41
+ return entries.map((e) => e.node);
42
+ }
1
43
  /**
2
- * Derive sidebar nav from a content collection: group by `section`, order by
3
- * `order` within a group, and order groups by the smallest `order` they
4
- * contain. Entries without a `section` fall under "Docs".
44
+ * Derive sidebar nav from a content collection. Each page's `section` (a string
45
+ * or a nested path) places it in the tree; within every level, entries order by
46
+ * `order` (then title), and a group inherits the smallest `order` of its
47
+ * descendants so groups sort by their earliest child. Top-level entries are
48
+ * always groups; pages without a section fall under "Docs".
5
49
  */
6
50
  export function navFromContent(content) {
7
- const groups = new Map();
51
+ const root = makeBuilder('');
8
52
  for (const item of content) {
9
- const section = item.section ?? 'Docs';
10
53
  const order = item.order ?? 0;
11
- const group = groups.get(section) ?? { minOrder: Infinity, items: [] };
12
- group.items.push({ title: item.title, url: item.path, order });
13
- group.minOrder = Math.min(group.minOrder, order);
14
- groups.set(section, group);
54
+ let node = root;
55
+ for (const seg of sectionPath(item.section)) {
56
+ let child = node.groups.get(seg);
57
+ if (!child) {
58
+ child = makeBuilder(seg);
59
+ node.groups.set(seg, child);
60
+ }
61
+ child.order = Math.min(child.order, order);
62
+ node = child;
63
+ }
64
+ node.leaves.push({ title: item.title, url: item.path, order });
65
+ }
66
+ // The root only ever holds groups (every page has at least one path segment),
67
+ // so its emitted items are all NavGroups.
68
+ return emitItems(root);
69
+ }
70
+ /**
71
+ * Flatten the nav tree to its leaf links in sidebar reading order (depth-first),
72
+ * for the prev/next pager.
73
+ */
74
+ export function flattenNav(nodes) {
75
+ const out = [];
76
+ for (const node of nodes) {
77
+ if (isNavGroup(node))
78
+ out.push(...flattenNav(node.items));
79
+ else
80
+ out.push(node);
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * The trail of group titles leading to the page at `url` (top group first,
86
+ * excluding the page itself), or `undefined` if the page is not in the tree.
87
+ * Drives the breadcrumb trail.
88
+ */
89
+ export function navTrail(nodes, url) {
90
+ for (const node of nodes) {
91
+ if (isNavGroup(node)) {
92
+ const inner = navTrail(node.items, url);
93
+ if (inner)
94
+ return [node.title, ...inner];
95
+ }
96
+ else if (node.url === url) {
97
+ return [];
98
+ }
15
99
  }
16
- return [...groups.entries()]
17
- .sort((a, b) => a[1].minOrder - b[1].minOrder)
18
- .map(([title, group]) => ({
19
- title,
20
- items: group.items.sort((a, b) => a.order - b.order).map(({ title, url }) => ({ title, url }))
21
- }));
100
+ return undefined;
22
101
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `svelte-docsmith/changelog` — the generated release index.
3
+ *
4
+ * At build time the `docsmith()` Vite plugin (in `vite.config.ts`) intercepts
5
+ * this import and replaces it with your package's `CHANGELOG.md`, parsed into
6
+ * releases and dated from git. This file is only the fallback that runs when
7
+ * the plugin is missing — see {@link missingPluginError}.
8
+ */
9
+ import type { ChangelogRelease } from '../core/changelog.js';
10
+ export declare const releases: ChangelogRelease[];
@@ -0,0 +1,3 @@
1
+ import { missingPluginError } from './missing-plugin.js';
2
+ export const releases = [];
3
+ throw missingPluginError('changelog');
@@ -0,0 +1,35 @@
1
+ import type { ChangelogRelease } from '../core/changelog.js';
2
+ /** Site details the feed needs beyond the releases themselves. */
3
+ export type FeedSite = {
4
+ /** Canonical origin, e.g. `https://docs.example.com`. */
5
+ url: string;
6
+ /** Feed title, usually the site title. */
7
+ title: string;
8
+ /** One-line feed description. */
9
+ description?: string;
10
+ /** Path the changelog lives at. Default: `/changelog`. */
11
+ path?: string;
12
+ };
13
+ /**
14
+ * Build an Atom feed body from parsed changelog releases. Framework-agnostic:
15
+ * wire it into a SvelteKit `src/routes/changelog.xml/+server.ts` over the
16
+ * generated `svelte-docsmith/changelog` index.
17
+ *
18
+ * ```ts
19
+ * import { releases } from 'svelte-docsmith/changelog';
20
+ * import { generateFeed } from 'svelte-docsmith';
21
+ *
22
+ * export const prerender = true;
23
+ * export function GET() {
24
+ * const body = generateFeed(releases, {
25
+ * url: 'https://my-docs.dev',
26
+ * title: 'My Library'
27
+ * });
28
+ * return new Response(body, { headers: { 'content-type': 'application/atom+xml' } });
29
+ * }
30
+ * ```
31
+ *
32
+ * Atom rather than RSS because it requires an unambiguous per-entry id and
33
+ * update timestamp, which is exactly what a version and its release date give.
34
+ */
35
+ export declare function generateFeed(releases: ChangelogRelease[], site: FeedSite): string;