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
@@ -22,29 +22,65 @@
22
22
  import { setContext, type Snippet } from 'svelte';
23
23
  import * as TabsPrimitive from '../shadcn/tabs/index.js';
24
24
  import TabsPhaseScope from './tabs-phase.svelte';
25
+ import { syncedValue, setSyncedValue, hydrateSyncedValue } from './tabs-sync.svelte.js';
25
26
 
26
27
  const {
27
28
  value,
29
+ syncKey,
28
30
  children
29
31
  }: {
30
32
  /** Value/label of the tab selected by default. Defaults to the first tab. */
31
33
  value?: string;
34
+ /**
35
+ * Sync group. Every `<Tabs>` with the same `syncKey` shares its selection
36
+ * (e.g. `"pkg"` for npm/pnpm/yarn blocks), and the choice is remembered
37
+ * across reloads and navigation.
38
+ */
39
+ syncKey?: string;
32
40
  children: Snippet;
33
41
  } = $props();
34
42
 
35
- // The first <TabItem> to register wins the default selection. Captured during
36
- // the collect pass, which renders (below) before <Tabs.Root> reads it — so the
37
- // correct panel is already selected in the server-rendered HTML.
38
- let firstValue = $state<string | undefined>();
43
+ // Each <TabItem> registers its value during the collect pass, which renders
44
+ // (below) before <Tabs.Root> reads them — so the right panel is already
45
+ // selected in the server-rendered HTML. The first registered value is the
46
+ // default; the full list validates a restored sync choice against this block.
47
+ let values = $state<string[]>([]);
39
48
  setContext<TabsContext>(TABS_CTX, {
40
- register: (v) => (firstValue ??= v)
49
+ register: (v) => {
50
+ if (!values.includes(v)) values.push(v);
51
+ }
41
52
  });
53
+ const firstValue = $derived<string | undefined>(values[0]);
54
+
55
+ // Restore a persisted selection after mount (client-only), so it does not
56
+ // diverge from the server's default render until hydration is done. `ready`
57
+ // gates persistence until then, so any value event the tab primitive fires
58
+ // while mounting can't overwrite the reader's stored choice before we read it.
59
+ let ready = $state(false);
60
+ $effect(() => {
61
+ if (syncKey) hydrateSyncedValue(syncKey);
62
+ ready = true;
63
+ });
64
+
65
+ // A restored sync choice (valid for this block) wins, then the explicit
66
+ // `value`, then the first tab. Reactive, so a sibling group's change or a
67
+ // storage restore switches this instance too.
68
+ const selected = $derived.by(() => {
69
+ const fallback = value ?? firstValue ?? '';
70
+ if (!syncKey) return fallback;
71
+ const stored = syncedValue(syncKey);
72
+ return stored && values.includes(stored) ? stored : fallback;
73
+ });
74
+
75
+ function handleValueChange(next: string) {
76
+ if (ready && syncKey && next) setSyncedValue(syncKey, next);
77
+ }
42
78
  </script>
43
79
 
44
80
  <!-- collect: registers each TabItem's value; renders nothing. -->
45
81
  <TabsPhaseScope phase="collect">{@render children()}</TabsPhaseScope>
46
82
 
47
- <TabsPrimitive.Root value={value ?? firstValue ?? ''} class="my-6">
83
+ <TabsPrimitive.Root value={selected} onValueChange={handleValueChange} class="my-6">
48
84
  <TabsPrimitive.List class="not-prose mb-3">
49
85
  <TabsPhaseScope phase="list">{@render children()}</TabsPhaseScope>
50
86
  </TabsPrimitive.List>
@@ -11,6 +11,12 @@ import { type Snippet } from 'svelte';
11
11
  type $$ComponentProps = {
12
12
  /** Value/label of the tab selected by default. Defaults to the first tab. */
13
13
  value?: string;
14
+ /**
15
+ * Sync group. Every `<Tabs>` with the same `syncKey` shares its selection
16
+ * (e.g. `"pkg"` for npm/pnpm/yarn blocks), and the choice is remembered
17
+ * across reloads and navigation.
18
+ */
19
+ syncKey?: string;
14
20
  children: Snippet;
15
21
  };
