svelte-docsmith 0.1.1 → 0.3.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 (126) hide show
  1. package/README.md +57 -25
  2. package/dist/clipboard.svelte.d.ts +1 -1
  3. package/dist/clipboard.svelte.js +15 -8
  4. package/dist/config.d.ts +41 -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 +4 -5
  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/themes/darkmatter.css +81 -0
  22. package/dist/types.d.ts +0 -6
  23. package/dist/ui/badge.svelte +10 -1
  24. package/dist/ui/badge.svelte.d.ts +2 -0
  25. package/dist/ui/callout.svelte +9 -9
  26. package/dist/ui/callout.svelte.d.ts +3 -3
  27. package/dist/ui/layouts/docs-footer.svelte +32 -6
  28. package/dist/ui/layouts/docs-header.svelte +5 -11
  29. package/dist/ui/layouts/docs-header.svelte.d.ts +0 -6
  30. package/dist/ui/layouts/docs-mobile-header.svelte +40 -8
  31. package/dist/ui/layouts/docs-mobile-header.svelte.d.ts +5 -4
  32. package/dist/ui/layouts/docs-shell.svelte +49 -5
  33. package/dist/ui/layouts/docs-shell.svelte.d.ts +16 -1
  34. package/dist/ui/layouts/error-page.svelte +82 -0
  35. package/dist/ui/layouts/error-page.svelte.d.ts +25 -0
  36. package/dist/ui/layouts/seo-head.svelte +65 -0
  37. package/dist/ui/layouts/seo-head.svelte.d.ts +11 -0
  38. package/dist/ui/markdown/h2.svelte +7 -3
  39. package/dist/ui/markdown/h3.svelte +7 -3
  40. package/dist/ui/markdown/pre.svelte +1 -1
  41. package/dist/ui/prop.svelte +71 -36
  42. package/dist/ui/prop.svelte.d.ts +1 -1
  43. package/dist/ui/props-table.svelte +10 -62
  44. package/dist/ui/search-trigger.svelte +31 -0
  45. package/dist/ui/search-trigger.svelte.d.ts +6 -0
  46. package/dist/ui/search.svelte +167 -0
  47. package/dist/ui/search.svelte.d.ts +13 -0
  48. package/dist/ui/shadcn/accordion/accordion.svelte.d.ts +1 -1
  49. package/dist/ui/shadcn/command/command-dialog.svelte +42 -0
  50. package/dist/ui/shadcn/command/command-dialog.svelte.d.ts +14 -0
  51. package/dist/ui/shadcn/command/command-empty.svelte +17 -0
  52. package/dist/ui/shadcn/command/command-empty.svelte.d.ts +4 -0
  53. package/dist/ui/shadcn/command/command-group.svelte +33 -0
  54. package/dist/ui/shadcn/command/command-group.svelte.d.ts +7 -0
  55. package/dist/ui/shadcn/command/command-input.svelte +36 -0
  56. package/dist/ui/shadcn/command/command-input.svelte.d.ts +4 -0
  57. package/dist/ui/shadcn/command/command-item.svelte +27 -0
  58. package/dist/ui/shadcn/command/command-item.svelte.d.ts +4 -0
  59. package/dist/ui/shadcn/command/command-link-item.svelte +20 -0
  60. package/dist/ui/shadcn/command/command-link-item.svelte.d.ts +4 -0
  61. package/dist/ui/shadcn/command/command-list.svelte +20 -0
  62. package/dist/ui/shadcn/command/command-list.svelte.d.ts +4 -0
  63. package/dist/ui/shadcn/command/command-loading.svelte +7 -0
  64. package/dist/ui/shadcn/command/command-loading.svelte.d.ts +4 -0
  65. package/dist/ui/shadcn/command/command-separator.svelte +17 -0
  66. package/dist/ui/shadcn/command/command-separator.svelte.d.ts +4 -0
  67. package/dist/ui/shadcn/command/command-shortcut.svelte +23 -0
  68. package/dist/ui/shadcn/command/command-shortcut.svelte.d.ts +5 -0
  69. package/dist/ui/shadcn/command/command.svelte +28 -0
  70. package/dist/ui/shadcn/command/command.svelte.d.ts +8 -0
  71. package/dist/ui/shadcn/command/index.d.ts +12 -0
  72. package/dist/ui/shadcn/command/index.js +14 -0
  73. package/dist/ui/shadcn/dialog/dialog-close.svelte +11 -0
  74. package/dist/ui/shadcn/dialog/dialog-close.svelte.d.ts +4 -0
  75. package/dist/ui/shadcn/dialog/dialog-content.svelte +48 -0
  76. package/dist/ui/shadcn/dialog/dialog-content.svelte.d.ts +13 -0
  77. package/dist/ui/shadcn/dialog/dialog-description.svelte +20 -0
  78. package/dist/ui/shadcn/dialog/dialog-description.svelte.d.ts +4 -0
  79. package/dist/ui/shadcn/dialog/dialog-footer.svelte +32 -0
  80. package/dist/ui/shadcn/dialog/dialog-footer.svelte.d.ts +8 -0
  81. package/dist/ui/shadcn/dialog/dialog-header.svelte +20 -0
  82. package/dist/ui/shadcn/dialog/dialog-header.svelte.d.ts +5 -0
  83. package/dist/ui/shadcn/dialog/dialog-overlay.svelte +20 -0
  84. package/dist/ui/shadcn/dialog/dialog-overlay.svelte.d.ts +4 -0
  85. package/dist/ui/shadcn/dialog/dialog-portal.svelte +7 -0
  86. package/dist/ui/shadcn/dialog/dialog-portal.svelte.d.ts +3 -0
  87. package/dist/ui/shadcn/dialog/dialog-title.svelte +17 -0
  88. package/dist/ui/shadcn/dialog/dialog-title.svelte.d.ts +4 -0
  89. package/dist/ui/shadcn/dialog/dialog-trigger.svelte +11 -0
  90. package/dist/ui/shadcn/dialog/dialog-trigger.svelte.d.ts +4 -0
  91. package/dist/ui/shadcn/dialog/dialog.svelte +7 -0
  92. package/dist/ui/shadcn/dialog/dialog.svelte.d.ts +3 -0
  93. package/dist/ui/shadcn/dialog/index.d.ts +11 -0
  94. package/dist/ui/shadcn/dialog/index.js +13 -0
  95. package/dist/ui/shadcn/input/index.d.ts +2 -0
  96. package/dist/ui/shadcn/input/index.js +4 -0
  97. package/dist/ui/shadcn/input/input.svelte +48 -0
  98. package/dist/ui/shadcn/input/input.svelte.d.ts +13 -0
  99. package/dist/ui/shadcn/input-group/index.d.ts +7 -0
  100. package/dist/ui/shadcn/input-group/index.js +9 -0
  101. package/dist/ui/shadcn/input-group/input-group-addon.svelte +53 -0
  102. package/dist/ui/shadcn/input-group/input-group-addon.svelte.d.ts +32 -0
  103. package/dist/ui/shadcn/input-group/input-group-button.svelte +49 -0
  104. package/dist/ui/shadcn/input-group/input-group-button.svelte.d.ts +32 -0
  105. package/dist/ui/shadcn/input-group/input-group-input.svelte +23 -0
  106. package/dist/ui/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  107. package/dist/ui/shadcn/input-group/input-group-text.svelte +22 -0
  108. package/dist/ui/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  109. package/dist/ui/shadcn/input-group/input-group-textarea.svelte +23 -0
  110. package/dist/ui/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  111. package/dist/ui/shadcn/input-group/input-group.svelte +24 -0
  112. package/dist/ui/shadcn/input-group/input-group.svelte.d.ts +5 -0
  113. package/dist/ui/shadcn/tabs/tabs.svelte.d.ts +1 -1
  114. package/dist/ui/shadcn/textarea/index.d.ts +2 -0
  115. package/dist/ui/shadcn/textarea/index.js +4 -0
  116. package/dist/ui/shadcn/textarea/textarea.svelte +23 -0
  117. package/dist/ui/shadcn/textarea/textarea.svelte.d.ts +5 -0
  118. package/dist/ui/tab-item.svelte +35 -9
  119. package/dist/ui/tab-item.svelte.d.ts +9 -4
  120. package/dist/ui/tabs-phase.svelte +19 -0
  121. package/dist/ui/tabs-phase.svelte.d.ts +9 -0
  122. package/dist/ui/tabs.svelte +47 -24
  123. package/dist/ui/tabs.svelte.d.ts +16 -6
  124. package/dist/vite.d.ts +8 -6
  125. package/dist/vite.js +140 -47
  126. package/package.json +8 -2
