svelte-docsmith 0.4.0 → 0.5.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.
- package/dist/config.d.ts +27 -0
- package/dist/config.js +9 -1
- package/dist/generate-llms.d.ts +31 -0
- package/dist/generate-llms.js +63 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/llms.d.ts +11 -0
- package/dist/llms.js +4 -0
- package/dist/normalize-path.d.ts +6 -0
- package/dist/normalize-path.js +8 -0
- package/dist/sitemap.d.ts +27 -0
- package/dist/sitemap.js +44 -0
- package/dist/ui/layouts/copy-page-menu.svelte +113 -0
- package/dist/ui/layouts/copy-page-menu.svelte.d.ts +9 -0
- package/dist/ui/layouts/docs-header.svelte +1 -1
- package/dist/ui/layouts/docs-mobile-header.svelte +1 -1
- package/dist/ui/layouts/docs-shell.svelte +83 -9
- package/dist/ui/layouts/docs-shell.svelte.d.ts +6 -0
- package/dist/ui/layouts/docs-sidebar.svelte +3 -2
- package/dist/ui/layouts/prev-next-nav.svelte +9 -2
- package/dist/ui/layouts/prev-next-nav.svelte.d.ts +1 -0
- package/dist/ui/layouts/seo-head.svelte +2 -1
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +9 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-content.svelte.d.ts +10 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +8 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-group.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-item.svelte.d.ts +8 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-label.svelte +27 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-label.svelte.d.ts +8 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-portal.svelte.d.ts +3 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte +23 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte.d.ts +5 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +7 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-sub.svelte.d.ts +3 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +4 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/ui/shadcn/dropdown-menu/dropdown-menu.svelte.d.ts +3 -0
- package/dist/ui/shadcn/dropdown-menu/index.d.ts +18 -0
- package/dist/ui/shadcn/dropdown-menu/index.js +18 -0
- package/dist/vite.d.ts +8 -1
- package/dist/vite.js +60 -3
- package/package.json +7 -4
package/dist/config.d.ts
CHANGED
|
@@ -29,6 +29,13 @@ export type DocsmithConfig = {
|
|
|
29
29
|
url?: string;
|
|
30
30
|
/** Default social-share image (absolute URL, or a path resolved against `url`). */
|
|
31
31
|
ogImage?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Base URL for the "Edit this page" link, pointing at the directory that maps
|
|
34
|
+
* to your app's working directory in the repo, e.g.
|
|
35
|
+
* `https://github.com/you/repo/edit/main/apps/docs`. DocsShell appends each
|
|
36
|
+
* page's source path. Omit to hide the edit link.
|
|
37
|
+
*/
|
|
38
|
+
editUrl?: string;
|
|
32
39
|
/** Optional GitHub URL; renders a link in the header when set. */
|
|
33
40
|
github?: string;
|
|
34
41
|
/** Optional version string, shown in the header (e.g. your library version). */
|
|
@@ -68,6 +75,10 @@ export type DocsContentItem = {
|
|
|
68
75
|
section?: string;
|
|
69
76
|
order?: number;
|
|
70
77
|
description?: string;
|
|
78
|
+
/** Source file path relative to the app cwd, for the "Edit this page" link. */
|
|
79
|
+
sourcePath?: string;
|
|
80
|
+
/** Last git commit date (ISO) for the page's source file. */
|
|
81
|
+
lastUpdated?: string;
|
|
71
82
|
/**
|
|
72
83
|
* The page's `h2`/`h3` headings, extracted at build time by the `docsmith()`
|
|
73
84
|
* vite plugin, so `DocsShell` can render the in-page TOC server-side. The
|
|
@@ -96,6 +107,22 @@ export type SearchDoc = {
|
|
|
96
107
|
/** Plain-text body: prose and headings, code and markup removed. */
|
|
97
108
|
text: string;
|
|
98
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* One page's record for LLM-facing output, emitted at build time by the
|
|
112
|
+
* `docsmith()` vite plugin as the `svelte-docsmith/llms` virtual module. Feeds
|
|
113
|
+
* `generateLlmsTxt` (an index) and `generateLlmsFullTxt` (the full corpus).
|
|
114
|
+
*/
|
|
115
|
+
export type LlmsDoc = {
|
|
116
|
+
path: string;
|
|
117
|
+
title: string;
|
|
118
|
+
section?: string;
|
|
119
|
+
/** Sidebar order within the section, so LLM output follows reading order. */
|
|
120
|
+
order?: number;
|
|
121
|
+
description?: string;
|
|
122
|
+
/** The page's markdown body (frontmatter and `<script>`/`<style>` removed,
|
|
123
|
+
* headings and code kept), with the title prepended as an `h1`. */
|
|
124
|
+
content: string;
|
|
125
|
+
};
|
|
99
126
|
/** A single sidebar link. */
|
|
100
127
|
export type NavItem = {
|
|
101
128
|
title: string;
|
package/dist/config.js
CHANGED
|
@@ -10,7 +10,15 @@ 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 [
|
|
13
|
+
for (const key of [
|
|
14
|
+
'github',
|
|
15
|
+
'version',
|
|
16
|
+
'logo',
|
|
17
|
+
'description',
|
|
18
|
+
'url',
|
|
19
|
+
'ogImage',
|
|
20
|
+
'editUrl'
|
|
21
|
+
]) {
|
|
14
22
|
if (config[key] !== undefined && typeof config[key] !== 'string') {
|
|
15
23
|
throw new Error(`[svelte-docsmith] config.${key} must be a string when set.`);
|
|
16
24
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LlmsDoc } from './config.js';
|
|
2
|
+
/** Site-level info for the LLM files: title, optional description, and origin. */
|
|
3
|
+
export type LlmsSite = {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Absolute origin, e.g. `https://my-docs.dev`, used to make links absolute. */
|
|
7
|
+
origin?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Build an [llms.txt](https://llmstxt.org) index: the site title and
|
|
11
|
+
* description, then a sectioned list of links to each page. Wire it into a
|
|
12
|
+
* `src/routes/llms.txt/+server.ts` over the `svelte-docsmith/llms` module.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { docs } from 'svelte-docsmith/llms';
|
|
16
|
+
* import { generateLlmsTxt } from 'svelte-docsmith';
|
|
17
|
+
*
|
|
18
|
+
* export const prerender = true;
|
|
19
|
+
* export function GET() {
|
|
20
|
+
* const body = generateLlmsTxt({ title: 'My Docs', origin: 'https://my-docs.dev' }, docs);
|
|
21
|
+
* return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateLlmsTxt(site: LlmsSite, docs: LlmsDoc[]): string;
|
|
26
|
+
/**
|
|
27
|
+
* Build `llms-full.txt`: the site header followed by the full markdown of every
|
|
28
|
+
* page, so an LLM can ingest the entire documentation in one request. Same
|
|
29
|
+
* wiring as {@link generateLlmsTxt}, at `src/routes/llms-full.txt/+server.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function generateLlmsFullTxt(site: LlmsSite, docs: LlmsDoc[]): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group docs by section and put both the sections and the docs within them in
|
|
3
|
+
* sidebar reading order: items by their `order`, sections by the smallest
|
|
4
|
+
* `order` they contain. Mirrors `navFromContent` so the LLM index matches the
|
|
5
|
+
* on-page navigation instead of falling back to path-alphabetical order.
|
|
6
|
+
*/
|
|
7
|
+
function bySection(docs) {
|
|
8
|
+
const groups = new Map();
|
|
9
|
+
for (const doc of docs) {
|
|
10
|
+
const section = doc.section ?? 'Docs';
|
|
11
|
+
const order = doc.order ?? 0;
|
|
12
|
+
const group = groups.get(section) ?? { minOrder: Infinity, items: [] };
|
|
13
|
+
group.items.push(doc);
|
|
14
|
+
group.minOrder = Math.min(group.minOrder, order);
|
|
15
|
+
groups.set(section, group);
|
|
16
|
+
}
|
|
17
|
+
return [...groups.entries()]
|
|
18
|
+
.sort((a, b) => a[1].minOrder - b[1].minOrder)
|
|
19
|
+
.map(([section, group]) => [
|
|
20
|
+
section,
|
|
21
|
+
group.items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build an [llms.txt](https://llmstxt.org) index: the site title and
|
|
26
|
+
* description, then a sectioned list of links to each page. Wire it into a
|
|
27
|
+
* `src/routes/llms.txt/+server.ts` over the `svelte-docsmith/llms` module.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { docs } from 'svelte-docsmith/llms';
|
|
31
|
+
* import { generateLlmsTxt } from 'svelte-docsmith';
|
|
32
|
+
*
|
|
33
|
+
* export const prerender = true;
|
|
34
|
+
* export function GET() {
|
|
35
|
+
* const body = generateLlmsTxt({ title: 'My Docs', origin: 'https://my-docs.dev' }, docs);
|
|
36
|
+
* return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function generateLlmsTxt(site, docs) {
|
|
41
|
+
const base = (site.origin ?? '').replace(/\/$/, '');
|
|
42
|
+
const out = [`# ${site.title}`];
|
|
43
|
+
if (site.description)
|
|
44
|
+
out.push('', `> ${site.description}`);
|
|
45
|
+
for (const [section, items] of bySection(docs)) {
|
|
46
|
+
out.push('', `## ${section}`, '');
|
|
47
|
+
for (const doc of items) {
|
|
48
|
+
const suffix = doc.description ? `: ${doc.description}` : '';
|
|
49
|
+
out.push(`- [${doc.title}](${base}${doc.path})${suffix}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out.join('\n') + '\n';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build `llms-full.txt`: the site header followed by the full markdown of every
|
|
56
|
+
* page, so an LLM can ingest the entire documentation in one request. Same
|
|
57
|
+
* wiring as {@link generateLlmsTxt}, at `src/routes/llms-full.txt/+server.ts`.
|
|
58
|
+
*/
|
|
59
|
+
export function generateLlmsFullTxt(site, docs) {
|
|
60
|
+
const header = site.description ? `# ${site.title}\n\n> ${site.description}` : `# ${site.title}`;
|
|
61
|
+
const ordered = bySection(docs).flatMap(([, items]) => items);
|
|
62
|
+
return [header, ...ordered.map((doc) => doc.content.trim())].join('\n\n') + '\n';
|
|
63
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,5 +18,7 @@ export { default as FileTree } from './ui/file-tree.svelte';
|
|
|
18
18
|
export { default as FileTreeItem } from './ui/file-tree-item.svelte';
|
|
19
19
|
export { default as PropsTable } from './ui/props-table.svelte';
|
|
20
20
|
export { default as Prop } from './ui/prop.svelte';
|
|
21
|
-
export { defineConfig, type DocsmithConfig, type DocsContentItem, type SearchDoc } from './config.js';
|
|
21
|
+
export { defineConfig, type DocsmithConfig, type DocsContentItem, type SearchDoc, type LlmsDoc } from './config.js';
|
|
22
22
|
export { createSearchEngine, type SearchEngine, type SearchResult } from './search/create-search.js';
|
|
23
|
+
export { generateSitemap, type SitemapEntry } from './sitemap.js';
|
|
24
|
+
export { generateLlmsTxt, generateLlmsFullTxt, type LlmsSite } from './generate-llms.js';
|
package/dist/index.js
CHANGED
|
@@ -28,3 +28,8 @@ export { defineConfig } from './config.js';
|
|
|
28
28
|
// Search engine (framework-agnostic; for building a custom search UI over the
|
|
29
29
|
// generated `svelte-docsmith/search` index)
|
|
30
30
|
export { createSearchEngine } from './search/create-search.js';
|
|
31
|
+
// Sitemap (framework-agnostic; wire into a `sitemap.xml/+server.ts`)
|
|
32
|
+
export { generateSitemap } from './sitemap.js';
|
|
33
|
+
// llms.txt (framework-agnostic; wire into `llms.txt/+server.ts` and
|
|
34
|
+
// `llms-full.txt/+server.ts` over the generated `svelte-docsmith/llms` index)
|
|
35
|
+
export { generateLlmsTxt, generateLlmsFullTxt } from './generate-llms.js';
|
package/dist/llms.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `svelte-docsmith/llms` — the generated LLM content 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' titles, sections, and full
|
|
6
|
+
* markdown bodies. This file is only the fallback that runs when the plugin is
|
|
7
|
+
* missing — it exists so the import type-checks and fails with a clear message
|
|
8
|
+
* instead of a mystery.
|
|
9
|
+
*/
|
|
10
|
+
import type { LlmsDoc } from './config.js';
|
|
11
|
+
export declare const docs: LlmsDoc[];
|
package/dist/llms.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip a trailing slash so route matching is consistent regardless of the
|
|
3
|
+
* consumer's SvelteKit `trailingSlash` setting: `/docs/intro/` and `/docs/intro`
|
|
4
|
+
* resolve to the same content entry. The root stays `/`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizePath(pathname: string): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip a trailing slash so route matching is consistent regardless of the
|
|
3
|
+
* consumer's SvelteKit `trailingSlash` setting: `/docs/intro/` and `/docs/intro`
|
|
4
|
+
* resolve to the same content entry. The root stays `/`.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizePath(pathname) {
|
|
7
|
+
return pathname.replace(/\/+$/, '') || '/';
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** One entry in a sitemap: a page path (joined to the origin) and optional last-modified date. */
|
|
2
|
+
export type SitemapEntry = {
|
|
3
|
+
/** Absolute path, e.g. `/docs/intro`. Joined to the origin. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** Last-modified date; an ISO string or `YYYY-MM-DD`. Only the date is emitted. */
|
|
6
|
+
lastmod?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Build a sitemap.xml body from a list of pages. Framework-agnostic: wire it
|
|
10
|
+
* into a SvelteKit `src/routes/sitemap.xml/+server.ts` that maps the generated
|
|
11
|
+
* `svelte-docsmith/content` index (plus any non-doc routes) into entries.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { docs } from 'svelte-docsmith/content';
|
|
15
|
+
* import { generateSitemap } from 'svelte-docsmith';
|
|
16
|
+
*
|
|
17
|
+
* export const prerender = true;
|
|
18
|
+
* export function GET() {
|
|
19
|
+
* const body = generateSitemap('https://my-docs.dev', [
|
|
20
|
+
* { path: '/' },
|
|
21
|
+
* ...docs.map((d) => ({ path: d.path, lastmod: d.lastUpdated }))
|
|
22
|
+
* ]);
|
|
23
|
+
* return new Response(body, { headers: { 'content-type': 'application/xml' } });
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateSitemap(origin: string, entries: SitemapEntry[]): string;
|
package/dist/sitemap.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const escapeXml = (s) => s.replace(/[&<>'"]/g, (c) => {
|
|
2
|
+
switch (c) {
|
|
3
|
+
case '&':
|
|
4
|
+
return '&';
|
|
5
|
+
case '<':
|
|
6
|
+
return '<';
|
|
7
|
+
case '>':
|
|
8
|
+
return '>';
|
|
9
|
+
case "'":
|
|
10
|
+
return ''';
|
|
11
|
+
default:
|
|
12
|
+
return '"';
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Build a sitemap.xml body from a list of pages. Framework-agnostic: wire it
|
|
17
|
+
* into a SvelteKit `src/routes/sitemap.xml/+server.ts` that maps the generated
|
|
18
|
+
* `svelte-docsmith/content` index (plus any non-doc routes) into entries.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { docs } from 'svelte-docsmith/content';
|
|
22
|
+
* import { generateSitemap } from 'svelte-docsmith';
|
|
23
|
+
*
|
|
24
|
+
* export const prerender = true;
|
|
25
|
+
* export function GET() {
|
|
26
|
+
* const body = generateSitemap('https://my-docs.dev', [
|
|
27
|
+
* { path: '/' },
|
|
28
|
+
* ...docs.map((d) => ({ path: d.path, lastmod: d.lastUpdated }))
|
|
29
|
+
* ]);
|
|
30
|
+
* return new Response(body, { headers: { 'content-type': 'application/xml' } });
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function generateSitemap(origin, entries) {
|
|
35
|
+
const base = origin.replace(/\/$/, '');
|
|
36
|
+
const urls = entries
|
|
37
|
+
.map(({ path, lastmod }) => {
|
|
38
|
+
const loc = `\t\t<loc>${escapeXml(base + path)}</loc>`;
|
|
39
|
+
const mod = lastmod ? `\n\t\t<lastmod>${lastmod.slice(0, 10)}</lastmod>` : '';
|
|
40
|
+
return `\t<url>\n${loc}${mod}\n\t</url>`;
|
|
41
|
+
})
|
|
42
|
+
.join('\n');
|
|
43
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from '$app/environment';
|
|
3
|
+
import { Button } from '../shadcn/button/index.js';
|
|
4
|
+
import * as DropdownMenu from '../shadcn/dropdown-menu/index.js';
|
|
5
|
+
import Check from '@lucide/svelte/icons/check';
|
|
6
|
+
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
|
7
|
+
import Copy from '@lucide/svelte/icons/copy';
|
|
8
|
+
import FileText from '@lucide/svelte/icons/file-text';
|
|
9
|
+
import ArrowUpRight from '@lucide/svelte/icons/arrow-up-right';
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
path,
|
|
13
|
+
origin = ''
|
|
14
|
+
}: {
|
|
15
|
+
/** The current page's normalized pathname, e.g. `/docs/intro`. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Absolute site origin for the AI deep links; falls back to `location.origin`. */
|
|
18
|
+
origin?: string;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
// Convention: each page's markdown lives at `<path>.md`, served by the
|
|
22
|
+
// consumer's catch-all endpoint over the `svelte-docsmith/llms` index.
|
|
23
|
+
const mdHref = $derived(`${path}.md`);
|
|
24
|
+
const base = $derived((origin || (browser ? location.origin : '')).replace(/\/$/, ''));
|
|
25
|
+
const absoluteMd = $derived(`${base}${mdHref}`);
|
|
26
|
+
const prompt = $derived(`Read ${absoluteMd} so I can ask questions about this page.`);
|
|
27
|
+
const chatgptHref = $derived(`https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`);
|
|
28
|
+
const claudeHref = $derived(`https://claude.ai/new?q=${encodeURIComponent(prompt)}`);
|
|
29
|
+
|
|
30
|
+
let copied = $state(false);
|
|
31
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
32
|
+
|
|
33
|
+
async function copyPage() {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(mdHref);
|
|
36
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
// `navigator.clipboard` is undefined on insecure origins and can reject
|
|
39
|
+
// without permission; fail quietly so the button never throws.
|
|
40
|
+
await navigator.clipboard.writeText(text);
|
|
41
|
+
copied = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
timer = setTimeout(() => (copied = false), 1500);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn('[svelte-docsmith] copy page failed', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class="border-border bg-background inline-flex items-center rounded-md border shadow-xs">
|
|
51
|
+
<Button
|
|
52
|
+
variant="ghost"
|
|
53
|
+
size="sm"
|
|
54
|
+
onclick={copyPage}
|
|
55
|
+
class="rounded-r-none"
|
|
56
|
+
aria-label="Copy this page as Markdown"
|
|
57
|
+
>
|
|
58
|
+
{#if copied}
|
|
59
|
+
<Check class="text-primary" />
|
|
60
|
+
Copied
|
|
61
|
+
{:else}
|
|
62
|
+
<Copy />
|
|
63
|
+
Copy page
|
|
64
|
+
{/if}
|
|
65
|
+
</Button>
|
|
66
|
+
<div class="bg-border h-5 w-px" aria-hidden="true"></div>
|
|
67
|
+
<DropdownMenu.Root>
|
|
68
|
+
<DropdownMenu.Trigger>
|
|
69
|
+
{#snippet child({ props })}
|
|
70
|
+
<Button
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="sm"
|
|
73
|
+
class="rounded-l-none px-2"
|
|
74
|
+
aria-label="More page actions"
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
<ChevronDown />
|
|
78
|
+
</Button>
|
|
79
|
+
{/snippet}
|
|
80
|
+
</DropdownMenu.Trigger>
|
|
81
|
+
<DropdownMenu.Content align="end" class="w-56">
|
|
82
|
+
<DropdownMenu.Item onSelect={copyPage}>
|
|
83
|
+
<Copy />
|
|
84
|
+
Copy page
|
|
85
|
+
</DropdownMenu.Item>
|
|
86
|
+
<DropdownMenu.Item>
|
|
87
|
+
{#snippet child({ props })}
|
|
88
|
+
<a href={mdHref} target="_blank" rel="noopener" {...props}>
|
|
89
|
+
<FileText />
|
|
90
|
+
View as Markdown
|
|
91
|
+
</a>
|
|
92
|
+
{/snippet}
|
|
93
|
+
</DropdownMenu.Item>
|
|
94
|
+
<DropdownMenu.Separator />
|
|
95
|
+
<DropdownMenu.Item>
|
|
96
|
+
{#snippet child({ props })}
|
|
97
|
+
<a href={chatgptHref} target="_blank" rel="noopener" {...props}>
|
|
98
|
+
<ArrowUpRight />
|
|
99
|
+
Open in ChatGPT
|
|
100
|
+
</a>
|
|
101
|
+
{/snippet}
|
|
102
|
+
</DropdownMenu.Item>
|
|
103
|
+
<DropdownMenu.Item>
|
|
104
|
+
{#snippet child({ props })}
|
|
105
|
+
<a href={claudeHref} target="_blank" rel="noopener" {...props}>
|
|
106
|
+
<ArrowUpRight />
|
|
107
|
+
Open in Claude
|
|
108
|
+
</a>
|
|
109
|
+
{/snippet}
|
|
110
|
+
</DropdownMenu.Item>
|
|
111
|
+
</DropdownMenu.Content>
|
|
112
|
+
</DropdownMenu.Root>
|
|
113
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** The current page's normalized pathname, e.g. `/docs/intro`. */
|
|
3
|
+
path: string;
|
|
4
|
+
/** Absolute site origin for the AI deep links; falls back to `location.origin`. */
|
|
5
|
+
origin?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const CopyPageMenu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type CopyPageMenu = ReturnType<typeof CopyPageMenu>;
|
|
9
|
+
export default CopyPageMenu;
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
</div>
|
|
80
80
|
<ScrollArea class="my-4 h-[calc(100vh-8rem)] pb-10 pl-2">
|
|
81
81
|
{#if config.nav?.length}
|
|
82
|
-
<nav class="flex flex-col gap-0.5 px-5 pb-1">
|
|
82
|
+
<nav aria-label="Main" class="flex flex-col gap-0.5 px-5 pb-1">
|
|
83
83
|
{#each config.nav as link (link.href)}
|
|
84
84
|
<a
|
|
85
85
|
href={link.href}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
} from '../../config.js';
|
|
10
10
|
import { createToc } from '../../toc/index.js';
|
|
11
11
|
import { createSearchState } from '../../search/context.svelte.js';
|
|
12
|
+
import { normalizePath } from '../../normalize-path.js';
|
|
12
13
|
import Search from '../search.svelte';
|
|
13
14
|
import BackgroundPattern from '../background-pattern.svelte';
|
|
14
15
|
import ThemeProvider from '../theme-provider.svelte';
|
|
@@ -18,7 +19,9 @@
|
|
|
18
19
|
import DocsSidebar from './docs-sidebar.svelte';
|
|
19
20
|
import PrevNextNav from './prev-next-nav.svelte';
|
|
20
21
|
import Breadcrumbs, { type Crumb } from './breadcrumbs.svelte';
|
|
22
|
+
import CopyPageMenu from './copy-page-menu.svelte';
|
|
21
23
|
import SeoHead from './seo-head.svelte';
|
|
24
|
+
import SquarePen from '@lucide/svelte/icons/square-pen';
|
|
22
25
|
import TableOfContents from '../table-of-contents.svelte';
|
|
23
26
|
import type { Snippet } from 'svelte';
|
|
24
27
|
|
|
@@ -32,6 +35,7 @@
|
|
|
32
35
|
search,
|
|
33
36
|
seo,
|
|
34
37
|
pattern = false,
|
|
38
|
+
copyPage = false,
|
|
35
39
|
layout = 'docs'
|
|
36
40
|
}: {
|
|
37
41
|
config: DocsmithConfig;
|
|
@@ -57,6 +61,12 @@
|
|
|
57
61
|
footer?: Snippet;
|
|
58
62
|
/** Render the decorative grid-and-glow page background. */
|
|
59
63
|
pattern?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Show the "Copy page" split button on doc pages: copy the page as
|
|
66
|
+
* Markdown, view the raw `.md`, or open it in ChatGPT / Claude. Requires a
|
|
67
|
+
* catch-all `<path>.md` endpoint over the `svelte-docsmith/llms` index.
|
|
68
|
+
*/
|
|
69
|
+
copyPage?: boolean;
|
|
60
70
|
/**
|
|
61
71
|
* `docs` (default): the three-column shell — sidebar, content, in-page TOC.
|
|
62
72
|
* `page`: full-bleed content with the same header/footer/background but no
|
|
@@ -75,12 +85,34 @@
|
|
|
75
85
|
|
|
76
86
|
const nav = $derived(navFromContent(content));
|
|
77
87
|
|
|
78
|
-
//
|
|
79
|
-
|
|
88
|
+
// Match content by a trailing-slash-normalized path, so `/docs/intro/` and
|
|
89
|
+
// `/docs/intro` resolve to the same page regardless of the app's trailingSlash.
|
|
90
|
+
const pathname = $derived(normalizePath(page.url.pathname));
|
|
91
|
+
|
|
92
|
+
// The content entry for the current route drives the SEO title/description,
|
|
93
|
+
// the "Edit this page" link, and the "Last updated" stamp.
|
|
94
|
+
const currentEntry = $derived(content.find((item) => item.path === pathname));
|
|
95
|
+
|
|
96
|
+
const editHref = $derived(
|
|
97
|
+
config.editUrl && currentEntry?.sourcePath
|
|
98
|
+
? config.editUrl.replace(/\/$/, '') + '/' + currentEntry.sourcePath
|
|
99
|
+
: undefined
|
|
100
|
+
);
|
|
101
|
+
const lastUpdatedLabel = $derived.by(() => {
|
|
102
|
+
const iso = currentEntry?.lastUpdated;
|
|
103
|
+
if (!iso) return undefined;
|
|
104
|
+
const d = new Date(iso);
|
|
105
|
+
return Number.isNaN(d.getTime())
|
|
106
|
+
? undefined
|
|
107
|
+
: new Intl.DateTimeFormat('en', { year: 'numeric', month: 'short', day: 'numeric' }).format(
|
|
108
|
+
d
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
const showFooterMeta = $derived(Boolean(editHref || lastUpdatedLabel));
|
|
80
112
|
|
|
81
113
|
// Ordered flat page list drives the prev/next links.
|
|
82
114
|
const flatNav = $derived(nav.flatMap((group) => group.items));
|
|
83
|
-
const pageIndex = $derived(flatNav.findIndex((item) => item.url ===
|
|
115
|
+
const pageIndex = $derived(flatNav.findIndex((item) => item.url === pathname));
|
|
84
116
|
const prev = $derived(pageIndex > 0 ? flatNav[pageIndex - 1] : undefined);
|
|
85
117
|
const next = $derived(
|
|
86
118
|
pageIndex >= 0 && pageIndex < flatNav.length - 1 ? flatNav[pageIndex + 1] : undefined
|
|
@@ -89,7 +121,7 @@
|
|
|
89
121
|
|
|
90
122
|
// Breadcrumb trail: the current page's sidebar group, then the page itself.
|
|
91
123
|
const currentGroup = $derived(
|
|
92
|
-
nav.find((group) => group.items.some((item) => item.url ===
|
|
124
|
+
nav.find((group) => group.items.some((item) => item.url === pathname))
|
|
93
125
|
);
|
|
94
126
|
const breadcrumbs = $derived.by(() => {
|
|
95
127
|
const crumbs: Crumb[] = [];
|
|
@@ -109,7 +141,7 @@
|
|
|
109
141
|
// Server-rendered TOC (headings extracted at build time into the content
|
|
110
142
|
// index) so the list is present on first paint. Once the client engine has
|
|
111
143
|
// scanned the DOM, its items take over (scroll-spy + edge-case accuracy).
|
|
112
|
-
const pageToc = $derived(content.find((item) => item.path ===
|
|
144
|
+
const pageToc = $derived(content.find((item) => item.path === pathname)?.toc ?? []);
|
|
113
145
|
const tocItems = $derived(toc.items.length ? toc.items : pageToc);
|
|
114
146
|
</script>
|
|
115
147
|
|
|
@@ -120,6 +152,14 @@
|
|
|
120
152
|
/>
|
|
121
153
|
|
|
122
154
|
<div class="relative isolate flex min-h-screen flex-col">
|
|
155
|
+
<!-- Keyboard skip link: first focusable element, visible only on focus. -->
|
|
156
|
+
<a
|
|
157
|
+
href="#main-content"
|
|
158
|
+
class="bg-primary text-primary-foreground focus-visible:ring-ring sr-only z-50 rounded-md px-3 py-2 text-sm font-medium focus-visible:not-sr-only focus-visible:absolute focus-visible:top-3 focus-visible:left-3 focus-visible:ring-2"
|
|
159
|
+
>
|
|
160
|
+
Skip to content
|
|
161
|
+
</a>
|
|
162
|
+
|
|
123
163
|
<!-- Owns light/dark for the whole app — consumers never wire mode-watcher. -->
|
|
124
164
|
<ThemeProvider />
|
|
125
165
|
|
|
@@ -138,7 +178,7 @@
|
|
|
138
178
|
{#if layout === 'page'}
|
|
139
179
|
<DocsMobileHeader {config} {logo} {actions} />
|
|
140
180
|
|
|
141
|
-
<main class="flex-1">
|
|
181
|
+
<main id="main-content" tabindex="-1" class="flex-1">
|
|
142
182
|
{@render children()}
|
|
143
183
|
</main>
|
|
144
184
|
{:else}
|
|
@@ -155,10 +195,44 @@
|
|
|
155
195
|
<div class="mx-auto flex w-full max-w-7xl flex-1 gap-12 px-4 md:px-6 lg:px-8 lg:pt-10">
|
|
156
196
|
<DocsSidebar {nav} />
|
|
157
197
|
|
|
158
|
-
<main
|
|
159
|
-
|
|
198
|
+
<main
|
|
199
|
+
bind:this={contentEl}
|
|
200
|
+
id="main-content"
|
|
201
|
+
tabindex="-1"
|
|
202
|
+
class="min-w-0 flex-1 py-6 lg:py-0"
|
|
203
|
+
>
|
|
204
|
+
<div class="flex items-start justify-between gap-4">
|
|
205
|
+
<Breadcrumbs items={breadcrumbs} />
|
|
206
|
+
{#if copyPage && currentEntry}
|
|
207
|
+
<div class="shrink-0">
|
|
208
|
+
<CopyPageMenu path={pathname} origin={config.url ?? ''} />
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
</div>
|
|
160
212
|
{@render children()}
|
|
161
|
-
|
|
213
|
+
|
|
214
|
+
{#if showFooterMeta}
|
|
215
|
+
<div
|
|
216
|
+
class="text-muted-foreground border-border mt-10 flex flex-wrap items-center gap-3 border-t pt-6 text-sm"
|
|
217
|
+
>
|
|
218
|
+
{#if editHref}
|
|
219
|
+
<a
|
|
220
|
+
href={editHref}
|
|
221
|
+
target="_blank"
|
|
222
|
+
rel="noopener noreferrer"
|
|
223
|
+
class="hover:text-foreground inline-flex items-center gap-1.5 transition-colors"
|
|
224
|
+
>
|
|
225
|
+
<SquarePen class="size-4" />
|
|
226
|
+
Edit this page
|
|
227
|
+
</a>
|
|
228
|
+
{/if}
|
|
229
|
+
{#if lastUpdatedLabel}
|
|
230
|
+
<span class="ml-auto">Last updated: {lastUpdatedLabel}</span>
|
|
231
|
+
{/if}
|
|
232
|
+
</div>
|
|
233
|
+
{/if}
|
|
234
|
+
|
|
235
|
+
<PrevNextNav {prev} {next} bordered={!showFooterMeta} />
|
|
162
236
|
</main>
|
|
163
237
|
|
|
164
238
|
<!-- Reserve the TOC column so its content filling in after hydration
|
|
@@ -27,6 +27,12 @@ type $$ComponentProps = {
|
|
|
27
27
|
footer?: Snippet;
|
|
28
28
|
/** Render the decorative grid-and-glow page background. */
|
|
29
29
|
pattern?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Show the "Copy page" split button on doc pages: copy the page as
|
|
32
|
+
* Markdown, view the raw `.md`, or open it in ChatGPT / Claude. Requires a
|
|
33
|
+
* catch-all `<path>.md` endpoint over the `svelte-docsmith/llms` index.
|
|
34
|
+
*/
|
|
35
|
+
copyPage?: boolean;
|
|
30
36
|
/**
|
|
31
37
|
* `docs` (default): the three-column shell — sidebar, content, in-page TOC.
|
|
32
38
|
* `page`: full-bleed content with the same header/footer/background but no
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { page } from '$app/state';
|
|
3
3
|
import { cn } from '../../shadcn.js';
|
|
4
|
+
import { normalizePath } from '../../normalize-path.js';
|
|
4
5
|
import type { NavGroup } from '../../config.js';
|
|
5
6
|
|
|
6
7
|
const { nav, class: className = '' }: { nav: NavGroup[]; class?: string } = $props();
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
|
|
9
10
|
<aside class={cn('sticky top-24 hidden h-screen w-56 shrink-0 bg-transparent lg:block', className)}>
|
|
10
11
|
<div class="max-h-[calc(100vh-10rem)] overflow-y-auto px-4">
|
|
11
|
-
<nav class="space-y-6">
|
|
12
|
+
<nav aria-label="Documentation" class="space-y-6">
|
|
12
13
|
{#each nav as group (group.title)}
|
|
13
14
|
<div class="space-y-3">
|
|
14
15
|
<h4 class="text-foreground px-2 text-sm font-semibold">{group.title}</h4>
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
href={item.url}
|
|
20
21
|
class={cn(
|
|
21
22
|
'hover:text-primary hover:bg-primary/20 block rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
22
|
-
page.url.pathname === item.url
|
|
23
|
+
normalizePath(page.url.pathname) === item.url
|
|
23
24
|
? 'text-primary bg-primary/20 font-medium'
|
|
24
25
|
: 'text-muted-foreground'
|
|
25
26
|
)}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { NavItem } from '../../config.js';
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
prev,
|
|
6
|
+
next,
|
|
7
|
+
bordered = true
|
|
8
|
+
}: { prev?: NavItem; next?: NavItem; bordered?: boolean } = $props();
|
|
5
9
|
</script>
|
|
6
10
|
|
|
7
11
|
{#if prev || next}
|
|
8
|
-
<nav
|
|
12
|
+
<nav
|
|
13
|
+
aria-label="Pagination"
|
|
14
|
+
class="flex justify-between gap-4 {bordered ? 'border-border mt-8 border-t pt-8' : 'mt-6'}"
|
|
15
|
+
>
|
|
9
16
|
{#if prev}
|
|
10
17
|
<a
|
|
11
18
|
href={prev.url}
|