16
22
  declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ const {
5
+ href,
6
+ variant = 'primary',
7
+ external = false,
8
+ arrow = variant === 'primary',
9
+ children
10
+ }: {
11
+ href: string;
12
+ /** `primary` is the filled button, `secondary` the outlined one beside it. */
13
+ variant?: 'primary' | 'secondary';
14
+ external?: boolean;
15
+ /** Trailing arrow that nudges on hover. Defaults on for `primary`. */
16
+ arrow?: boolean;
17
+ children: Snippet;
18
+ } = $props();
19
+
20
+ const rel = $derived(external ? 'noopener noreferrer' : undefined);
21
+ const target = $derived(external ? '_blank' : undefined);
22
+ </script>
23
+
24
+ <!-- A call-to-action link for <Hero> and <CTA>. Deliberately small: anything more
25
+ elaborate is better written as a plain anchor in your own page. -->
26
+ <a
27
+ {href}
28
+ {rel}
29
+ {target}
30
+ class="group inline-flex h-11 items-center gap-2 rounded-lg px-6 font-medium transition-all {variant ===
31
+ 'primary'
32
+ ? 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md'
33
+ : 'border-border bg-background hover:bg-accent border'}"
34
+ >
35
+ {@render children()}
36
+ {#if arrow}
37
+ <svg
38
+ viewBox="0 0 24 24"
39
+ fill="none"
40
+ stroke="currentColor"
41
+ stroke-width="2"
42
+ stroke-linecap="round"
43
+ stroke-linejoin="round"
44
+ class="size-4 transition-transform group-hover:translate-x-0.5"
45
+ aria-hidden="true"
46
+ >
47
+ <path d="M5 12h14M12 5l7 7-7 7" />
48
+ </svg>
49
+ {/if}
50
+ </a>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ href: string;
4
+ /** `primary` is the filled button, `secondary` the outlined one beside it. */
5
+ variant?: 'primary' | 'secondary';
6
+ external?: boolean;
7
+ /** Trailing arrow that nudges on hover. Defaults on for `primary`. */
8
+ arrow?: boolean;
9
+ children: Snippet;
10
+ };
11
+ declare const Action: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Action = ReturnType<typeof Action>;
13
+ export default Action;
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ const {
5
+ title,
6
+ description,
7
+ actions,
8
+ children
9
+ }: {
10
+ title: string;
11
+ /** Supporting line below the heading. */
12
+ description?: string;
13
+ /** Call-to-action buttons. */
14
+ actions?: Snippet;
15
+ /** Anything to sit below the actions, e.g. a note or a callout. */
16
+ children?: Snippet;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- The closing panel of a landing page: a bordered card with a soft glow behind
21
+ the heading, so the page ends on the primary colour rather than trailing off. -->
22
+ <section class="mx-auto max-w-7xl px-4 py-20 md:px-6 lg:px-8 lg:py-28">
23
+ <div
24
+ class="border-border bg-card relative isolate overflow-hidden rounded-2xl border px-6 py-16 text-center shadow-sm md:px-12"
25
+ >
26
+ <div class="pointer-events-none absolute inset-0 -z-10" aria-hidden="true">
27
+ <div
28
+ class="bg-primary/15 absolute top-0 left-1/2 h-75 w-150 -translate-x-1/2 rounded-full opacity-50 blur-[100px]"
29
+ ></div>
30
+ </div>
31
+
32
+ <h2 class="mx-auto max-w-2xl text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
33
+ {title}
34
+ </h2>
35
+
36
+ {#if description}
37
+ <p class="text-muted-foreground mx-auto mt-4 max-w-xl text-lg leading-relaxed text-pretty">
38
+ {description}
39
+ </p>
40
+ {/if}
41
+
42
+ {#if actions}
43
+ <div class="mt-8 flex flex-wrap items-center justify-center gap-3">
44
+ {@render actions()}
45
+ </div>
46
+ {/if}
47
+
48
+ {#if children}
49
+ <div class="mx-auto mt-10 max-w-xl text-left">{@render children()}</div>
50
+ {/if}
51
+ </div>
52
+ </section>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title: string;
4
+ /** Supporting line below the heading. */
5
+ description?: string;
6
+ /** Call-to-action buttons. */
7
+ actions?: Snippet;
8
+ /** Anything to sit below the actions, e.g. a note or a callout. */
9
+ children?: Snippet;
10
+ };
11
+ declare const Cta: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Cta = ReturnType<typeof Cta>;
13
+ export default Cta;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ const {
5
+ title,
6
+ description,
7
+ columns = 3,
8
+ background = 'default',
9
+ children
10
+ }: {
11
+ /** Section heading above the grid. */
12
+ title?: string;
13
+ /** Supporting line below the heading. */
14
+ description?: string;
15
+ /** Columns at the widest breakpoint; the grid steps down on smaller screens. */
16
+ columns?: 2 | 3;
17
+ /**
18
+ * `muted` gives the section a tinted band with rules above and below, for
19
+ * alternating against neighbouring sections.
20
+ */
21
+ background?: 'default' | 'muted';
22
+ /** The `<Feature>` cells. */
23
+ children: Snippet;
24
+ } = $props();
25
+ </script>
26
+
27
+ <section class={background === 'muted' ? 'border-border/50 bg-muted/20 border-y' : ''}>
28
+ <div class="mx-auto max-w-7xl px-4 py-16 md:px-6 lg:px-8 lg:py-24">
29
+ {#if title || description}
30
+ <div class="mx-auto max-w-2xl text-center">
31
+ {#if title}
32
+ <h2 class="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">{title}</h2>
33
+ {/if}
34
+ {#if description}
35
+ <p class="text-muted-foreground mt-4 text-lg leading-relaxed text-pretty">
36
+ {description}
37
+ </p>
38
+ {/if}
39
+ </div>
40
+ {/if}
41
+
42
+ <div
43
+ class="grid gap-x-10 gap-y-10 sm:grid-cols-2 {columns === 3 ? 'lg:grid-cols-3' : ''} {title ||
44
+ description
45
+ ? 'mt-14'
46
+ : ''}"
47
+ >
48
+ {@render children()}
49
+ </div>
50
+ </div>
51
+ </section>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Section heading above the grid. */
4
+ title?: string;
5
+ /** Supporting line below the heading. */
6
+ description?: string;
7
+ /** Columns at the widest breakpoint; the grid steps down on smaller screens. */
8
+ columns?: 2 | 3;
9
+ /**
10
+ * `muted` gives the section a tinted band with rules above and below, for
11
+ * alternating against neighbouring sections.
12
+ */
13
+ background?: 'default' | 'muted';
14
+ /** The `<Feature>` cells. */
15
+ children: Snippet;
16
+ };
17
+ declare const FeatureGrid: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ type FeatureGrid = ReturnType<typeof FeatureGrid>;
19
+ export default FeatureGrid;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ const {
5
+ title,
6
+ icon,
7
+ children
8
+ }: {
9
+ title: string;
10
+ /** Optional leading icon, rendered in a tinted square. */
11
+ icon?: Snippet;
12
+ /** The description. */
13
+ children?: Snippet;
14
+ } = $props();
15
+ </script>
16
+
17
+ <!-- One cell of a <FeatureGrid>. Icon beside the text rather than above it, so a
18
+ row of features stays compact as the column count grows. -->
19
+ <div class="flex gap-4">
20
+ {#if icon}
21
+ <div
22
+ class="bg-primary/10 text-primary flex size-10 shrink-0 items-center justify-center rounded-lg"
23
+ >
24
+ {@render icon()}
25
+ </div>
26
+ {/if}
27
+ <div class="min-w-0">
28
+ <h3 class="font-semibold">{title}</h3>
29
+ {#if children}
30
+ <p class="text-muted-foreground mt-1 text-sm leading-relaxed">{@render children()}</p>
31
+ {/if}
32
+ </div>
33
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title: string;
4
+ /** Optional leading icon, rendered in a tinted square. */
5
+ icon?: Snippet;
6
+ /** The description. */
7
+ children?: Snippet;
8
+ };
9
+ declare const Feature: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Feature = ReturnType<typeof Feature>;
11
+ export default Feature;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ const {
5
+ title,
6
+ description,
7
+ eyebrow,
8
+ actions,
9
+ media
10
+ }: {
11
+ /** The headline. Pass a snippet instead of a string to mark part of it up. */
12
+ title: string | Snippet;
13
+ /** Supporting paragraph below the headline. */
14
+ description?: string | Snippet;
15
+ /** A pill, badge, or announcement link sitting above the headline. */
16
+ eyebrow?: Snippet;
17
+ /** Call-to-action buttons. */
18
+ actions?: Snippet;
19
+ /**
20
+ * The second column: a code sample, screenshot, or live demo. Without it the
21
+ * hero collapses to a single centred column.
22
+ */
23
+ media?: Snippet;
24
+ } = $props();
25
+ </script>
26
+
27
+ <!-- Renders either a plain string or a snippet, so simple pages stay simple. -->
28
+ {#snippet content(value: string | Snippet)}
29
+ {#if typeof value === 'string'}{value}{:else}{@render value()}{/if}
30
+ {/snippet}
31
+
32
+ <section
33
+ class="mx-auto grid max-w-7xl items-center gap-12 px-4 pt-16 pb-20 md:px-6 lg:gap-10 lg:px-8 lg:pt-24 lg:pb-28 {media
34
+ ? 'lg:grid-cols-[1.05fr_1fr]'
35
+ : 'max-w-3xl text-center'}"
36
+ >
37
+ <div class="min-w-0">
38
+ {#if eyebrow}
39
+ {@render eyebrow()}
40
+ {/if}
41
+
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
44
+ ? 'mt-6'
45
+ : ''}"
46
+ >
47
+ {@render content(title)}
48
+ </h1>
49
+
50
+ {#if description}
51
+ <p
52
+ class="text-muted-foreground mt-6 max-w-xl text-lg leading-relaxed text-pretty {media
53
+ ? ''
54
+ : 'mx-auto'}"
55
+ >
56
+ {@render content(description)}
57
+ </p>
58
+ {/if}
59
+
60
+ {#if actions}
61
+ <div class="mt-8 flex flex-wrap items-center gap-3 {media ? '' : 'justify-center'}">
62
+ {@render actions()}
63
+ </div>
64
+ {/if}
65
+ </div>
66
+
67
+ {#if media}
68
+ <div class="min-w-0">{@render media()}</div>
69
+ {/if}
70
+ </section>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** The headline. Pass a snippet instead of a string to mark part of it up. */
4
+ title: string | Snippet;
5
+ /** Supporting paragraph below the headline. */
6
+ description?: string | Snippet;
7
+ /** A pill, badge, or announcement link sitting above the headline. */
8
+ eyebrow?: Snippet;
9
+ /** Call-to-action buttons. */
10
+ actions?: Snippet;
11
+ /**
12
+ * The second column: a code sample, screenshot, or live demo. Without it the
13
+ * hero collapses to a single centred column.
14
+ */
15
+ media?: Snippet;
16
+ };
17
+ declare const Hero: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ type Hero = ReturnType<typeof Hero>;
19
+ export default Hero;
@@ -4,10 +4,14 @@
4
4
  import { Separator } from '../shadcn/separator/index.js';
5
5
  import GithubIcon from '../icons/github.svelte';
6
6
  import SearchTrigger from '../chrome/search-trigger.svelte';
7
+ import { useSearch } from '../../search/context.svelte.js';
7
8
  import ThemeToggle from '../chrome/theme-toggle.svelte';
8
- import type { DocsmithConfig } from '../../core/index.js';
9
+ import type { DocsmithConfig, DocsmithLink } from '../../core/index.js';
9
10
  import BookOpenText from '@lucide/svelte/icons/book-open-text';
10
11
  import type { Snippet } from 'svelte';
12
+ import { page } from '$app/state';
13
+ import { cn } from '../../utils/cn.js';
14
+ import { normalizePath } from '../../utils/normalize-path.js';
11
15
 
12
16
  const {
13
17
  config,
@@ -21,6 +25,26 @@
21
25
  actions?: Snippet;
22
26
  } = $props();
23
27
 
28
+ // Present only when the consumer passed a `search` loader to DocsShell.
29
+ const search = useSearch();
30
+
31
+ const current = $derived(normalizePath(page.url.pathname));
32
+
33
+ /**
34
+ * A header link is usually the entry point to a whole section rather than a
35
+ * page you sit on: "Docs" points at `/docs/introduction`, and should stay lit
36
+ * while the reader moves through `/docs/*`. So match the link's first segment,
37
+ * not the exact path. The root is exempt, since every path starts with `/`.
38
+ */
39
+ function isActive(link: DocsmithLink): boolean {
40
+ if (link.external) return false;
41
+ const href = normalizePath(link.href);
42
+ if (href === current) return true;
43
+ if (href === '/') return false;
44
+ const section = '/' + href.split('/').filter(Boolean)[0];
45
+ return current === section || current.startsWith(section + '/');
46
+ }
47
+
24
48
  let isScrolled = $state(false);
25
49
  </script>
26
50
 
@@ -55,7 +79,11 @@
55
79
  href={link.href}
56
80
  target={link.external ? '_blank' : undefined}
57
81
  rel={link.external ? 'noopener noreferrer' : undefined}
58
- class="text-muted-foreground hover:text-foreground rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
82
+ aria-current={isActive(link) ? 'page' : undefined}
83
+ class={cn(
84
+ 'hover:text-foreground rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
85
+ isActive(link) ? 'text-foreground bg-muted/60' : 'text-muted-foreground'
86
+ )}
59
87
  >
60
88
  {link.label}
61
89
  </a>
@@ -76,7 +104,12 @@
76
104
 
77
105
  <SearchTrigger />
78
106
 
79
- <Separator orientation="vertical" class="bg-border/40 mx-1 min-h-0 h-6" />
107
+ <!-- The separator divides the search trigger from the action icons, so
108
+ it only earns its place when there is a trigger to divide from.
109
+ Without this a site that hasn't wired search shows a stray rule. -->
110
+ {#if search}
111
+ <Separator orientation="vertical" class="bg-border/40 mx-1 min-h-0 h-6" />
112
+ {/if}
80
113
 
81
114
  <div class="flex items-center gap-0.5">
82
115
  {#if actions}
@@ -3,6 +3,8 @@
3
3
  import { afterNavigate } from '$app/navigation';
4
4
  import {
5
5
  navFromContent,
6
+ flattenNav,
7
+ navTrail,
6
8
  type DocsContentItem,
7
9
  type DocsmithConfig,
8
10
  type SearchDoc
@@ -13,6 +15,7 @@
13
15
  import Search from '../chrome/search.svelte';
14
16
  import BackgroundPattern from '../chrome/background-pattern.svelte';
15
17
  import ThemeProvider from '../chrome/theme-provider.svelte';
18
+ import AnnouncementBar from '../chrome/announcement-bar.svelte';
16
19
  import DocsHeader from './docs-header.svelte';
17
20
  import DocsFooter from './docs-footer.svelte';
18
21
  import DocsMobileHeader from './docs-mobile-header.svelte';
@@ -128,8 +131,8 @@
128
131
  readingTime && currentEntry?.readingTime ? `${currentEntry.readingTime} min read` : undefined
129
132
  );
130
133
 
131
- // Ordered flat page list drives the prev/next links.
132
- const flatNav = $derived(nav.flatMap((group) => group.items));
134
+ // Ordered flat page list (leaves in sidebar reading order) drives prev/next.
135
+ const flatNav = $derived(flattenNav(nav));
133
136
  const pageIndex = $derived(flatNav.findIndex((item) => item.url === pathname));
134
137
  const prev = $derived(pageIndex > 0 ? flatNav[pageIndex - 1] : undefined);
135
138
  const next = $derived(
@@ -137,13 +140,10 @@
137
140
  );
138
141
  const currentTitle = $derived(pageIndex >= 0 ? flatNav[pageIndex].title : config.title);
139
142
 
140
- // Breadcrumb trail: the current page's sidebar group, then the page itself.
141
- const currentGroup = $derived(
142
- nav.find((group) => group.items.some((item) => item.url === pathname))
143
- );
143
+ // Breadcrumb trail: the current page's sidebar group path, then the page
144
+ // itself (so a nested page reads e.g. Guides > Advanced > Middleware).
144
145
  const breadcrumbs = $derived.by(() => {
145
- const crumbs: Crumb[] = [];
146
- if (currentGroup) crumbs.push({ title: currentGroup.title });
146
+ const crumbs: Crumb[] = (navTrail(nav, pathname) ?? []).map((title) => ({ title }));
147
147
  if (pageIndex >= 0) crumbs.push({ title: currentTitle });
148
148
  return crumbs;
149
149
  });
@@ -189,6 +189,12 @@
189
189
  <BackgroundPattern />
190
190
  {/if}
191
191
 
192
+ <!-- Announcement bar (if configured) sits above the sticky header, so it
193
+ scrolls away while the header stays pinned. -->
194
+ {#if config.announcement}
195
+ <AnnouncementBar announcement={config.announcement} />
196
+ {/if}
197
+
192
198
  <!-- One header system everywhere: DocsHeader on desktop, DocsMobileHeader
193
199
  below lg. The `page` layout just omits the sidebar nav and in-page TOC. -->
194
200
  <DocsHeader {config} {logo} {actions} />
@@ -1,35 +1,69 @@
1
1
  <script lang="ts">
2
2
  import { page } from '$app/state';
3
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
3
4
  import { cn } from '../../utils/cn.js';
4
5
  import { normalizePath } from '../../utils/normalize-path.js';
5
- import type { NavGroup } from '../../core/index.js';
6
+ import { isNavGroup, type NavGroup, type NavNode } from '../../core/index.js';
6
7
 
7
8
  const { nav, class: className = '' }: { nav: NavGroup[]; class?: string } = $props();
9
+
10
+ const current = $derived(normalizePath(page.url.pathname));
11
+
12
+ // Whether a subtree contains the current page — drives which nested groups
13
+ // start open. Recomputes on navigation, so navigating into a collapsed branch
14
+ // (e.g. via prev/next) opens it, while manual toggles on other branches stick.
15
+ function containsCurrent(node: NavNode): boolean {
16
+ return isNavGroup(node) ? node.items.some(containsCurrent) : node.url === current;
17
+ }
8
18
  </script>
9
19
 
20
+ {#snippet tree(items: NavNode[])}
21
+ <ul class="space-y-1">
22
+ {#each items as node (isNavGroup(node) ? 'g:' + node.title : node.url)}
23
+ {#if isNavGroup(node)}
24
+ <li>
25
+ <details class="group/nav" open={containsCurrent(node)}>
26
+ <summary
27
+ class="text-muted-foreground hover:text-foreground flex cursor-pointer list-none items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium transition-colors select-none [&::-webkit-details-marker]:hidden"
28
+ >
29
+ <span class="truncate">{node.title}</span>
30
+ <ChevronRight
31
+ class="ml-auto size-3.5 shrink-0 opacity-70 transition-transform duration-200 ease-out group-open/nav:rotate-90 motion-reduce:transition-none"
32
+ aria-hidden="true"
33
+ />
34
+ </summary>
35
+ <div class="border-border/60 mt-1 ml-2 border-l pl-3">
36
+ {@render tree(node.items)}
37
+ </div>
38
+ </details>
39
+ </li>
40
+ {:else}
41
+ <li>
42
+ <a
43
+ href={node.url}
44
+ aria-current={current === node.url ? 'page' : undefined}
45
+ class={cn(
46
+ 'hover:text-primary hover:bg-primary/20 block rounded-md px-2 py-1.5 text-sm transition-colors',
47
+ current === node.url
48
+ ? 'text-primary bg-primary/20 font-medium'
49
+ : 'text-muted-foreground'
50
+ )}
51
+ >
52
+ {node.title}
53
+ </a>
54
+ </li>
55
+ {/if}
56
+ {/each}
57
+ </ul>
58
+ {/snippet}
59
+
10
60
  <aside class={cn('sticky top-24 hidden h-screen w-56 shrink-0 bg-transparent lg:block', className)}>
11
61
  <div class="max-h-[calc(100vh-10rem)] overflow-y-auto px-4">
12
62
  <nav aria-label="Documentation" class="space-y-6">
13
63
  {#each nav as group (group.title)}
14
64
  <div class="space-y-3">
15
65
  <h4 class="text-foreground px-2 text-sm font-semibold">{group.title}</h4>
16
- <ul class="space-y-1">
17
- {#each group.items as item (item.url)}
18
- <li>
19
- <a
20
- href={item.url}
21
- class={cn(
22
- 'hover:text-primary hover:bg-primary/20 block rounded-md px-2 py-1.5 text-sm transition-colors',
23
- normalizePath(page.url.pathname) === item.url
24
- ? 'text-primary bg-primary/20 font-medium'
25
- : 'text-muted-foreground'
26
- )}
27
- >
28
- {item.title}
29
- </a>
30
- </li>
31
- {/each}
32
- </ul>
66
+ {@render tree(group.items)}
33
67
  </div>
34
68
  {/each}
35
69
  </nav>
@@ -1,4 +1,4 @@
1
- import type { NavGroup } from '../../core/index.js';
1
+ import { type NavGroup } from '../../core/index.js';
2
2
  type $$ComponentProps = {
3
3
  nav: NavGroup[];
4
4
  class?: string;