package/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
6
  The documentation framework for Svelte 5 library authors whose interactive
7
- examples need to live inside one real, stateful SvelteKit app not sandboxed as
7
+ examples need to live inside one real, stateful SvelteKit app, not sandboxed as
8
8
  isolated islands.
9
9
 
10
10
  Write a markdown file under `src/routes/docs/` and you get a styled page with
11
11
  syntax highlighting, heading anchors, a sidebar derived from your content, and a
12
- live table of contents no per-page wiring, no content collection to configure.
12
+ live table of contents. No per-page wiring, no content collection to configure.
13
13
 
14
14
  > **Status: pre-1.0.** Published to npm and usable today. The public API is still
15
15
  > stabilising, so minor releases may include breaking changes until v1.0.
@@ -20,14 +20,14 @@ live table of contents — no per-page wiring, no content collection to configur
20
20
  npm install svelte-docsmith
21
21
  ```
22
22
 
23
- Peer dependencies: Svelte 5, SvelteKit 2, and Tailwind v4 set up the standard
23
+ Peer dependencies: Svelte 5, SvelteKit 2, and Tailwind v4, set up the standard
24
24
  way in your app.
25
25
 
26
26
  ## Setup
27
27
 
28
28
  Three small pieces, once.
29
29
 
30
- **1. The markdown pipeline** in `svelte.config.js`. One call bundles mdsvex,
30
+ **1. The markdown pipeline.** In `svelte.config.js`, one call bundles mdsvex,
31
31
  Shiki highlighting (dual light/dark themes, a generous language set, plain-text
32
32
  fallback for unknown languages), heading anchors, and the packaged page layout:
33
33
 
@@ -44,9 +44,9 @@ export default {
44
44
  };
45
45
  ```
