svelte-docsmith 0.2.0 → 0.4.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 (124) hide show
  1. package/README.md +66 -25
  2. package/dist/clipboard.svelte.d.ts +1 -1
  3. package/dist/clipboard.svelte.js +15 -8
  4. package/dist/config.d.ts +36 -6
  5. package/dist/config.js +33 -7
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.js +4 -0
  8. package/dist/markdown-layout.svelte +14 -1
  9. package/dist/markdown-layout.svelte.d.ts +2 -0
  10. package/dist/preprocess.d.ts +4 -5
  11. package/dist/preprocess.js +15 -7
  12. package/dist/search/context.svelte.d.ts +7 -0
  13. package/dist/search/context.svelte.js +20 -0
  14. package/dist/search/create-search.d.ts +14 -0
  15. package/dist/search/create-search.js +56 -0
  16. package/dist/search/snippet.d.ts +7 -0
  17. package/dist/search/snippet.js +43 -0
  18. package/dist/search.d.ts +10 -0
  19. package/dist/search.js +4 -0
  20. package/dist/theme.css +25 -0
  21. package/dist/types.d.ts +0 -6
  22. package/dist/ui/badge.svelte +10 -1
  23. package/dist/ui/badge.svelte.d.ts +2 -0
  24. package/dist/ui/callout.svelte +9 -9
  25. package/dist/ui/callout.svelte.d.ts +3 -3
  26. package/dist/ui/layouts/docs-header.svelte +5 -11
  27. package/dist/ui/layouts/docs-header.svelte.d.ts +0 -6
  28. package/dist/ui/layouts/docs-mobile-header.svelte +40 -8
  29. package/dist/ui/layouts/docs-mobile-header.svelte.d.ts +5 -4
  30. package/dist/ui/layouts/docs-shell.svelte +49 -5
  31. package/dist/ui/layouts/docs-shell.svelte.d.ts +16 -1
  32. package/dist/ui/layouts/error-page.svelte +82 -0
  33. package/dist/ui/layouts/error-page.svelte.d.ts +25 -0
  34. package/dist/ui/layouts/seo-head.svelte +65 -0
  35. package/dist/ui/layouts/seo-head.svelte.d.ts +11 -0
  36. package/dist/ui/markdown/h2.svelte +7 -3
  37. package/dist/ui/markdown/h3.svelte +7 -3
  38. package/dist/ui/markdown/pre.svelte +79 -29
  39. package/dist/ui/prop.svelte +71 -36
  40. package/dist/ui/prop.svelte.d.ts +1 -1
  41. package/dist/ui/props-table.svelte +10 -62
  42. package/dist/ui/search-trigger.svelte +31 -0
  43. package/dist/ui/search-trigger.svelte.d.ts +6 -0
  44. package/dist/ui/search.svelte +167 -0
  45. package/dist/ui/search.svelte.d.ts +13 -0
  46. package/dist/ui/shadcn/accordion/accordion.svelte.d.ts +1 -1
  47. package/dist/ui/shadcn/command/command-dialog.svelte +42 -0
  48. package/dist/ui/shadcn/command/command-dialog.svelte.d.ts +14 -0
  49. package/dist/ui/shadcn/command/command-empty.svelte +17 -0
  50. package/dist/ui/shadcn/command/command-empty.svelte.d.ts +4 -0
  51. package/dist/ui/shadcn/command/command-group.svelte +33 -0
  52. package/dist/ui/shadcn/command/command-group.svelte.d.ts +7 -0
  53. package/dist/ui/shadcn/command/command-input.svelte +36 -0
  54. package/dist/ui/shadcn/command/command-input.svelte.d.ts +4 -0
  55. package/dist/ui/shadcn/command/command-item.svelte +27 -0
  56. package/dist/ui/shadcn/command/command-item.svelte.d.ts +4 -0
  57. package/dist/ui/shadcn/command/command-link-item.svelte +20 -0
  58. package/dist/ui/shadcn/command/command-link-item.svelte.d.ts +4 -0
  59. package/dist/ui/shadcn/command/command-list.svelte +20 -0
  60. package/dist/ui/shadcn/command/command-list.svelte.d.ts +4 -0
  61. package/dist/ui/shadcn/command/command-loading.svelte +7 -0
  62. package/dist/ui/shadcn/command/command-loading.svelte.d.ts +4 -0
  63. package/dist/ui/shadcn/command/command-separator.svelte +17 -0
  64. package/dist/ui/shadcn/command/command-separator.svelte.d.ts +4 -0
  65. package/dist/ui/shadcn/command/command-shortcut.svelte +23 -0
  66. package/dist/ui/shadcn/command/command-shortcut.svelte.d.ts +5 -0
  67. package/dist/ui/shadcn/command/command.svelte +28 -0
  68. package/dist/ui/shadcn/command/command.svelte.d.ts +8 -0
  69. package/dist/ui/shadcn/command/index.d.ts +12 -0
  70. package/dist/ui/shadcn/command/index.js +14 -0
  71. package/dist/ui/shadcn/dialog/dialog-close.svelte +11 -0
  72. package/dist/ui/shadcn/dialog/dialog-close.svelte.d.ts +4 -0
  73. package/dist/ui/shadcn/dialog/dialog-content.svelte +48 -0
  74. package/dist/ui/shadcn/dialog/dialog-content.svelte.d.ts +13 -0
  75. package/dist/ui/shadcn/dialog/dialog-description.svelte +20 -0
  76. package/dist/ui/shadcn/dialog/dialog-description.svelte.d.ts +4 -0
  77. package/dist/ui/shadcn/dialog/dialog-footer.svelte +32 -0
  78. package/dist/ui/shadcn/dialog/dialog-footer.svelte.d.ts +8 -0
  79. package/dist/ui/shadcn/dialog/dialog-header.svelte +20 -0
  80. package/dist/ui/shadcn/dialog/dialog-header.svelte.d.ts +5 -0
  81. package/dist/ui/shadcn/dialog/dialog-overlay.svelte +20 -0
  82. package/dist/ui/shadcn/dialog/dialog-overlay.svelte.d.ts +4 -0
  83. package/dist/ui/shadcn/dialog/dialog-portal.svelte +7 -0
  84. package/dist/ui/shadcn/dialog/dialog-portal.svelte.d.ts +3 -0
  85. package/dist/ui/shadcn/dialog/dialog-title.svelte +17 -0
  86. package/dist/ui/shadcn/dialog/dialog-title.svelte.d.ts +4 -0
  87. package/dist/ui/shadcn/dialog/dialog-trigger.svelte +11 -0
  88. package/dist/ui/shadcn/dialog/dialog-trigger.svelte.d.ts +4 -0
  89. package/dist/ui/shadcn/dialog/dialog.svelte +7 -0
  90. package/dist/ui/shadcn/dialog/dialog.svelte.d.ts +3 -0
  91. package/dist/ui/shadcn/dialog/index.d.ts +11 -0
  92. package/dist/ui/shadcn/dialog/index.js +13 -0
  93. package/dist/ui/shadcn/input/index.d.ts +2 -0
  94. package/dist/ui/shadcn/input/index.js +4 -0
  95. package/dist/ui/shadcn/input/input.svelte +48 -0
  96. package/dist/ui/shadcn/input/input.svelte.d.ts +13 -0
  97. package/dist/ui/shadcn/input-group/index.d.ts +7 -0
  98. package/dist/ui/shadcn/input-group/index.js +9 -0
  99. package/dist/ui/shadcn/input-group/input-group-addon.svelte +53 -0
  100. package/dist/ui/shadcn/input-group/input-group-addon.svelte.d.ts +32 -0
  101. package/dist/ui/shadcn/input-group/input-group-button.svelte +49 -0
  102. package/dist/ui/shadcn/input-group/input-group-button.svelte.d.ts +32 -0
  103. package/dist/ui/shadcn/input-group/input-group-input.svelte +23 -0
  104. package/dist/ui/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  105. package/dist/ui/shadcn/input-group/input-group-text.svelte +22 -0
  106. package/dist/ui/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  107. package/dist/ui/shadcn/input-group/input-group-textarea.svelte +23 -0
  108. package/dist/ui/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  109. package/dist/ui/shadcn/input-group/input-group.svelte +24 -0
  110. package/dist/ui/shadcn/input-group/input-group.svelte.d.ts +5 -0
  111. package/dist/ui/shadcn/tabs/tabs.svelte.d.ts +1 -1
  112. package/dist/ui/shadcn/textarea/index.d.ts +2 -0
  113. package/dist/ui/shadcn/textarea/index.js +4 -0
  114. package/dist/ui/shadcn/textarea/textarea.svelte +23 -0
  115. package/dist/ui/shadcn/textarea/textarea.svelte.d.ts +5 -0
  116. package/dist/ui/tab-item.svelte +35 -9
  117. package/dist/ui/tab-item.svelte.d.ts +9 -4
  118. package/dist/ui/tabs-phase.svelte +19 -0
  119. package/dist/ui/tabs-phase.svelte.d.ts +9 -0
  120. package/dist/ui/tabs.svelte +47 -24
  121. package/dist/ui/tabs.svelte.d.ts +16 -6
  122. package/dist/vite.d.ts +8 -6
  123. package/dist/vite.js +140 -47
  124. package/package.json +8 -2
