svelte-docsmith 0.13.0 → 0.13.1

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.
@@ -23,7 +23,9 @@
23
23
  }: { title?: string; description?: string; children: Snippet } = $props();
24
24
  </script>
25
25
 
26
- <article class="prose prose-base dark:prose-invert max-w-none pb-16">
26
+ <!-- max-w-prose (~65ch) keeps body measure in the DESIGN.md 65–75ch band on
27
+ ultrawide screens; code blocks and not-prose widgets still overflow locally. -->
28
+ <article class="prose prose-base dark:prose-invert max-w-prose pb-16">
27
29
  {#if title}
28
30
  <h1>{title}</h1>
29
31
  {/if}
@@ -1,12 +1,11 @@
1
1
  <!-- Decorative page background: a faint grid fading out from the top, with two
2
- soft primary/accent glows. Render it inside a `relative isolate` container. -->
2
+ soft primary/accent glows. Grid lines use --border so they track every theme
3
+ preset; render inside a `relative isolate` container. -->
3
4
  <div
4
5
  class="pointer-events-none absolute inset-0 -z-10 h-full w-full overflow-hidden"
5
6
  aria-hidden="true"
6
7
  >
7
- <div
8
- class="fixed inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px] mask-[radial-gradient(ellipse_80%_80%_at_50%_0%,#000_70%,transparent_100%)]"
9
- ></div>
8
+ <div class="pattern-grid fixed inset-0"></div>
10
9
 
11
10
  <div
12
11
  class="bg-primary/20 pointer-events-none fixed top-0 left-1/2 h-[500px] w-[800px] -translate-x-1/2 rounded-full opacity-40 mix-blend-screen blur-[120px] dark:opacity-20 dark:mix-blend-normal"
@@ -15,3 +14,22 @@ soft primary/accent glows. Render it inside a `relative isolate` container. -->
15
14
  class="bg-accent/25 pointer-events-none fixed top-28 left-1/2 h-[400px] w-[600px] -translate-x-1/2 rounded-full opacity-30 mix-blend-screen blur-[100px] dark:opacity-20 dark:mix-blend-normal"
16
15
  ></div>
17
16
  </div>
17
+
18
+ <style>
19
+ .pattern-grid {
20
+ /* 24px cell; line colour follows the theme border token at low opacity. */
21
+ background-image:
22
+ linear-gradient(
23
+ to right,
24
+ color-mix(in oklch, var(--border) 70%, transparent) 1px,
25
+ transparent 1px
26
+ ),
27
+ linear-gradient(
28
+ to bottom,
29
+ color-mix(in oklch, var(--border) 70%, transparent) 1px,
30
+ transparent 1px
31
+ );
32
+ background-size: 24px 24px;
33
+ mask-image: radial-gradient(ellipse 80% 80% at 50% 0%, #000 70%, transparent 100%);
34
+ }
35
+ </style>
@@ -34,6 +34,7 @@
34
34
 
35
35
  let query = $state('');
36
36
  let engine = $state<SearchEngine | null>(null);
37
+ let indexDocs = $state<SearchDoc[]>([]);
37
38
  let status = $state<'idle' | 'loading' | 'error'>('idle');
38
39
  // The version the cached engine was built for, so a loader that returns only
39
40
  // one version's records is rebuilt when the reader switches versions. Stays
@@ -44,6 +45,27 @@
44
45
  // Scope to the active version on a versioned site; `undefined` otherwise.
45
46
  const results = $derived(engine && trimmed ? engine.search(query, undefined, activeVersion) : []);
46
47
 
48
+ // Empty-state starters: prefer common first pages when present, then fill
49
+ // from the rest of the index so a custom site still has something useful.
50
+ const suggestions = $derived.by(() => {
51
+ const scoped = activeVersion ? indexDocs.filter((d) => d.version === activeVersion) : indexDocs;
52
+ if (!scoped.length) return [];
53
+ const preferred = [/introduction/i, /quick[- ]?start/i, /install/i, /getting started/i];
54
+ const picked: SearchDoc[] = [];
55
+ for (const re of preferred) {
56
+ const hit = scoped.find(
57
+ (d) =>
58
+ !picked.includes(d) && (re.test(d.title) || re.test(d.path) || re.test(d.section ?? ''))
59
+ );
60
+ if (hit) picked.push(hit);
61
+ }
62
+ for (const d of scoped) {
63
+ if (picked.length >= 4) break;
64
+ if (!picked.includes(d)) picked.push(d);
65
+ }
66
+ return picked.slice(0, 4);
67
+ });
68
+
47
69
  // Build the index the first time the palette opens; keep it for later opens.
48
70
  async function ensureEngine() {
49
71
  const version = activeVersion;
@@ -54,6 +76,7 @@
54
76
  import('../../search/create-search.js'),
55
77
  load(version)
56
78
  ]);
79
+ indexDocs = docs;
57
80
  engine = createSearchEngine(docs);
58
81
  engineVersion = version;
59
82
  status = 'idle';
@@ -106,16 +129,48 @@
106
129
  Search is unavailable right now.
107
130
  </div>
108
131
  {:else if !trimmed}
109
- <div
110
- class="text-muted-foreground flex flex-col items-center gap-3 px-4 py-10 text-center text-sm"
111
- >
112
- <span
113
- class="bg-muted/60 text-muted-foreground flex size-11 items-center justify-center rounded-full"
132
+ {#if suggestions.length}
133
+ <p class="text-muted-foreground px-2.5 pb-1.5 text-xs font-medium tracking-wide">
134
+ Suggested
135
+ </p>
136
+ {#each suggestions as doc (doc.path)}
137
+ <Command.LinkItem
138
+ href={doc.path}
139
+ onSelect={() => select(doc.path)}
140
+ class="group/result mb-0.5 flex items-center gap-3 rounded-lg px-2.5 py-2 aria-selected:bg-accent aria-selected:text-accent-foreground"
141
+ >
142
+ <span
143
+ class="border-border/60 bg-muted/40 text-muted-foreground group-aria-selected/result:border-accent-foreground/20 group-aria-selected/result:text-accent-foreground flex size-8 shrink-0 items-center justify-center rounded-md border"
144
+ >
145
+ <FileText class="size-4" />
146
+ </span>
147
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
148
+ <span class="truncate font-medium">{doc.title}</span>
149
+ {#if doc.section}
150
+ <span
151
+ class="text-muted-foreground group-aria-selected/result:text-accent-foreground/80 text-xs"
152
+ >
153
+ {doc.section}
154
+ </span>
155
+ {/if}
156
+ </span>
157
+ <CornerDownLeft
158
+ class="text-muted-foreground group-aria-selected/result:text-accent-foreground size-4 shrink-0 opacity-0 group-aria-selected/result:opacity-100"
159
+ />
160
+ </Command.LinkItem>
161
+ {/each}
162
+ {:else}
163
+ <div
164
+ class="text-muted-foreground flex flex-col items-center gap-3 px-4 py-10 text-center text-sm"
114
165
  >
115
- <SearchIcon class="size-5" />
116
- </span>
117
- <span>Search across every page of the documentation.</span>
118
- </div>
166
+ <span
167
+ class="bg-muted/60 text-muted-foreground flex size-11 items-center justify-center rounded-full"
168
+ >
169
+ <SearchIcon class="size-5" />
170
+ </span>
171
+ <span>Search across every page of the documentation.</span>
172
+ </div>
173
+ {/if}
119
174
  {:else if results.length === 0}
120
175
  <div class="text-muted-foreground px-4 py-10 text-center text-sm">
121
176
  No results for <span class="text-foreground font-medium">“{trimmed}”</span>.
@@ -5,12 +5,17 @@
5
5
  import { mode, toggleMode } from 'mode-watcher';
6
6
 
7
7
  const { class: className }: { class?: string } = $props();
8
+
9
+ // Label names the destination mode so the control is self-describing for
10
+ // assistive tech and matches what the icon shows (sun = go light, moon = go dark).
11
+ const label = $derived(mode.current === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
8
12
  </script>
9
13
 
10
14
  <Button
11
15
  size="icon"
12
16
  variant="ghost"
13
17
  onclick={toggleMode}
18
+ aria-label={label}
14
19
  class="text-muted-foreground hover:text-foreground size-8 {className ?? ''}"
15
20
  >
16
21
  {#if mode.current === 'dark'}
@@ -18,5 +23,4 @@
18
23
  {:else}
19
24
  <Moon class="size-4" />
20
25
  {/if}
21
- <span class="sr-only">Toggle theme</span>
22
26
  </Button>
@@ -5,6 +5,7 @@
5
5
  title,
6
6
  description,
7
7
  actions,
8
+ before,
8
9
  children
9
10
  }: {
10
11
  title: string;
@@ -12,7 +13,12 @@
12
13
  description?: string;
13
14
  /** Call-to-action buttons. */
14
15
  actions?: Snippet;
15
- /** Anything to sit below the actions, e.g. a note or a callout. */
16
+ /**
17
+ * Optional content above the heading (e.g. a pre-release note). Placed
18
+ * before the title so the section can still *end* on the actions.
19
+ */
20
+ before?: Snippet;
21
+ /** Anything to sit below the actions, e.g. a secondary note. */
16
22
  children?: Snippet;
17
23
  } = $props();
18
24
  </script>
@@ -29,12 +35,18 @@
29
35
  ></div>
30
36
  </div>
31
37
 
32
- <h2 class="mx-auto max-w-2xl text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
38
+ {#if before}
39
+ <div class="mx-auto mb-8 max-w-xl text-left">{@render before()}</div>
40
+ {/if}
41
+
42
+ <h2
43
+ class="font-serif mx-auto max-w-2xl text-3xl font-semibold tracking-tight text-balance sm:text-4xl"
44
+ >
33
45
  {title}
34
46
  </h2>
35
47
 
36
48
  {#if description}
37
- <p class="text-muted-foreground mx-auto mt-4 max-w-xl text-lg leading-relaxed text-pretty">
49
+ <p class="text-foreground mx-auto mt-4 max-w-xl text-lg leading-relaxed text-pretty">
38
50
  {description}
39
51
  </p>
40
52
  {/if}
@@ -5,7 +5,12 @@ type $$ComponentProps = {
5
5
  description?: string;
6
6
  /** Call-to-action buttons. */
7
7
  actions?: Snippet;
8
- /** Anything to sit below the actions, e.g. a note or a callout. */
8
+ /**
9
+ * Optional content above the heading (e.g. a pre-release note). Placed
10
+ * before the title so the section can still *end* on the actions.
11
+ */
12
+ before?: Snippet;
13
+ /** Anything to sit below the actions, e.g. a secondary note. */
9
14
  children?: Snippet;
10
15
  };
11
16
  declare const Cta: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -29,10 +29,12 @@
29
29
  {#if title || description}
30
30
  <div class="mx-auto max-w-2xl text-center">
31
31
  {#if title}
32
- <h2 class="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">{title}</h2>
32
+ <h2 class="font-serif text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
33
+ {title}
34
+ </h2>
33
35
  {/if}
34
36
  {#if description}
35
- <p class="text-muted-foreground mt-4 text-lg leading-relaxed text-pretty">
37
+ <p class="text-foreground mt-4 text-lg leading-relaxed text-pretty">
36
38
  {description}
37
39
  </p>
38
40
  {/if}
@@ -27,7 +27,7 @@
27
27
  <div class="min-w-0">
28
28
  <h3 class="font-semibold">{title}</h3>
29
29
  {#if children}
30
- <p class="text-muted-foreground mt-1 text-sm leading-relaxed">{@render children()}</p>
30
+ <p class="text-foreground/80 mt-1 text-sm leading-relaxed">{@render children()}</p>
31
31
  {/if}
32
32
  </div>
33
33
  </div>
@@ -40,7 +40,7 @@
40
40
  {/if}
41
41
 
42
42
  <h1
43
- class="text-5xl font-semibold tracking-[-0.03em] text-balance sm:text-6xl lg:text-[4.25rem] lg:leading-[1.02] {eyebrow
43
+ class="font-serif text-5xl font-semibold tracking-[-0.03em] text-balance sm:text-6xl lg:text-[4.25rem] lg:leading-[1.02] {eyebrow
44
44
  ? 'mt-6'
45
45
  : ''}"
46
46
  >
@@ -49,7 +49,7 @@
49
49
 
50
50
  {#if description}
51
51
  <p
52
- class="text-muted-foreground mt-6 max-w-xl text-lg leading-relaxed text-pretty {media
52
+ class="text-foreground mt-6 max-w-xl text-lg leading-relaxed text-pretty {media
53
53
  ? ''
54
54
  : 'mx-auto'}"
55
55
  >
@@ -13,9 +13,10 @@
13
13
  import * as Sheet from '../shadcn/sheet/index.js';
14
14
  import { useSearch } from '../../search/context.svelte.js';
15
15
  import { useDocsPage } from '../docs-page-context.js';
16
+ import GithubIcon from '../icons/github.svelte';
16
17
  import BookOpenText from '@lucide/svelte/icons/book-open-text';
18
+ import List from '@lucide/svelte/icons/list';
17
19
  import Menu from '@lucide/svelte/icons/menu';
18
- import PanelRight from '@lucide/svelte/icons/panel-right';
19
20
  import SearchIcon from '@lucide/svelte/icons/search';
20
21
  import type { Snippet } from 'svelte';
21
22
 
@@ -124,15 +125,32 @@
124
125
  </Button>
125
126
  {/if}
126
127
 
128
+ {#if config.github}
129
+ <Button
130
+ size="icon"
131
+ variant="ghost"
132
+ target="_blank"
133
+ rel="noopener noreferrer"
134
+ href={config.github}
135
+ class="text-muted-foreground hover:text-foreground size-8"
136
+ >
137
+ <GithubIcon class="size-4" />
138
+ <span class="sr-only">GitHub</span>
139
+ </Button>
140
+ {/if}
141
+
127
142
  <ThemeToggle />
128
143
 
129
144
  {#if tocItems.length > 0}
130
145
  <Popover.Root bind:open={isTocOpen}>
131
146
  <Popover.Trigger>
132
147
  {#snippet child({ props })}
133
- <Button variant="ghost" size="icon" class="size-8" {...props}>
134
- <PanelRight class="size-4" />
135
- <span class="sr-only">Table of contents</span>
148
+ <!-- Labeled (not icon-only) so first-timers find "on this page"
149
+ without guessing PanelRight. Label collapses on the narrowest
150
+ phones so the title still has room. -->
151
+ <Button variant="ghost" size="sm" class="h-8 gap-1.5 px-2" {...props}>
152
+ <List class="size-4 shrink-0" />
153
+ <span class="text-xs font-medium max-[22rem]:sr-only">On this page</span>
136
154
  </Button>
137
155
  {/snippet}
138
156
  </Popover.Trigger>
@@ -151,7 +151,7 @@
151
151
  readingTime && view.readingMinutes ? `${view.readingMinutes} min read` : undefined
152
152
  );
153
153
 
154
- const breadcrumbs = $derived(view.breadcrumbs.map((title) => ({ title })));
154
+ const breadcrumbs = $derived(view.breadcrumbs);
155
155
 
156
156
  // The archived-version banner, or nothing. Its "view this page in <current>"
157
157
  // link is the current version's entry in the same list the switcher renders,
@@ -286,6 +286,10 @@
286
286
  <PageFeedback
287
287
  path={view.pathname}
288
288
  onfeedback={typeof feedback === 'function' ? feedback : undefined}
289
+ editHref={view.editHref}
290
+ issueHref={config.github
291
+ ? `${config.github}/issues/new?title=${encodeURIComponent(`Feedback: ${view.title}`)}&body=${encodeURIComponent(`Page: ${view.pathname}\n\n<!-- What was unclear or missing? -->\n`)}`
292
+ : undefined}
289
293
  />
290
294
  </div>
291
295
  {/key}
@@ -43,7 +43,7 @@
43
43
  href={node.url}
44
44
  aria-current={current === node.url ? 'page' : undefined}
45
45
  class={cn(
46
- 'hover:text-primary hover:bg-primary/20 block rounded-md px-2 py-1.5 text-sm transition-colors',
46
+ 'hover:text-primary hover:bg-primary/10 block rounded-md px-2 py-1.5 text-sm transition-colors',
47
47
  current === node.url
48
48
  ? 'text-primary bg-primary/20 font-medium'
49
49
  : 'text-muted-foreground'
@@ -2,19 +2,29 @@
2
2
  import { Button } from '../shadcn/button/index.js';
3
3
  import ThumbsUp from '@lucide/svelte/icons/thumbs-up';
4
4
  import ThumbsDown from '@lucide/svelte/icons/thumbs-down';
5
+ import SquarePen from '@lucide/svelte/icons/square-pen';
6
+ import ExternalLink from '@lucide/svelte/icons/external-link';
5
7
 
6
8
  const {
7
9
  path,
8
- onfeedback
10
+ onfeedback,
11
+ editHref,
12
+ issueHref
9
13
  }: {
10
14
  /** The current page path, passed to the callback so it can attribute votes. */
11
15
  path: string;
12
16
  /** Called once per page when the reader votes; wire it to your analytics. */
13
17
  onfeedback?: (vote: 'up' | 'down', path: string) => void;
18
+ /** "Edit this page" target — offered after a "No" vote when present. */
19
+ editHref?: string;
20
+ /** "Open an issue" target — offered after a "No" vote when present. */
21
+ issueHref?: string;
14
22
  } = $props();
15
23
 
16
24
  let vote = $state<'up' | 'down' | null>(null);
17
25
 
26
+ const hasRecovery = $derived(Boolean(editHref || issueHref));
27
+
18
28
  function cast(value: 'up' | 'down') {
19
29
  if (vote) return;
20
30
  vote = value;
@@ -23,8 +33,40 @@
23
33
  </script>
24
34
 
25
35
  <div class="flex flex-col items-center gap-2.5 text-center">
26
- {#if vote}
36
+ {#if vote === 'up'}
27
37
  <p class="text-muted-foreground text-sm">Thanks for your feedback!</p>
38
+ {:else if vote === 'down'}
39
+ <p class="text-muted-foreground text-sm">
40
+ {hasRecovery ? 'Thanks — want to help improve this page?' : 'Thanks for your feedback!'}
41
+ </p>
42
+ {#if hasRecovery}
43
+ <div class="flex flex-wrap items-center justify-center gap-2">
44
+ {#if editHref}
45
+ <Button
46
+ variant="outline"
47
+ size="sm"
48
+ href={editHref}
49
+ target="_blank"
50
+ rel="noopener noreferrer"
51
+ >
52
+ <SquarePen />
53
+ Edit this page
54
+ </Button>
55
+ {/if}
56
+ {#if issueHref}
57
+ <Button
58
+ variant="outline"
59
+ size="sm"
60
+ href={issueHref}
61
+ target="_blank"
62
+ rel="noopener noreferrer"
63
+ >
64
+ <ExternalLink />
65
+ Open an issue
66
+ </Button>
67
+ {/if}
68
+ </div>
69
+ {/if}
28
70
  {:else}
29
71
  <p class="text-muted-foreground text-sm">Was this page helpful?</p>
30
72
  <div class="flex items-center gap-2">
@@ -3,6 +3,10 @@ type $$ComponentProps = {
3
3
  path: string;
4
4
  /** Called once per page when the reader votes; wire it to your analytics. */
5
5
  onfeedback?: (vote: 'up' | 'down', path: string) => void;
6
+ /** "Edit this page" target — offered after a "No" vote when present. */
7
+ editHref?: string;
8
+ /** "Open an issue" target — offered after a "No" vote when present. */
9
+ issueHref?: string;
6
10
  };
7
11
  declare const PageFeedback: import("svelte").Component<$$ComponentProps, {}, "">;
8
12
  type PageFeedback = ReturnType<typeof PageFeedback>;
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { NavItem } from '../../core/index.js';
3
+ import ChevronLeft from '@lucide/svelte/icons/chevron-left';
4
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
3
5
 
4
6
  const {
5
7
  prev,
@@ -16,9 +18,14 @@
16
18
  {#if prev}
17
19
  <a
18
20
  href={prev.url}
19
- class="border-border hover:border-primary flex min-w-0 flex-1 flex-col gap-1 rounded-lg border p-4 transition-colors"
21
+ class="border-border hover:border-primary/60 hover:bg-primary/5 group flex min-w-0 flex-1 flex-col gap-1 rounded-lg border p-4 transition-colors"
20
22
  >
21
- <span class="text-muted-foreground text-xs">Previous</span>
23
+ <span
24
+ class="text-muted-foreground group-hover:text-primary flex items-center gap-1 text-xs"
25
+ >
26
+ <ChevronLeft class="size-3.5" aria-hidden="true" />
27
+ Previous
28
+ </span>
22
29
  <span class="text-foreground truncate font-medium">{prev.title}</span>
23
30
  </a>
24
31
  {:else}
@@ -27,9 +34,14 @@
27
34
  {#if next}
28
35
  <a
29
36
  href={next.url}
30
- class="border-border hover:border-primary ml-auto flex min-w-0 flex-1 flex-col gap-1 rounded-lg border p-4 text-right transition-colors"
37
+ class="border-border hover:border-primary/60 hover:bg-primary/5 group ml-auto flex min-w-0 flex-1 flex-col items-end gap-1 rounded-lg border p-4 text-right transition-colors"
31
38
  >
32
- <span class="text-muted-foreground text-xs">Next</span>
39
+ <span
40
+ class="text-muted-foreground group-hover:text-primary flex items-center gap-1 text-xs"
41
+ >
42
+ Next
43
+ <ChevronRight class="size-3.5" aria-hidden="true" />
44
+ </span>
33
45
  <span class="text-foreground truncate font-medium">{next.title}</span>
34
46
  </a>
35
47
  {:else}
@@ -9,7 +9,7 @@
9
9
  * component that renders them.
10
10
  */
11
11
  import type { DocsContentItem } from './content.js';
12
- import { type NavGroup, type NavItem } from './nav.js';
12
+ import { type Breadcrumb, type NavGroup, type NavItem } from './nav.js';
13
13
  import { type ResolvedVersion } from './version.js';
14
14
  /** The page's build-time headings, as extracted into the content index. */
15
15
  type PageToc = NonNullable<DocsContentItem['toc']>;
@@ -56,10 +56,11 @@ export type DocsPageView = {
56
56
  /** The page's nav title, falling back to the site title off the nav tree. */
57
57
  title: string;
58
58
  /**
59
- * Breadcrumb titles: the page's group trail, then the page itself. Titles
60
- * only, so this layer never depends on a component's `Crumb` type.
59
+ * Breadcrumb trail: group steps (with a jump URL to the group's first page),
60
+ * then the current page as a title-only final crumb. No component types
61
+ * here — the shell maps this onto its `Crumb` shape.
61
62
  */
62
- breadcrumbs: string[];
63
+ breadcrumbs: Breadcrumb[];
63
64
  /** The page's server-rendered TOC, from {@link entry}. */
64
65
  toc: PageToc;
65
66
  /**
@@ -24,7 +24,7 @@ export function resolveDocsPage(input) {
24
24
  const title = pageIndex >= 0 ? flatNav[pageIndex].title : siteTitle;
25
25
  const breadcrumbs = navTrail(nav, pathname) ?? [];
26
26
  if (pageIndex >= 0)
27
- breadcrumbs.push(title);
27
+ breadcrumbs.push({ title });
28
28
  return {
29
29
  pathname,
30
30
  activeVersion: active,
@@ -1,5 +1,5 @@
1
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
3
  export { resolveVersions, activeVersion, currentVersion, currentOnly, scopeContent, mapPathToVersion, type DocsVersion, type DocsVersions, type ResolvedVersion } from './version.js';
4
- export { navFromContent, flattenNav, navTrail, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
4
+ export { navFromContent, flattenNav, navTrail, type Breadcrumb, isNavGroup, type NavItem, type NavGroup, type NavNode } from './nav.js';
5
5
  export { resolveDocsPage, type DocsPageView, type VersionLink } from './docs-page.js';
@@ -26,9 +26,14 @@ export declare function navFromContent(content: DocsContentItem[]): NavGroup[];
26
26
  * for the prev/next pager.
27
27
  */
28
28
  export declare function flattenNav(nodes: NavNode[]): NavItem[];
29
+ /** One step in a breadcrumb trail: a label, and an optional jump target. */
30
+ export type Breadcrumb = {
31
+ title: string;
32
+ url?: string;
33
+ };
29
34
  /**
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.
35
+ * The trail of groups leading to the page at `url` (top group first, excluding
36
+ * the page itself), or `undefined` if the page is not in the tree. Each group
37
+ * step carries a `url` to its first leaf so breadcrumbs are navigable.
33
38
  */
34
- export declare function navTrail(nodes: NavNode[], url: string): string[] | undefined;
39
+ export declare function navTrail(nodes: NavNode[], url: string): Breadcrumb[] | undefined;
package/dist/core/nav.js CHANGED
@@ -82,16 +82,35 @@ export function flattenNav(nodes) {
82
82
  return out;
83
83
  }
84
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.
85
+ * First leaf URL under a group, depth-first. Groups have no page of their own,
86
+ * so breadcrumbs that name a group link to its earliest page (the place a
87
+ * reader lands if they "go up" into that section).
88
+ */
89
+ function firstLeafUrl(group) {
90
+ for (const child of group.items) {
91
+ if (isNavGroup(child)) {
92
+ const url = firstLeafUrl(child);
93
+ if (url)
94
+ return url;
95
+ }
96
+ else {
97
+ return child.url;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * The trail of groups leading to the page at `url` (top group first, excluding
104
+ * the page itself), or `undefined` if the page is not in the tree. Each group
105
+ * step carries a `url` to its first leaf so breadcrumbs are navigable.
88
106
  */
89
107
  export function navTrail(nodes, url) {
90
108
  for (const node of nodes) {
91
109
  if (isNavGroup(node)) {
92
110
  const inner = navTrail(node.items, url);
93
- if (inner)
94
- return [node.title, ...inner];
111
+ if (inner) {
112
+ return [{ title: node.title, url: firstLeafUrl(node) }, ...inner];
113
+ }
95
114
  }
96
115
  else if (node.url === url) {
97
116
  return [];
package/dist/theme.css CHANGED
@@ -16,6 +16,8 @@
16
16
  @import 'tw-animate-css';
17
17
  @import 'tailwindcss';
18
18
  @import '@fontsource-variable/inter';
19
+ @import '@fontsource-variable/source-serif-4';
20
+ @import '@fontsource/jetbrains-mono';
19
21
 
20
22
  /* Generate utilities for the classes used inside this package. */
21
23
  @source './';
@@ -139,9 +141,12 @@
139
141
  --sidebar-accent-foreground: oklch(0.2101 0.0318 264.6645);
140
142
  --sidebar-border: oklch(0.9276 0.0058 264.5313);
141
143
  --sidebar-ring: oklch(0.6716 0.1368 48.513);
142
- --font-sans: Inter, sans-serif;
143
- --font-serif: Source Serif 4, serif;
144
- --font-mono: JetBrains Mono, monospace;
144
+ /* Fontsource variable packages register as "… Variable"; keep the
145
+ * static family names as fallbacks for consumers who load Google Fonts
146
+ * or system stacks instead. */
147
+ --font-sans: 'Inter Variable', Inter, sans-serif;
148
+ --font-serif: 'Source Serif 4 Variable', 'Source Serif 4', serif;
149
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
145
150
  --radius: 0.75rem;
146
151
  --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
147
152
  --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-docsmith",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "A framework for building beautiful documentation sites with Svelte.",
5
5
  "author": {
6
6
  "name": "George Daskalakis",
@@ -107,6 +107,8 @@
107
107
  },
108
108
  "dependencies": {
109
109
  "@fontsource-variable/inter": "^5.2.8",
110
+ "@fontsource-variable/source-serif-4": "^5.3.0",
111
+ "@fontsource/jetbrains-mono": "^5.3.0",
110
112
  "@hbsnow/rehype-sectionize": "^1.0.7",
111
113
  "@lucide/svelte": "^1.24.0",
112
114
  "@shikijs/transformers": "^3.3.0",