46
46
 
47
- **2. The Vite plugin** in `vite.config.ts`. It scans your doc pages'
48
- frontmatter into the `svelte-docsmith/content` module (so the sidebar is derived
49
- from content, never hand-written) and powers `LiveExample`:
47
+ **2. The Vite plugin.** In `vite.config.ts`, it scans your doc pages' frontmatter
48
+ into the `svelte-docsmith/content` module (so the sidebar is derived from
49
+ content, never hand-written), builds the search index, and powers `LiveExample`:
50
50
 
51
51
  ```ts
52
52
  // vite.config.ts
@@ -63,9 +63,9 @@ export default defineConfig({
63
63
  By default it scans `src/routes/docs`; pass `docsmith({ content: 'src/routes/guide' })`
64
64
  to point elsewhere.
65
65
 
66
- **3. The stylesheet** in your root `app.css`. Your app needs Tailwind v4 set up
67
- the standard way (`tailwindcss` + the `@tailwindcss/vite` plugin, stylesheet
68
- imported in the root layout). After that the whole style contract is one import:
66
+ **3. The stylesheet.** In your root `app.css`, once Tailwind v4 is set up the
67
+ standard way (`tailwindcss` + the `@tailwindcss/vite` plugin, stylesheet imported
68
+ in the root layout), the whole style contract is one import:
69
69
 
70
70
  ```css
71
71
  @import 'tailwindcss';
@@ -75,13 +75,14 @@ imported in the root layout). After that the whole style contract is one import:
75
75
  `theme.css` makes Tailwind scan the package, defines the shadcn theme tokens
76
76
  (`--background`, `--primary`, `--radius`, …) for `:root` and `.dark`, and pulls
77
77
  in the typography and animation plugins. Override any token by redefining it
