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/README.md CHANGED
@@ -4,30 +4,39 @@
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.
16
16
 
17
17
  ## Install
18
18
 
19
+ The fastest way to start a new docs site is the scaffolder, which wires up
20
+ everything below for you:
21
+
22
+ ```bash
23
+ npm create svelte-docsmith@latest my-docs
24
+ ```
25
+
26
+ To add DocSmith to an existing SvelteKit app, install the package instead:
27
+
19
28
  ```bash
20
29
  npm install svelte-docsmith
21
30
  ```
22
31
 
23
- Peer dependencies: Svelte 5, SvelteKit 2, and Tailwind v4 set up the standard
32
+ Peer dependencies: Svelte 5, SvelteKit 2, and Tailwind v4, set up the standard
24
33
  way in your app.
25
34
 
26
35
  ## Setup
27
36
 
28
37
  Three small pieces, once.
29
38
 
30
- **1. The markdown pipeline** in `svelte.config.js`. One call bundles mdsvex,
39
+ **1. The markdown pipeline.** In `svelte.config.js`, one call bundles mdsvex,
31
40
  Shiki highlighting (dual light/dark themes, a generous language set, plain-text
32
41
  fallback for unknown languages), heading anchors, and the packaged page layout:
33
42
 
@@ -44,9 +53,9 @@ export default {
44
53
  };
45
54
  ```
46
55
 
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`:
56
+ **2. The Vite plugin.** In `vite.config.ts`, it scans your doc pages' frontmatter
57
+ into the `svelte-docsmith/content` module (so the sidebar is derived from
58
+ content, never hand-written), builds the search index, and powers `LiveExample`:
50
59
 
51
60
  ```ts
52
61
  // vite.config.ts
@@ -63,9 +72,9 @@ export default defineConfig({
63
72
  By default it scans `src/routes/docs`; pass `docsmith({ content: 'src/routes/guide' })`
64
73
  to point elsewhere.
65
74
 
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:
75
+ **3. The stylesheet.** In your root `app.css`, once Tailwind v4 is set up the
76
+ standard way (`tailwindcss` + the `@tailwindcss/vite` plugin, stylesheet imported
77
+ in the root layout), the whole style contract is one import:
69
78
 
70
79
  ```css
71
80
  @import 'tailwindcss';
@@ -75,13 +84,14 @@ imported in the root layout). After that the whole style contract is one import:
75
84
  `theme.css` makes Tailwind scan the package, defines the shadcn theme tokens
76
85
  (`--background`, `--primary`, `--radius`, …) for `:root` and `.dark`, and pulls
77
86
  in the typography and animation plugins. Override any token by redefining it