package/dist/theme.css CHANGED
@@ -80,6 +80,31 @@
80
80
  --shadow-2xl: var(--shadow-2xl);
81
81
  }
82
82
 
83
+ /* A slim, rounded, theme-aware scrollbar — sits inset on transparent track so
84
+ * it reads as part of the surface, not a browser chrome. Used by scrolling
85
+ * overlays (the search palette, popovers) where the native bar looks heavy. */
86
+ @utility scrollbar-slim {
87
+ scrollbar-width: thin;
88
+ scrollbar-color: color-mix(in oklab, var(--border) 85%, transparent) transparent;
89
+
90
+ &::-webkit-scrollbar {
91
+ width: 10px;
92
+ height: 10px;
93
+ }
94
+ &::-webkit-scrollbar-track {
95
+ background: transparent;
96
+ }
97
+ &::-webkit-scrollbar-thumb {
98
+ border-radius: 9999px;
99
+ border: 3px solid transparent;
100
+ background-clip: padding-box;
101
+ background-color: color-mix(in oklab, var(--border) 85%, transparent);
102
+ }
103
+ &::-webkit-scrollbar-thumb:hover {
104
+ background-color: var(--muted-foreground);
105
+ }
106
+ }
107
+
83
108
  @layer base {
84
109
  :root {
85
110
  --background: oklch(0.9383 0.0042 236.4993);
package/dist/types.d.ts CHANGED
@@ -5,9 +5,3 @@ import type { Snippet } from 'svelte';
5
5
  export type WithChildren<T> = T & {
6
6
  children: Snippet;
7
7
  };
8
- /**
9
- * Adds an optional `children` snippet to a props type.
10
- */
11
- export type WithChildrenOptional<T> = T & {
12
- children?: Snippet;
13
- };
@@ -19,12 +19,15 @@
19
19
  const {
20
20
  variant = 'default',
21
21
  href,
22
+ external = false,
22
23
  children
23
24
  }: {
24
25
  /** Visual intent. Default: `default`. */
25
26
  variant?: BadgeVariant;
26
27
  /** Turns the badge into a link. */
27
28
  href?: string;
29
+ /** For a linked badge, open in a new tab with `rel="noopener"`. */
30
+ external?: boolean;
28
31
  children: Snippet;
29
32
  } = $props();
30
33
 
@@ -49,6 +52,12 @@
49
52
  };
50
53
  </script>
51
54
 
52
- <ShadcnBadge variant={base[variant]} {href} class={extra[variant]}>
55
+ <ShadcnBadge
56
+ variant={base[variant]}
57
+ {href}
58
+ target={external ? '_blank' : undefined}
59
+ rel={external ? 'noopener noreferrer' : undefined}
60
+ class={extra[variant]}
61
+ >
53
62
  {@render children()}
54
63
  </ShadcnBadge>
@@ -5,6 +5,8 @@ type $$ComponentProps = {
5
5
  variant?: BadgeVariant;
6
6
  /** Turns the badge into a link. */
7
7
  href?: string;
8
+ /** For a linked badge, open in a new tab with `rel="noopener"`. */
9
+ external?: boolean;
8
10
  children: Snippet;
9
11
  };
10
12
  declare const Badge: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" module>
2
- export type CalloutType = 'note' | 'tip' | 'warning' | 'danger';
2
+ export type CalloutVariant = 'note' | 'tip' | 'warning' | 'danger';
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
@@ -11,32 +11,32 @@
11
11
  import type { Snippet } from 'svelte';
12
12
 
13
13
  const {
14
- type = 'note',
14
+ variant = 'note',
15
15
  title,
16
16
  children
17
17
  }: {
18
18
  /** Visual intent. Default: `note`. */
19
- type?: CalloutType;
20
- /** Optional heading; defaults to the capitalized type. */
19
+ variant?: CalloutVariant;
20
+ /** Optional heading; defaults to the capitalized variant. */
21
21
  title?: string;
22
22
  children: Snippet;
23
23
  } = $props();
24
24
 
25
- const variants: Record<CalloutType, { icon: Component; label: string; klass: string }> = {
25
+ const variants: Record<CalloutVariant, { icon: Component; label: string; klass: string }> = {
26
26
  note: { icon: Info, label: 'Note', klass: 'callout-note' },
27
27
  tip: { icon: Lightbulb, label: 'Tip', klass: 'callout-tip' },
28
28
  warning: { icon: TriangleAlert, label: 'Warning', klass: 'callout-warning' },
29
29
  danger: { icon: CircleAlert, label: 'Danger', klass: 'callout-danger' }
30
30
  };
31
31
 
32
- const variant = $derived(variants[type]);
33
- const Icon = $derived(variant.icon);
32
+ const resolved = $derived(variants[variant]);
33
+ const Icon = $derived(resolved.icon);
34
34
  </script>
35
35
 
36
- <div role="note" class="not-prose callout {variant.klass}">
36
+ <div role="note" class="not-prose callout {resolved.klass}">
37
37
  <Icon class="callout-icon" size={18} aria-hidden="true" />
38
38
  <div class="callout-body">
39
- <p class="callout-title">{title ?? variant.label}</p>
39
+ <p class="callout-title">{title ?? resolved.label}</p>
40
40
  <div class="callout-content">{@render children()}</div>
41
41
  </div>
42
42
  </div>
@@ -1,10 +1,10 @@
1
- export type CalloutType = 'note' | 'tip' | 'warning' | 'danger';
1
+ export type CalloutVariant = 'note' | 'tip' | 'warning' | 'danger';
2
2
  import type { Component } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  type $$ComponentProps = {
5
5
  /** Visual intent. Default: `note`. */
6
- type?: CalloutType;
7
- /** Optional heading; defaults to the capitalized type. */
6
+ variant?: CalloutVariant;
7
+ /** Optional heading; defaults to the capitalized variant. */
8
8
  title?: string;
9
9
  children: Snippet;
10
10
  };
@@ -3,6 +3,7 @@
3
3
  import { Button } from '../shadcn/button/index.js';
4
4
  import { Separator } from '../shadcn/separator/index.js';
5
5
  import GithubIcon from '../icons/github.svelte';
6
+ import SearchTrigger from '../search-trigger.svelte';
6
7
  import ThemeToggle from '../theme-toggle.svelte';
7
8
  import type { DocsmithConfig } from '../../config.js';
8
9
  import BookOpenText from '@lucide/svelte/icons/book-open-text';
@@ -11,20 +12,13 @@
11
12
  const {
12
13
  config,
13
14
  logo,
14
- actions,
15
- standalone = false
15
+ actions
16
16
  }: {
17
17
  config: DocsmithConfig;
18
18
  /** Custom logo mark; defaults to a book icon in a primary-tinted chip. */
19
19
  logo?: Snippet;
20
20
  /** Extra header controls, rendered before the theme toggle. */
21
21
  actions?: Snippet;
22
- /**
23
- * Show at every breakpoint (for pages without the docs mobile header, e.g.
24
- * a landing page). Default: desktop-only, since the docs shell pairs this
25
- * with a separate mobile header.
26
- */
27
- standalone?: boolean;
28
22
  } = $props();
29
23
 
30
24
  let isScrolled = $state(false);
@@ -33,9 +27,7 @@
33
27
  <svelte:window onscroll={() => (isScrolled = window.scrollY > 50)} />
34
28
 
35
29
  <header
36
- class="sticky top-0 z-40 w-full transition-all duration-200 {standalone
37
- ? 'block'
38
- : 'hidden lg:block'} {isScrolled
30
+ class="sticky top-0 z-40 hidden w-full transition-all duration-200 lg:block {isScrolled
39
31
  ? 'bg-background/60 supports-backdrop-filter:bg-background/60 border-border/40 border-b backdrop-blur-xl'
40
32
  : 'border-transparent bg-transparent'}"
41
33
  >
@@ -82,6 +74,8 @@
82
74
  </div>
83
75
  {/if}
84
76
 
77
+ <SearchTrigger />
78
+
85
79
  <Separator orientation="vertical" class="bg-border/40 mx-1 min-h-0 h-6" />
86
80
 
87
81
  <div class="flex items-center gap-0.5">
@@ -6,12 +6,6 @@ type $$ComponentProps = {
6
6
  logo?: Snippet;
7
7
  /** Extra header controls, rendered before the theme toggle. */
8
8
  actions?: Snippet;
9
- /**
10
- * Show at every breakpoint (for pages without the docs mobile header, e.g.
11
- * a landing page). Default: desktop-only, since the docs shell pairs this
12
- * with a separate mobile header.
13
- */
14
- standalone?: boolean;
15
9
  };
16
10
  declare const DocsHeader: import("svelte").Component<$$ComponentProps, {}, "">;
17
11
  type DocsHeader = ReturnType<typeof DocsHeader>;
@@ -8,33 +8,39 @@
8
8
  import { Button } from '../shadcn/button/index.js';
9
9
  import * as Popover from '../shadcn/popover/index.js';
10
10
  import { ScrollArea } from '../shadcn/scroll-area/index.js';
11
+ import { Separator } from '../shadcn/separator/index.js';
11
12
  import * as Sheet from '../shadcn/sheet/index.js';
13
+ import { useSearch } from '../../search/context.svelte.js';
12
14
  import BookOpenText from '@lucide/svelte/icons/book-open-text';
13
15
  import Menu from '@lucide/svelte/icons/menu';
14
16
  import PanelRight from '@lucide/svelte/icons/panel-right';
17
+ import SearchIcon from '@lucide/svelte/icons/search';
15
18
  import type { Snippet } from 'svelte';
16
19
 
17
20
  const {
18
21
  config,
19
- nav,
20
- title,
21
- tocItems,
22
+ nav = [],
23
+ title = config.title,
24
+ tocItems = [],
22
25
  tocActiveId = null,
23
26
  logo,
24
27
  actions
25
28
  }: {
26
29
  config: DocsmithConfig;
27
- nav: NavGroup[];
28
- /** Title of the current page, shown between the menu and controls. */
29
- title: string;
30
+ /** Sidebar groups; omit on non-doc pages (the landing/`page` layout). */
31
+ nav?: NavGroup[];
32
+ /** Title shown between the menu and controls. Defaults to the site title. */
33
+ title?: string;
30
34
  /** In-page TOC entries; the TOC popover is hidden when empty. */
31
- tocItems: TocItem[];
35
+ tocItems?: TocItem[];
32
36
  /** Id of the heading currently in view. */
33
37
  tocActiveId?: string | null;
34
38
  logo?: Snippet;
35
39
  actions?: Snippet;
36
40
  } = $props();
37
41
 
42
+ const search = useSearch();
43
+
38
44
  let isMenuOpen = $state(false);
39
45
  let isTocOpen = $state(false);
40
46
 
@@ -72,7 +78,26 @@
72
78
  </a>
73
79
  </div>
74
80
  <ScrollArea class="my-4 h-[calc(100vh-8rem)] pb-10 pl-2">
75
- <DocsSidebar {nav} class="static block h-auto w-full" />
81
+ {#if config.nav?.length}
82
+ <nav class="flex flex-col gap-0.5 px-5 pb-1">
83
+ {#each config.nav as link (link.href)}
84
+ <a
85
+ href={link.href}
86
+ target={link.external ? '_blank' : undefined}
87
+ rel={link.external ? 'noopener noreferrer' : undefined}
88
+ class="text-muted-foreground hover:text-foreground rounded-md py-1.5 text-sm font-medium transition-colors"
89
+ >
90
+ {link.label}
91
+ </a>
92
+ {/each}
93
+ </nav>
94
+ {#if nav.length}
95
+ <Separator class="bg-border/60 mx-5 my-2" />
96
+ {/if}
97
+ {/if}
98
+ {#if nav.length}
99
+ <DocsSidebar {nav} class="static block h-auto w-full" />
100
+ {/if}
76
101
  </ScrollArea>
77
102
  </Sheet.Content>
78
103
  </Sheet.Root>
@@ -84,6 +109,13 @@
84
109
  {@render actions()}
85
110
  {/if}
86
111
 
112
+ {#if search}
113
+ <Button variant="ghost" size="icon" class="size-8" onclick={() => (search.open = true)}>
114
+ <SearchIcon class="size-4" />
115
+ <span class="sr-only">Search documentation</span>
116
+ </Button>
117
+ {/if}
118
+
87
119
  <ThemeToggle />
88
120
 
89
121
  {#if tocItems.length > 0}
@@ -3,11 +3,12 @@ import type { TocItem } from '../../toc/index.js';
3
3
  import type { Snippet } from 'svelte';
4
4
  type $$ComponentProps = {
5
5
  config: DocsmithConfig;
6
- nav: NavGroup[];
7
- /** Title of the current page, shown between the menu and controls. */
8
- title: string;
6
+ /** Sidebar groups; omit on non-doc pages (the landing/`page` layout). */
7
+ nav?: NavGroup[];
8
+ /** Title shown between the menu and controls. Defaults to the site title. */
9
+ title?: string;
9
10
  /** In-page TOC entries; the TOC popover is hidden when empty. */
10
- tocItems: TocItem[];
11
+ tocItems?: TocItem[];
11
12
  /** Id of the heading currently in view. */
12
13
  tocActiveId?: string | null;
13
14
  logo?: Snippet;
@@ -1,8 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { page } from '$app/state';
3
3
  import { afterNavigate } from '$app/navigation';
4
- import { navFromContent, type DocsContentItem, type DocsmithConfig } from '../../config.js';
4
+ import {
5
+ navFromContent,
6
+ type DocsContentItem,
7
+ type DocsmithConfig,
8
+ type SearchDoc
9
+ } from '../../config.js';
5
10
  import { createToc } from '../../toc/index.js';
11
+ import { createSearchState } from '../../search/context.svelte.js';
12
+ import Search from '../search.svelte';
6
13
  import BackgroundPattern from '../background-pattern.svelte';
7
14
  import ThemeProvider from '../theme-provider.svelte';
8
15
  import DocsHeader from './docs-header.svelte';
@@ -11,6 +18,7 @@
11
18
  import DocsSidebar from './docs-sidebar.svelte';
12
19
  import PrevNextNav from './prev-next-nav.svelte';
13
20
  import Breadcrumbs, { type Crumb } from './breadcrumbs.svelte';
21
+ import SeoHead from './seo-head.svelte';
14
22
  import TableOfContents from '../table-of-contents.svelte';
15
23
  import type { Snippet } from 'svelte';
16
24
 
@@ -21,12 +29,26 @@
21
29
  logo,
22
30
  actions,
23
31
  footer,
32
+ search,
33
+ seo,
24
34
  pattern = false,
25
35
  layout = 'docs'
26
36
  }: {
27
37
  config: DocsmithConfig;
28
38
  content?: DocsContentItem[];
29
39
  children: Snippet;
40
+ /**
41
+ * Override the head tags for this page. Doc pages get their `<title>` and
42
+ * description from frontmatter automatically; use this on non-doc pages
43
+ * (the landing page, custom routes) or to override.
44
+ */
45
+ seo?: { title?: string; description?: string };
46
+ /**
47
+ * Enable the ⌘K search palette by lazily providing the generated index,
48
+ * e.g. `search={() => import('svelte-docsmith/search').then((m) => m.docs)}`.
49
+ * Omit to hide search. The index is fetched only when search first opens.
50
+ */
51
+ search?: () => Promise<SearchDoc[]>;
30
52
  /** Custom logo mark for the header and mobile menu. */
31
53
  logo?: Snippet;
32
54
  /** Extra header controls (desktop and mobile), before the theme toggle. */
@@ -44,8 +66,18 @@
44
66
  layout?: 'docs' | 'page';
45
67
  } = $props();
46
68
 
69
+ // Publish search state on context so the header triggers and the single
70
+ // dialog share one open-state; only when the consumer wired an index. The
71
+ // `search` loader is static for the shell's lifetime, so reading it once at
72
+ // init is intentional.
73
+ // svelte-ignore state_referenced_locally
74
+ if (search) createSearchState();
75
+
47
76
  const nav = $derived(navFromContent(content));
48
77
 
78
+ // The content entry for the current route drives the SEO title/description.
79
+ const currentEntry = $derived(content.find((item) => item.path === page.url.pathname));
80
+
49
81
  // Ordered flat page list drives the prev/next links.
50
82
  const flatNav = $derived(nav.flatMap((group) => group.items));
51
83
  const pageIndex = $derived(flatNav.findIndex((item) => item.url === page.url.pathname));
@@ -81,28 +113,40 @@
81
113
  const tocItems = $derived(toc.items.length ? toc.items : pageToc);
82
114
  </script>
83
115
 
116
+ <SeoHead
117
+ {config}
118
+ title={seo?.title ?? currentEntry?.title}
119
+ description={seo?.description ?? currentEntry?.description}
120
+ />
121
+
84
122
  <div class="relative isolate flex min-h-screen flex-col">
85
123
  <!-- Owns light/dark for the whole app — consumers never wire mode-watcher. -->
86
124
  <ThemeProvider />
87
125
 
126
+ {#if search}
127
+ <Search load={search} />
128
+ {/if}
129
+
88
130
  {#if pattern}
89
131
  <BackgroundPattern />
90
132
  {/if}
91
133
 
134
+ <!-- One header system everywhere: DocsHeader on desktop, DocsMobileHeader
135
+ below lg. The `page` layout just omits the sidebar nav and in-page TOC. -->
136
+ <DocsHeader {config} {logo} {actions} />
137
+
92
138
  {#if layout === 'page'}
93
- <DocsHeader {config} {logo} {actions} standalone />
139
+ <DocsMobileHeader {config} {logo} {actions} />
94
140
 
95
141
  <main class="flex-1">
96
142
  {@render children()}
97
143
  </main>
98
144
  {:else}
99
- <DocsHeader {config} {logo} {actions} />
100
-
101
145
  <DocsMobileHeader
102
146
  {config}
103
147
  {nav}
104
148
  title={currentTitle}
105
- tocItems={toc.items}
149
+ {tocItems}
106
150
  tocActiveId={toc.activeId}
107
151
  {logo}
108
152
  {actions}
@@ -1,9 +1,24 @@
1
- import { type DocsContentItem, type DocsmithConfig } from '../../config.js';
1
+ import { type DocsContentItem, type DocsmithConfig, type SearchDoc } from '../../config.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  type $$ComponentProps = {
4
4
  config: DocsmithConfig;
5
5
  content?: DocsContentItem[];
6
6
  children: Snippet;
7
+ /**
8
+ * Override the head tags for this page. Doc pages get their `<title>` and
9
+ * description from frontmatter automatically; use this on non-doc pages
10
+ * (the landing page, custom routes) or to override.
11
+ */
12
+ seo?: {
13
+ title?: string;
14
+ description?: string;
15
+ };
16
+ /**
17
+ * Enable the ⌘K search palette by lazily providing the generated index,
18
+ * e.g. `search={() => import('svelte-docsmith/search').then((m) => m.docs)}`.
19
+ * Omit to hide search. The index is fetched only when search first opens.
20
+ */
21
+ search?: () => Promise<SearchDoc[]>;
7
22
  /** Custom logo mark for the header and mobile menu. */
8
23
  logo?: Snippet;
9
24
  /** Extra header controls (desktop and mobile), before the theme toggle. */
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import type { DocsContentItem, DocsmithConfig, SearchDoc } from '../../config.js';
4
+ import DocsShell from './docs-shell.svelte';
5
+ import ArrowLeft from '@lucide/svelte/icons/arrow-left';
6
+ import type { Snippet } from 'svelte';
7
+
8
+ const {
9
+ config,
10
+ content = [],
11
+ search,
12
+ status,
13
+ title,
14
+ message,
15
+ home = '/',
16
+ homeLabel = 'Back to home',
17
+ pattern = true,
18
+ children
19
+ }: {
20
+ config: DocsmithConfig;
21
+ /** Content index, so the error page keeps the same header/footer as the site. */
22
+ content?: DocsContentItem[];
23
+ /** Enable the ⌘K search palette on the error page (same loader as DocsShell). */
24
+ search?: () => Promise<SearchDoc[]>;
25
+ /** HTTP status; defaults to the current `page.status`. */
26
+ status?: number;
27
+ /** Heading; defaults to a message keyed off the status. */
28
+ title?: string;
29
+ /** Body line; defaults to the error message, or a status-appropriate default. */
30
+ message?: string;
31
+ /** Where the primary action links. Default: the site root. */
32
+ home?: string;
33
+ homeLabel?: string;
34
+ /** Render the decorative page background. Default: `true`. */
35
+ pattern?: boolean;
36
+ /** Extra content below the action (e.g. a search prompt or links). */
37
+ children?: Snippet;
38
+ } = $props();
39
+
40
+ const resolvedStatus = $derived(status ?? page.status);
41
+ const isNotFound = $derived(resolvedStatus === 404);
42
+ const resolvedTitle = $derived(title ?? (isNotFound ? 'Page not found' : 'Something went wrong'));
43
+ const resolvedMessage = $derived(
44
+ message ??
45
+ page.error?.message ??
46
+ (isNotFound
47
+ ? "The page you're looking for doesn't exist or has moved."
48
+ : 'An unexpected error occurred. Please try again.')
49
+ );
50
+ </script>
51
+
52
+ <DocsShell {config} {content} {search} {pattern} layout="page" seo={{ title: resolvedTitle }}>
53
+ <section
54
+ class="mx-auto flex min-h-[60vh] max-w-2xl flex-col items-center justify-center px-4 py-20 text-center md:px-6"
55
+ >
56
+ <p class="text-primary font-mono text-sm font-semibold tracking-widest tabular-nums">
57
+ {resolvedStatus}
58
+ </p>
59
+ <h1 class="mt-4 text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
60
+ {resolvedTitle}
61
+ </h1>
62
+ <p class="text-muted-foreground mt-4 text-lg leading-relaxed text-pretty">
63
+ {resolvedMessage}
64
+ </p>
65
+
66
+ <div class="mt-8 flex flex-wrap items-center justify-center gap-3">
67
+ <a
68
+ href={home}
69
+ class="group bg-primary text-primary-foreground inline-flex h-11 items-center gap-2 rounded-lg px-6 font-medium shadow-sm transition-all hover:bg-primary/90 hover:shadow-md"
70
+ >
71
+ <ArrowLeft class="size-4 transition-transform group-hover:-translate-x-0.5" />
72
+ {homeLabel}
73
+ </a>
74
+ </div>
75
+
76
+ {#if children}
77
+ <div class="mt-8">
78
+ {@render children()}
79
+ </div>
80
+ {/if}
81
+ </section>
82
+ </DocsShell>
@@ -0,0 +1,25 @@
1
+ import type { DocsContentItem, DocsmithConfig, SearchDoc } from '../../config.js';
2
+ import type { Snippet } from 'svelte';
3
+ type $$ComponentProps = {
4
+ config: DocsmithConfig;
5
+ /** Content index, so the error page keeps the same header/footer as the site. */
6
+ content?: DocsContentItem[];
7
+ /** Enable the ⌘K search palette on the error page (same loader as DocsShell). */
8
+ search?: () => Promise<SearchDoc[]>;
9
+ /** HTTP status; defaults to the current `page.status`. */
10
+ status?: number;
11
+ /** Heading; defaults to a message keyed off the status. */
12
+ title?: string;
13
+ /** Body line; defaults to the error message, or a status-appropriate default. */
14
+ message?: string;
15
+ /** Where the primary action links. Default: the site root. */
16
+ home?: string;
17
+ homeLabel?: string;
18
+ /** Render the decorative page background. Default: `true`. */
19
+ pattern?: boolean;
20
+ /** Extra content below the action (e.g. a search prompt or links). */
21
+ children?: Snippet;
22
+ };
23
+ declare const ErrorPage: import("svelte").Component<$$ComponentProps, {}, "">;
24
+ type ErrorPage = ReturnType<typeof ErrorPage>;
25
+ export default ErrorPage;
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import type { DocsmithConfig } from '../../config.js';
4
+
5
+ const {
6
+ config,
7
+ title,
8
+ description
9
+ }: {
10
+ config: DocsmithConfig;
11
+ /** The page's own title (frontmatter or override); omit for the site root. */
12
+ title?: string;
13
+ /** The page's own description; falls back to `config.description`. */
14
+ description?: string;
15
+ } = $props();
16
+
17
+ // "Page · Site", or just the site title when they would duplicate (the home page).
18
+ const fullTitle = $derived(
19
+ title && title !== config.title ? `${title} · ${config.title}` : config.title
20
+ );
21
+ const ogTitle = $derived(title ?? config.title);
22
+ const metaDescription = $derived(description ?? config.description);
23
+
24
+ const origin = $derived(config.url?.replace(/\/$/, ''));
25
+ // Absolute URLs need a configured origin; otherwise canonical/og:url are omitted.
26
+ const canonical = $derived(origin ? origin + page.url.pathname : undefined);
27
+ const image = $derived.by(() => {
28
+ const src = config.ogImage;
29
+ if (!src) return undefined;
30
+ if (/^https?:\/\//.test(src)) return src;
31
+ return origin ? origin + '/' + src.replace(/^\//, '') : src;
32
+ });
33
+ </script>
34
+
35
+ <svelte:head>
36
+ <title>{fullTitle}</title>
37
+ {#if metaDescription}
38
+ <meta name="description" content={metaDescription} />
39
+ {/if}
40
+ {#if canonical}
41
+ <link rel="canonical" href={canonical} />
42
+ {/if}
43
+
44
+ <meta property="og:title" content={ogTitle} />
45
+ {#if metaDescription}
46
+ <meta property="og:description" content={metaDescription} />
47
+ {/if}
48
+ <meta property="og:type" content="website" />
49
+ <meta property="og:site_name" content={config.title} />
50
+ {#if canonical}
51
+ <meta property="og:url" content={canonical} />
52
+ {/if}
53
+ {#if image}
54
+ <meta property="og:image" content={image} />
55
+ {/if}
56
+
57
+ <meta name="twitter:card" content={image ? 'summary_large_image' : 'summary'} />
58
+ <meta name="twitter:title" content={ogTitle} />
59
+ {#if metaDescription}
60
+ <meta name="twitter:description" content={metaDescription} />
61
+ {/if}
62
+ {#if image}
63
+ <meta name="twitter:image" content={image} />
64
+ {/if}
65
+ </svelte:head>
@@ -0,0 +1,11 @@
1
+ import type { DocsmithConfig } from '../../config.js';
2
+ type $$ComponentProps = {
3
+ config: DocsmithConfig;
4
+ /** The page's own title (frontmatter or override); omit for the site root. */
5
+ title?: string;
6
+ /** The page's own description; falls back to `config.description`. */
7
+ description?: string;
8
+ };
9
+ declare const SeoHead: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type SeoHead = ReturnType<typeof SeoHead>;
11
+ export default SeoHead;
@@ -5,9 +5,13 @@
5
5
  const { children, ...attrs }: HeadingProps = $props();
6
6
  </script>
7
7
 
8
- <h2 {...attrs} class="flex gap-2 items-center group">
8
+ <h2 {...attrs} class="group flex items-center gap-2">
9
9
  {@render children()}
10
- <a href="#{attrs.id}" class="opacity-0 group-hover:opacity-100 transition-opacity">
11
- <Link class="size-5 text-primary" />
10
+ <a
11
+ href="#{attrs.id}"
12
+ aria-label="Link to this section"
13
+ class="opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
14
+ >
15
+ <Link class="text-primary size-5" />
12
16
  </a>
13
17
  </h2>