78
- after the import.
78
+ after the import, or import a preset (see [Themes](#themes)).
79
79
 
80
80
  ## The shell
81
81
 
82
82
  Add `DocsShell` once, in `src/routes/docs/+layout.svelte`. It composes the
83
83
  header, sidebar, content area, and table of contents. `docs` is the generated
84
- content index no import of a content collection, no alias to configure:
84
+ content index, so there is no content collection to import and no alias to
85
+ configure:
85
86
 
86
87
  ```svelte
87
88
  <script lang="ts">
@@ -103,7 +104,7 @@ content index — no import of a content collection, no alias to configure:
103
104
  ## Doc pages
104
105
 
105
106
  Each page is a `+page.md` under `src/routes/docs/`. Frontmatter drives the
106
- sidebar; the body is markdown:
107
+ sidebar and the `<h1>`; the body is markdown, starting at `##`:
107
108
 
108
109
  ````md
109
110
  ---
@@ -122,12 +123,12 @@ npm install my-library
122
123
 
123
124
  `title` names the page. `section` groups pages in the sidebar; `order` sorts
124
125
  within a group (and orders the groups by their smallest `order`). Add the file
125
- and it appears in the sidebar styled, highlighted, with a table of contents.
126
+ and it appears in the sidebar, styled, highlighted, with a table of contents.
126
127
 
127
128
  ## Live examples
128
129
 
129
130
  `LiveExample` renders a real, interactive component next to its own
130
- syntax-highlighted source one file, imported twice, so the demo and its code
131
+ syntax-highlighted source: one file, imported twice, so the demo and its code
131
132
  can never drift. The `?source` import is served by the same `docsmith()` Vite
132
133
  plugin you already added:
133
134
 
@@ -143,20 +144,51 @@ plugin you already added:
143
144
  </LiveExample>
144
145
  ```
145
146
 
147
+ ## Batteries included
148
+
149
+ - **Search.** A ⌘K / Ctrl-K command palette over a build-time full-text index, no
150
+ service to host. Pass a `search` loader to `DocsShell`:
151
+ `search={() => import('svelte-docsmith/search').then((m) => m.docs)}`.
152
+ - **SEO.** `DocsShell` writes `<title>`, meta description, canonical, and Open
153
+ Graph / Twitter tags for every page from its frontmatter, no per-page wiring.
154
+ - **Error pages.** `ErrorPage` gives a styled 404 that keeps the site chrome;
155
+ drop it into a SvelteKit `+error.svelte`.
156
+ - **Components.** `Callout`, `Tabs`, `Steps`, `Card`, `Accordion`, `Badge`,
157
+ `Kbd`, `FileTree`, `PropsTable`, and more, authored right inside markdown.
158
+ - Heading anchors, a scroll-spy table of contents, and one-click copy on every
159
+ code block, wired up for you.
160
+
161
+ ## Themes
162
+
163
+ Eleven presets ship in the box. Pick one with a single import after the base
164
+ contract:
165
+
166
+ ```css
167
+ @import 'svelte-docsmith/theme.css';
168
+ @import 'svelte-docsmith/themes/amethyst.css';
169
+ ```
170
+
171
+ Available: `darkmatter`, `tangerine`, `amethyst`, `graphite`, `evergreen`,
172
+ `rose`, `ocean`, `nord`, `claude`, `bubblegum`, `mono`. Or skip the preset and
173
+ override the tokens yourself.
174
+
146
175
  ## What's exported
147
176
 
148
- | Entry point | What it is |
149
- | ---------------------------- | -------------------------------------------------------------------------------------------------------- |
150
- | `svelte-docsmith` | `DocsShell`, `LiveExample`, `Tabs`, `TabItem`, `defineConfig` + `DocsmithConfig`/`DocsContentItem` types |
151
- | `svelte-docsmith/preprocess` | `docsmith()` the mdsvex/Shiki pipeline (Node, config time) |
152
- | `svelte-docsmith/vite` | `docsmith()` content index + `?source` (Node, build time) |
153
- | `svelte-docsmith/content` | `docs` the generated sidebar content index |
154
- | `svelte-docsmith/theme.css` | the style contract |
177
+ | Entry point | What it is |
178
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
179
+ | `svelte-docsmith` | All components (`DocsShell`, `ErrorPage`, `LiveExample`, `Callout`, `Tabs`, …), plus `defineConfig`, `createSearchEngine`, and the types |
180
+ | `svelte-docsmith/preprocess` | `docsmith()`, the mdsvex/Shiki pipeline (Node, config time) |
181
+ | `svelte-docsmith/vite` | `docsmith()`, the content index, search index, and `?source` transform (Node, build) |
182
+ | `svelte-docsmith/content` | `docs`, the generated sidebar content index |
183
+ | `svelte-docsmith/search` | `docs`, the generated full-text search index (lazy-load it) |
184
+ | `svelte-docsmith/theme.css` | the base style contract |
185
+ | `svelte-docsmith/themes/*.css` | the eleven theme presets |
155
186
 
156
187
  The vendored shadcn primitives and internal helpers (the TOC engine, clipboard
157
- utility, markdown renderer map) are **not** part of the public API they can
158
- change between releases. Get buttons, cards, etc. from `shadcn-svelte` directly.
188
+ utility, markdown renderer map) are **not** part of the public API; they can
189
+ change between releases. Get buttons, cards, and the like from `shadcn-svelte`
190
+ directly.
159
191
 
160
192
  ## License
161
193
 
162
- MIT see [LICENSE](LICENSE).
194
+ MIT. See [LICENSE](LICENSE).
@@ -1,6 +1,6 @@
1
1
  export declare function useClipboard(timeout?: number): {
2
2
  readText: (node: HTMLElement) => void;
3
- copy: () => void;
3
+ copy: () => Promise<void>;
4
4
  readonly currentText: string;
5
5
  readonly copied: boolean;
6
6
  };
@@ -4,14 +4,21 @@ export function useClipboard(timeout = 1500) {
4
4
  function readText(node) {
5
5
  text = node.innerText.trim();
6
6
  }
7
- function copy() {
8
- if (text) {
9
- navigator.clipboard.writeText(text).then(() => {
10
- copied = true;
11
- setTimeout(() => {
12
- copied = false;
13
- }, timeout);
14
- });
7
+ async function copy() {
8
+ if (!text)
9
+ return;
10
+ // `navigator.clipboard` is undefined in insecure (non-HTTPS) contexts, and
11
+ // writeText can reject when the page lacks clipboard permission. Fail quietly
12
+ // so a copy button never throws an unhandled rejection.
13
+ try {
14
+ await navigator.clipboard.writeText(text);
15
+ copied = true;
16
+ setTimeout(() => {
17
+ copied = false;
18
+ }, timeout);
19
+ }
20
+ catch (error) {
21
+ console.warn('[svelte-docsmith] copy to clipboard failed', error);
15
22
  }
16
23
  }
17
24
  return {
package/dist/config.d.ts CHANGED
@@ -15,8 +15,20 @@ export type DocsmithFooterColumn = {
15
15
  links: DocsmithLink[];
16
16
  };
17
17
  export type DocsmithConfig = {
18
- /** Site title, shown in the header/sidebar. */
18
+ /** Site title, shown in the header/sidebar and in the `<title>` suffix. */
19
19
  title: string;
20
+ /**
21
+ * Default meta description, used for pages whose frontmatter has none (and
22
+ * as the site's social-share description).
23
+ */
24
+ description?: string;
25
+ /**
26
+ * Canonical site origin, e.g. `https://docs.example.com`. When set, DocsShell
27
+ * emits `<link rel="canonical">` and absolute Open Graph URLs.
28
+ */
29
+ url?: string;
30
+ /** Default social-share image (absolute URL, or a path resolved against `url`). */
31
+ ogImage?: string;
20
32
  /** Optional GitHub URL; renders a link in the header when set. */
21
33
  github?: string;
22
34
  /** Optional version string, shown in the header (e.g. your library version). */
@@ -31,6 +43,11 @@ export type DocsmithConfig = {
31
43
  copyright?: string;
32
44
  /** Titled columns of links. */
33
45
  columns?: DocsmithFooterColumn[];
46
+ /**
47
+ * Show the "Powered by Svelte DocSmith" attribution in the footer.
48
+ * Defaults to `true`; set `false` to hide it.
49
+ */
50
+ poweredBy?: boolean;
34
51
  };
35
52
  };
36
53
  /**
@@ -41,8 +58,9 @@ export type DocsmithConfig = {
41
58
  export declare function defineConfig(config: DocsmithConfig): DocsmithConfig;
42
59
  /**
43
60
  * The minimal shape `DocsShell` needs from each content entry to build the
44
- * sidebar. A velite `docs` collection with title/section/order/path frontmatter
45
- * satisfies this structurally.
61
+ * sidebar. The generated `svelte-docsmith/content` module produces these from
62
+ * your pages' frontmatter; any source with matching title/path/section/order
63
+ * fields satisfies it structurally.
46
64
  */
47
65
  export type DocsContentItem = {
48
66
  title: string;
@@ -61,6 +79,23 @@ export type DocsContentItem = {
61
79
  depth: 2 | 3;
62
80
  }[];
63
81
  };
82
+ /**
83
+ * One page's searchable record, emitted at build time by the `docsmith()` vite
84
+ * plugin as the `svelte-docsmith/search` virtual module. `text` is the page's
85
+ * prose and headings reduced to plain text (frontmatter, `<script>` blocks,
86
+ * fenced code, tags, and markdown punctuation stripped) so a client-side search
87
+ * index can be built over it without shipping raw markdown.
88
+ */
89
+ export type SearchDoc = {
90
+ path: string;
91
+ title: string;
92
+ section?: string;
93
+ description?: string;
94
+ /** The page's `h2`/`h3` heading text, for weighting title/heading matches. */
95
+ headings: string[];
96
+ /** Plain-text body: prose and headings, code and markup removed. */
97
+ text: string;
98
+ };
64
99
  /** A single sidebar link. */
65
100
  export type NavItem = {
66
101
  title: string;
@@ -72,8 +107,8 @@ export type NavGroup = {
72
107
  items: NavItem[];
73
108
  };
74
109
  /**
75
- * Derive sidebar nav from a content collection (PLAN.md §2.6): group by
76
- * `section`, order by `order` within a group, and order groups by the smallest
77
- * `order` they contain. Entries without a `section` fall under "Docs".
110
+ * Derive sidebar nav from a content collection: group by `section`, order by
111
+ * `order` within a group, and order groups by the smallest `order` they
112
+ * contain. Entries without a `section` fall under "Docs".
78
113
  */
79
114
  export declare function navFromContent(content: DocsContentItem[]): NavGroup[];
package/dist/config.js CHANGED
@@ -10,7 +10,7 @@ export function defineConfig(config) {
10
10
  if (typeof config.title !== 'string' || config.title.trim() === '') {
11
11
  throw new Error('[svelte-docsmith] config.title is required — the site title shown in the sidebar header.');
12
12
  }
13
- for (const key of ['github', 'version', 'logo']) {
13
+ for (const key of ['github', 'version', 'logo', 'description', 'url', 'ogImage']) {
14
14
  if (config[key] !== undefined && typeof config[key] !== 'string') {
15
15
  throw new Error(`[svelte-docsmith] config.${key} must be a string when set.`);
16
16
  }
@@ -25,16 +25,42 @@ export function defineConfig(config) {
25
25
  }
26
26
  }
27
27
  }
28
- if (config.footer !== undefined &&
29
- (typeof config.footer !== 'object' || config.footer === null)) {
30
- throw new Error('[svelte-docsmith] config.footer must be an object ({ copyright?, columns? }).');
28
+ if (config.footer !== undefined) {
29
+ if (typeof config.footer !== 'object' || config.footer === null) {
30
+ throw new Error('[svelte-docsmith] config.footer must be an object ({ copyright?, columns?, poweredBy? }).');
31
+ }
32
+ const { copyright, columns, poweredBy } = config.footer;
33
+ if (copyright !== undefined && typeof copyright !== 'string') {
34
+ throw new Error('[svelte-docsmith] config.footer.copyright must be a string when set.');
35
+ }
36
+ if (poweredBy !== undefined && typeof poweredBy !== 'boolean') {
37
+ throw new Error('[svelte-docsmith] config.footer.poweredBy must be a boolean when set.');
38
+ }
39
+ if (columns !== undefined) {
40
+ if (!Array.isArray(columns)) {
41
+ throw new Error('[svelte-docsmith] config.footer.columns must be an array of { title, links } columns.');
42
+ }
43
+ for (const column of columns) {
44
+ if (typeof column?.title !== 'string') {
45
+ throw new Error('[svelte-docsmith] each config.footer.columns entry needs a string `title`.');
46
+ }
47
+ if (!Array.isArray(column.links)) {
48
+ throw new Error(`[svelte-docsmith] config.footer column "${column.title}" needs a \`links\` array.`);
49
+ }
50
+ for (const link of column.links) {
51
+ if (typeof link?.label !== 'string' || typeof link?.href !== 'string') {
52
+ throw new Error(`[svelte-docsmith] each link in config.footer column "${column.title}" needs a string \`label\` and \`href\`.`);
53
+ }
54
+ }
55
+ }
56
+ }
31
57
  }
32
58
  return config;
33
59
  }
34
60
  /**
35
- * Derive sidebar nav from a content collection (PLAN.md §2.6): group by
36
- * `section`, order by `order` within a group, and order groups by the smallest
37
- * `order` they contain. Entries without a `section` fall under "Docs".
61
+ * Derive sidebar nav from a content collection: group by `section`, order by
62
+ * `order` within a group, and order groups by the smallest `order` they
63
+ * contain. Entries without a `section` fall under "Docs".
38
64
  */
39
65
  export function navFromContent(content) {
40
66
  const groups = new Map();
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  export { default as DocsShell } from './ui/layouts/docs-shell.svelte';
2
+ export { default as ErrorPage } from './ui/layouts/error-page.svelte';
2
3
  export { default as ThemeProvider } from './ui/theme-provider.svelte';
3
4
  export { default as ThemeToggle } from './ui/theme-toggle.svelte';
4
5
  export { default as LiveExample } from './ui/live-example.svelte';
5
6
  export { default as Tabs } from './ui/tabs.svelte';
6
7
  export { default as TabItem } from './ui/tab-item.svelte';
7
- export { default as Callout } from './ui/callout.svelte';
8
+ export { default as Callout, type CalloutVariant } from './ui/callout.svelte';
8
9
  export { default as Steps } from './ui/steps.svelte';
9
10
  export { default as Step } from './ui/step.svelte';
10
11
  export { default as Card } from './ui/card.svelte';
@@ -17,4 +18,5 @@ export { default as FileTree } from './ui/file-tree.svelte';
17
18
  export { default as FileTreeItem } from './ui/file-tree-item.svelte';
18
19
  export { default as PropsTable } from './ui/props-table.svelte';
19
20
  export { default as Prop } from './ui/prop.svelte';
20
- export { defineConfig, type DocsmithConfig, type DocsContentItem } from './config.js';
21
+ export { defineConfig, type DocsmithConfig, type DocsContentItem, type SearchDoc } from './config.js';
22
+ export { createSearchEngine, type SearchEngine, type SearchResult } from './search/create-search.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // renderer map — is internal and may change between releases.
4
4
  // Chrome
5
5
  export { default as DocsShell } from './ui/layouts/docs-shell.svelte';
6
+ export { default as ErrorPage } from './ui/layouts/error-page.svelte';
6
7
  export { default as ThemeProvider } from './ui/theme-provider.svelte';
7
8
  export { default as ThemeToggle } from './ui/theme-toggle.svelte';
8
9
  // Docs components (authored inside markdown or composed in pages)
@@ -24,3 +25,6 @@ export { default as PropsTable } from './ui/props-table.svelte';
24
25
  export { default as Prop } from './ui/prop.svelte';
25
26
  // Config
26
27
  export { defineConfig } from './config.js';
28
+ // Search engine (framework-agnostic; for building a custom search UI over the
29
+ // generated `svelte-docsmith/search` index)
30
+ export { createSearchEngine } from './search/create-search.js';
@@ -13,9 +13,22 @@
13
13
  <script lang="ts">
14
14
  import type { Snippet } from 'svelte';
15
15
 
16
- const { children }: { children: Snippet } = $props();
16
+ // mdsvex passes each frontmatter field to the layout as a prop, so the page's
17
+ // `<h1>` (and lead subtitle) come from `title`/`description` — authored once in
18
+ // frontmatter, never repeated in the body. Pages start their content at `##`.
19
+ const {
20
+ title,
21
+ description,
22
+ children
23
+ }: { title?: string; description?: string; children: Snippet } = $props();
17
24
  </script>
18
25
 
19
26
  <article class="prose prose-base dark:prose-invert max-w-none pb-16">
27
+ {#if title}
28
+ <h1>{title}</h1>
29
+ {/if}
30
+ {#if description}
31
+ <p class="lead">{description}</p>
32
+ {/if}
20
33
  {@render children()}
21
34
  </article>
@@ -11,6 +11,8 @@ export declare const table: import("svelte").Component<{
11
11
  }, {}, "">;
12
12
  import type { Snippet } from 'svelte';
13
13
  type $$ComponentProps = {
14
+ title?: string;
15
+ description?: string;
14
16
  children: Snippet;
15
17
  };
16
18
  declare const MarkdownLayout: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -24,10 +24,9 @@ export interface DocsmithPreprocessOptions {
24
24
  rehypePlugins?: MdsvexOptions['rehypePlugins'];
25
25
  }
26
26
  /**
27
- * Build the svelte-docsmith preprocessor: mdsvex with Shiki highlighting
28
- * (raw-code `highlight` hook the correct mdsvex integration point, see the
29
- * repo's PLAN.md §4.5), heading slugs/anchor sections, and the packaged doc
30
- * layout. Unknown code-fence languages fall back to plain text instead of
31
- * failing the build.
27
+ * Build the svelte-docsmith preprocessor: mdsvex with Shiki highlighting (via
28
+ * the raw-code `highlight` hook, the correct mdsvex integration point), heading
29
+ * slugs/anchor sections, and the packaged doc layout. Unknown code-fence
30
+ * languages fall back to plain text instead of failing the build.
32
31
  */
33
32
  export declare function docsmith(options?: DocsmithPreprocessOptions): PreprocessorGroup;
@@ -12,11 +12,10 @@ import { fileURLToPath } from 'node:url';
12
12
  import rehypeSlug from 'rehype-slug';
13
13
  import { DEFAULT_LANGS, DEFAULT_THEMES, lazyHighlighter } from './highlight.js';
14
14
  /**
15
- * Build the svelte-docsmith preprocessor: mdsvex with Shiki highlighting
16
- * (raw-code `highlight` hook the correct mdsvex integration point, see the
17
- * repo's PLAN.md §4.5), heading slugs/anchor sections, and the packaged doc
18
- * layout. Unknown code-fence languages fall back to plain text instead of
19
- * failing the build.
15
+ * Build the svelte-docsmith preprocessor: mdsvex with Shiki highlighting (via
16
+ * the raw-code `highlight` hook, the correct mdsvex integration point), heading
17
+ * slugs/anchor sections, and the packaged doc layout. Unknown code-fence
18
+ * languages fall back to plain text instead of failing the build.
20
19
  */
21
20
  export function docsmith(options = {}) {
22
21
  const themes = options.themes ?? DEFAULT_THEMES;
@@ -0,0 +1,7 @@
1
+ export declare class SearchState {
2
+ open: boolean;
3
+ }
4
+ /** Create the search state and publish it on context. Call in `DocsShell`. */
5
+ export declare function createSearchState(): SearchState;
6
+ /** Read the search state, or `undefined` when search is not enabled. */
7
+ export declare function useSearch(): SearchState | undefined;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared open-state for the search palette. `DocsShell` creates it once; the
3
+ * header/mobile-header triggers and the single `<Search>` dialog read it through
4
+ * context, so one keyboard shortcut and one dialog serve every trigger.
5
+ */
6
+ import { getContext, setContext } from 'svelte';
7
+ const KEY = Symbol('docsmith-search');
8
+ export class SearchState {
9
+ open = $state(false);
10
+ }
11
+ /** Create the search state and publish it on context. Call in `DocsShell`. */
12
+ export function createSearchState() {
13
+ const state = new SearchState();
14
+ setContext(KEY, state);
15
+ return state;
16
+ }
17
+ /** Read the search state, or `undefined` when search is not enabled. */
18
+ export function useSearch() {
19
+ return getContext(KEY);
20
+ }
@@ -0,0 +1,14 @@
1
+ import type { SearchDoc } from '../config.js';
2
+ /** One result row: enough to render a link and a preview. */
3
+ export type SearchResult = {
4
+ path: string;
5
+ title: string;
6
+ section?: string;
7
+ snippet: string;
8
+ };
9
+ export type SearchEngine = {
10
+ /** Ranked results for `query`, at most `limit` (default 8). */
11
+ search(query: string, limit?: number): SearchResult[];
12
+ };
13
+ /** Build a search engine over `docs`. Indexing is synchronous and one-time. */
14
+ export declare function createSearchEngine(docs: SearchDoc[]): SearchEngine;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The client-side search engine. Builds a FlexSearch document index over the
3
+ * generated `svelte-docsmith/search` records (title, headings, description, and
4
+ * plain-text body) and returns a `search()` that maps hits back to page links
5
+ * with a context snippet. Framework-agnostic on purpose so it can be unit-tested
6
+ * without a DOM; the `<Search>` component lazy-loads both this and the index.
7
+ */
8
+ import { Document } from 'flexsearch';
9
+ import { buildSnippet } from './snippet.js';
10
+ /** Build a search engine over `docs`. Indexing is synchronous and one-time. */
11
+ export function createSearchEngine(docs) {
12
+ const index = new Document({
13
+ tokenize: 'forward',
14
+ document: {
15
+ id: 'id',
16
+ // Title and headings first so their matches outrank body matches.
17
+ index: [
18
+ { field: 'title' },
19
+ { field: 'headings' },
20
+ { field: 'description' },
21
+ { field: 'text' }
22
+ ]
23
+ }
24
+ });
25
+ docs.forEach((doc, id) => {
26
+ index.add({
27
+ id,
28
+ title: doc.title,
29
+ headings: doc.headings.join(' '),
30
+ description: doc.description ?? '',
31
+ text: doc.text
32
+ });
33
+ });
34
+ return {
35
+ search(query, limit = 8) {
36
+ const trimmed = query.trim();
37
+ if (!trimmed)
38
+ return [];
39
+ // `merge: true` returns unified, unique ids ranked across fields.
40
+ const hits = index.search(trimmed, { limit, merge: true });
41
+ const results = [];
42
+ for (const { id } of hits) {
43
+ const doc = docs[id];
44
+ if (!doc)
45
+ continue;
46
+ results.push({
47
+ path: doc.path,
48
+ title: doc.title,
49
+ section: doc.section,
50
+ snippet: buildSnippet(doc.text || doc.description || '', trimmed)
51
+ });
52
+ }
53
+ return results;
54
+ }
55
+ };
56
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Build a short plain-text excerpt of `text` centered on the first matching
3
+ * query term, for showing context under a search result. Falls back to the head
4
+ * of the text when nothing matches. Word boundaries are respected so the excerpt
5
+ * never starts or ends mid-word, and elisions are marked with an ellipsis.
6
+ */
7
+ export declare function buildSnippet(text: string, query: string, maxLength?: number): string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Build a short plain-text excerpt of `text` centered on the first matching
3
+ * query term, for showing context under a search result. Falls back to the head
4
+ * of the text when nothing matches. Word boundaries are respected so the excerpt
5
+ * never starts or ends mid-word, and elisions are marked with an ellipsis.
6
+ */
7
+ export function buildSnippet(text, query, maxLength = 160) {
8
+ const clean = text.replace(/\s+/g, ' ').trim();
9
+ if (!clean)
10
+ return '';
11
+ const haystack = clean.toLowerCase();
12
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
13
+ let match = -1;
14
+ for (const term of terms) {
15
+ const i = haystack.indexOf(term);
16
+ if (i !== -1 && (match === -1 || i < match))
17
+ match = i;
18
+ }
19
+ if (match === -1) {
20
+ return clean.length > maxLength ? clean.slice(0, maxLength).trimEnd() + '…' : clean;
21
+ }
22
+ // Window the text around the match, then snap the edges to word boundaries.
23
+ const half = Math.floor(maxLength / 2);
24
+ let end = Math.min(clean.length, Math.max(match + half, maxLength));
25
+ let start = Math.max(0, end - maxLength);
26
+ end = Math.min(clean.length, start + maxLength);
27
+ if (start > 0) {
28
+ const space = clean.indexOf(' ', start);
29
+ if (space !== -1 && space < match)
30
+ start = space + 1;
31
+ }
32
+ if (end < clean.length) {
33
+ const space = clean.lastIndexOf(' ', end);
34
+ if (space > match)
35
+ end = space;
36
+ }
37
+ let snippet = clean.slice(start, end).trim();
38
+ if (start > 0)
39
+ snippet = '…' + snippet;
40
+ if (end < clean.length)
41
+ snippet = snippet + '…';
42
+ return snippet;
43
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `svelte-docsmith/search` — the generated search index.
3
+ *
4
+ * At build time the `docsmith()` Vite plugin (in `vite.config.ts`) intercepts
5
+ * this import and replaces it with your doc pages' plain-text bodies. This file
6
+ * is only the fallback that runs when the plugin is missing — it exists so the
7
+ * import type-checks and fails with a clear message instead of a mystery.
8
+ */
9
+ import type { SearchDoc } from './config.js';
10
+ export declare const docs: SearchDoc[];
package/dist/search.js ADDED
@@ -0,0 +1,4 @@
1
+ export const docs = [];
2
+ throw new Error("[svelte-docsmith] 'svelte-docsmith/search' requires the docsmith() plugin. " +
3
+ "Add it to your vite.config: import { docsmith } from 'svelte-docsmith/vite'; " +
4
+ 'plugins: [docsmith(), tailwindcss(), sveltekit()]');
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);