78
- after the import.
87
+ after the import, or import a preset (see [Themes](#themes)).
79
88
 
80
89
  ## The shell
81
90
 
82
91
  Add `DocsShell` once, in `src/routes/docs/+layout.svelte`. It composes the
83
92
  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:
93
+ content index, so there is no content collection to import and no alias to
94
+ configure:
85
95
 
86
96
  ```svelte
87
97
  <script lang="ts">
@@ -103,7 +113,7 @@ content index — no import of a content collection, no alias to configure:
103
113
  ## Doc pages
104
114
 
105
115
  Each page is a `+page.md` under `src/routes/docs/`. Frontmatter drives the
106
- sidebar; the body is markdown:
116
+ sidebar and the `<h1>`; the body is markdown, starting at `##`:
107
117
 
108
118
  ````md
109
119
  ---
@@ -122,12 +132,12 @@ npm install my-library
122
132
 
123
133
  `title` names the page. `section` groups pages in the sidebar; `order` sorts
124
134
  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.
135
+ and it appears in the sidebar, styled, highlighted, with a table of contents.
126
136
 
127
137
  ## Live examples
128
138
 
129
139
  `LiveExample` renders a real, interactive component next to its own
130
- syntax-highlighted source one file, imported twice, so the demo and its code
140
+ syntax-highlighted source: one file, imported twice, so the demo and its code
131
141
  can never drift. The `?source` import is served by the same `docsmith()` Vite
132
142
  plugin you already added:
133
143
 
@@ -143,20 +153,51 @@ plugin you already added:
143
153
  </LiveExample>
144
154
  ```
145
155
 
156
+ ## Batteries included
157
+
158
+ - **Search.** A ⌘K / Ctrl-K command palette over a build-time full-text index, no
159
+ service to host. Pass a `search` loader to `DocsShell`:
160
+ `search={() => import('svelte-docsmith/search').then((m) => m.docs)}`.
161
+ - **SEO.** `DocsShell` writes `<title>`, meta description, canonical, and Open
162
+ Graph / Twitter tags for every page from its frontmatter, no per-page wiring.
163
+ - **Error pages.** `ErrorPage` gives a styled 404 that keeps the site chrome;
164
+ drop it into a SvelteKit `+error.svelte`.
165
+ - **Components.** `Callout`, `Tabs`, `Steps`, `Card`, `Accordion`, `Badge`,
166
+ `Kbd`, `FileTree`, `PropsTable`, and more, authored right inside markdown.
167
+ - Heading anchors, a scroll-spy table of contents, and one-click copy on every
168
+ code block, wired up for you.
169
+
170
+ ## Themes
171
+
172
+ Eleven presets ship in the box. Pick one with a single import after the base
173
+ contract:
174
+
175
+ ```css
176
+ @import 'svelte-docsmith/theme.css';
177
+ @import 'svelte-docsmith/themes/amethyst.css';
178
+ ```
179
+
180
+ Available: `darkmatter`, `tangerine`, `amethyst`, `graphite`, `evergreen`,
181
+ `rose`, `ocean`, `nord`, `claude`, `bubblegum`, `mono`. Or skip the preset and
182
+ override the tokens yourself.
183
+
146
184
  ## What's exported
147
185
 
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 |
186
+ | Entry point | What it is |
187
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
188
+ | `svelte-docsmith` | All components (`DocsShell`, `ErrorPage`, `LiveExample`, `Callout`, `Tabs`, …), plus `defineConfig`, `createSearchEngine`, and the types |
189
+ | `svelte-docsmith/preprocess` | `docsmith()`, the mdsvex/Shiki pipeline (Node, config time) |
190
+ | `svelte-docsmith/vite` | `docsmith()`, the content index, search index, and `?source` transform (Node, build) |
191
+ | `svelte-docsmith/content` | `docs`, the generated sidebar content index |
192
+ | `svelte-docsmith/search` | `docs`, the generated full-text search index (lazy-load it) |
193
+ | `svelte-docsmith/theme.css` | the base style contract |
194
+ | `svelte-docsmith/themes/*.css` | the eleven theme presets |
155
195
 
156
196
  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.
197
+ utility, markdown renderer map) are **not** part of the public API; they can
198
+ change between releases. Get buttons, cards, and the like from `shadcn-svelte`
199
+ directly.
159
200
 
160
201
  ## License
161
202
 
162
- MIT see [LICENSE](LICENSE).
203
+ 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). */
@@ -46,8 +58,9 @@ export type DocsmithConfig = {
46
58
  export declare function defineConfig(config: DocsmithConfig): DocsmithConfig;
47
59
  /**
48
60
  * The minimal shape `DocsShell` needs from each content entry to build the
49
- * sidebar. A velite `docs` collection with title/section/order/path frontmatter
50
- * 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.
51
64
  */
52
65
  export type DocsContentItem = {
53
66
  title: string;
@@ -66,6 +79,23 @@ export type DocsContentItem = {
66
79
  depth: 2 | 3;
67
80
  }[];
68
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
+ };
69
99
  /** A single sidebar link. */
70
100
  export type NavItem = {
71
101
  title: string;
@@ -77,8 +107,8 @@ export type NavGroup = {
77
107
  items: NavItem[];
78
108
  };
79
109
  /**
80
- * Derive sidebar nav from a content collection (PLAN.md §2.6): group by
81
- * `section`, order by `order` within a group, and order groups by the smallest
82
- * `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".
83
113
  */
84
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;
@@ -6,17 +6,16 @@
6
6
  * doesn't care where it's registered). It must never import component code.
7
7
  */
8
8
  import rehypeSectionize from '@hbsnow/rehype-sectionize';
9
- import { transformerNotationHighlight } from '@shikijs/transformers';
9
+ import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from '@shikijs/transformers';
10
10
  import { escapeSvelte, mdsvex } from 'mdsvex';
11
11
  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;
@@ -40,7 +39,16 @@ export function docsmith(options = {}) {
40
39
  const html = escapeSvelte(highlighter.codeToHtml(code, {
41
40
  lang: language,
42
41
  themes,
43
- transformers: [transformerNotationHighlight()]
42
+ // Comment-driven annotations authors write inside the fence:
43
+ // line highlight, diff (++/--), focus, error/warning, and
44
+ // word highlight. Each strips its own marker comment.
45
+ transformers: [
46
+ transformerNotationHighlight(),
47
+ transformerNotationDiff(),
48
+ transformerNotationFocus(),
49
+ transformerNotationErrorLevel(),
50
+ transformerNotationWordHighlight()
51
+ ]
44
52
  }));
45
53
  // Without a layout there is no `Components.pre` to render through —
46
54
  // keep Shiki's own <pre> as-is.
@@ -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()]');