svelte-docsmith 0.8.0 → 0.10.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/bin/svelte-docsmith.mjs +161 -0
- package/dist/buildtime/archive.d.ts +31 -0
- package/dist/buildtime/archive.js +50 -0
- package/dist/buildtime/changelog.d.ts +20 -0
- package/dist/buildtime/changelog.js +74 -0
- package/dist/buildtime/fence-meta.d.ts +20 -0
- package/dist/buildtime/fence-meta.js +48 -0
- package/dist/buildtime/markdown-layout.svelte.d.ts +3 -0
- package/dist/buildtime/markdown-source.d.ts +31 -0
- package/dist/buildtime/markdown-source.js +90 -0
- package/dist/buildtime/preprocess.d.ts +13 -0
- package/dist/buildtime/preprocess.js +77 -17
- package/dist/buildtime/twoslash.d.ts +26 -0
- package/dist/buildtime/twoslash.js +40 -0
- package/dist/buildtime/vite/collect.d.ts +4 -3
- package/dist/buildtime/vite/collect.js +31 -12
- package/dist/buildtime/vite/extract.js +9 -35
- package/dist/buildtime/vite/frontmatter.js +4 -3
- package/dist/buildtime/vite/git.d.ts +7 -0
- package/dist/buildtime/vite/git.js +32 -0
- package/dist/buildtime/vite/index.d.ts +22 -0
- package/dist/buildtime/vite/index.js +27 -4
- package/dist/buildtime/vite/releases.d.ts +12 -0
- package/dist/buildtime/vite/releases.js +57 -0
- package/dist/components/changelog/changelog-entry.svelte +90 -0
- package/dist/components/changelog/changelog-entry.svelte.d.ts +7 -0
- package/dist/components/changelog/changelog.svelte +56 -0
- package/dist/components/changelog/changelog.svelte.d.ts +14 -0
- package/dist/components/chrome/copy-button.svelte +3 -2
- package/dist/components/chrome/search.svelte +16 -4
- package/dist/components/chrome/search.svelte.d.ts +4 -1
- package/dist/components/chrome/version-banner.svelte +82 -0
- package/dist/components/chrome/version-banner.svelte.d.ts +10 -0
- package/dist/components/chrome/version-switcher.svelte +63 -0
- package/dist/components/chrome/version-switcher.svelte.d.ts +14 -0
- package/dist/components/docs/mermaid.svelte +213 -0
- package/dist/components/docs/mermaid.svelte.d.ts +6 -0
- package/dist/components/landing/action.svelte +50 -0
- package/dist/components/landing/action.svelte.d.ts +13 -0
- package/dist/components/landing/cta.svelte +52 -0
- package/dist/components/landing/cta.svelte.d.ts +13 -0
- package/dist/components/landing/feature-grid.svelte +51 -0
- package/dist/components/landing/feature-grid.svelte.d.ts +19 -0
- package/dist/components/landing/feature.svelte +33 -0
- package/dist/components/landing/feature.svelte.d.ts +11 -0
- package/dist/components/landing/hero.svelte +70 -0
- package/dist/components/landing/hero.svelte.d.ts +19 -0
- package/dist/components/layouts/docs-header.svelte +61 -5
- package/dist/components/layouts/docs-header.svelte.d.ts +9 -1
- package/dist/components/layouts/docs-mobile-header.svelte +25 -2
- package/dist/components/layouts/docs-mobile-header.svelte.d.ts +9 -1
- package/dist/components/layouts/docs-shell.svelte +84 -66
- package/dist/components/layouts/docs-shell.svelte.d.ts +11 -2
- package/dist/components/layouts/error-page.svelte +23 -3
- package/dist/components/layouts/error-page.svelte.d.ts +8 -2
- package/dist/components/layouts/seo-head.svelte +8 -1
- package/dist/components/layouts/seo-head.svelte.d.ts +2 -0
- package/dist/components/markdown/pre.svelte +238 -8
- package/dist/components/markdown/pre.svelte.d.ts +6 -0
- package/dist/core/changelog.d.ts +31 -0
- package/dist/core/changelog.js +7 -0
- package/dist/core/content.d.ts +9 -0
- package/dist/core/docs-page.d.ts +70 -0
- package/dist/core/docs-page.js +55 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/version.d.ts +80 -0
- package/dist/core/version.js +85 -0
- package/dist/fallbacks/changelog.d.ts +10 -0
- package/dist/fallbacks/changelog.js +3 -0
- package/dist/fallbacks/content.d.ts +2 -0
- package/dist/fallbacks/content.js +1 -0
- package/dist/generate/feed.d.ts +35 -0
- package/dist/generate/feed.js +80 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/search/context.svelte.d.ts +2 -0
- package/dist/search/context.svelte.js +2 -0
- package/dist/search/create-search.d.ts +8 -2
- package/dist/search/create-search.js +11 -3
- package/dist/theme.css +53 -0
- package/package.json +27 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
3
|
+
|
|
4
|
+
const { release }: { release: ChangelogRelease } = $props();
|
|
5
|
+
|
|
6
|
+
const formatted = $derived(
|
|
7
|
+
release.date
|
|
8
|
+
? new Date(release.date).toLocaleDateString('en-US', {
|
|
9
|
+
year: 'numeric',
|
|
10
|
+
month: 'long',
|
|
11
|
+
day: 'numeric'
|
|
12
|
+
})
|
|
13
|
+
: undefined
|
|
14
|
+
);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- The version is the anchor the feed links to, so it carries the id. -->
|
|
18
|
+
<section id={release.version} class="scroll-mt-24">
|
|
19
|
+
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
20
|
+
<h2 class="text-2xl font-semibold tracking-tight">
|
|
21
|
+
{#if release.path}
|
|
22
|
+
<a href={release.path} class="hover:text-primary transition-colors">{release.version}</a>
|
|
23
|
+
{:else}
|
|
24
|
+
{release.version}
|
|
25
|
+
{/if}
|
|
26
|
+
</h2>
|
|
27
|
+
{#if formatted}
|
|
28
|
+
<time datetime={release.date} class="text-muted-foreground text-sm">{formatted}</time>
|
|
29
|
+
{/if}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{#each release.groups as group (group.kind)}
|
|
33
|
+
<h3 class="text-muted-foreground mt-6 text-xs font-semibold tracking-wide uppercase">
|
|
34
|
+
{group.kind}
|
|
35
|
+
</h3>
|
|
36
|
+
<ul class="mt-3 space-y-4">
|
|
37
|
+
{#each group.items as item (item)}
|
|
38
|
+
<li class="border-border/70 border-l-2 pl-4">
|
|
39
|
+
<!-- Rendered at build time from your own repository's changelog. -->
|
|
40
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
41
|
+
<div class="changelog-prose">{@html item}</div>
|
|
42
|
+
</li>
|
|
43
|
+
{/each}
|
|
44
|
+
</ul>
|
|
45
|
+
{/each}
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
/* The entries arrive as HTML fragments rather than flowing through the
|
|
50
|
+
markdown layout's prose styles, so they need their own small set. */
|
|
51
|
+
.changelog-prose :global(p) {
|
|
52
|
+
margin: 0 0 0.75rem;
|
|
53
|
+
line-height: 1.65;
|
|
54
|
+
}
|
|
55
|
+
.changelog-prose :global(p:last-child) {
|
|
56
|
+
margin-bottom: 0;
|
|
57
|
+
}
|
|
58
|
+
.changelog-prose :global(code) {
|
|
59
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
60
|
+
font-size: 0.85em;
|
|
61
|
+
padding: 0.05em 0.3em;
|
|
62
|
+
border-radius: 0.3rem;
|
|
63
|
+
background: color-mix(in oklch, var(--muted) 60%, transparent);
|
|
64
|
+
}
|
|
65
|
+
.changelog-prose :global(a) {
|
|
66
|
+
color: var(--primary);
|
|
67
|
+
text-decoration: underline;
|
|
68
|
+
text-underline-offset: 2px;
|
|
69
|
+
}
|
|
70
|
+
.changelog-prose :global(ul) {
|
|
71
|
+
margin: 0.5rem 0 0.75rem;
|
|
72
|
+
padding-left: 1.1rem;
|
|
73
|
+
list-style: disc;
|
|
74
|
+
}
|
|
75
|
+
.changelog-prose :global(li) {
|
|
76
|
+
margin: 0.25rem 0;
|
|
77
|
+
}
|
|
78
|
+
.changelog-prose :global(pre) {
|
|
79
|
+
margin: 0.75rem 0;
|
|
80
|
+
padding: 0.75rem 1rem;
|
|
81
|
+
border-radius: var(--radius, 0.5rem);
|
|
82
|
+
background: var(--muted);
|
|
83
|
+
overflow-x: auto;
|
|
84
|
+
font-size: 0.85em;
|
|
85
|
+
}
|
|
86
|
+
.changelog-prose :global(pre code) {
|
|
87
|
+
background: none;
|
|
88
|
+
padding: 0;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
release: ChangelogRelease;
|
|
4
|
+
};
|
|
5
|
+
declare const ChangelogEntry: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ChangelogEntry = ReturnType<typeof ChangelogEntry>;
|
|
7
|
+
export default ChangelogEntry;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
3
|
+
import ChangelogEntry from './changelog-entry.svelte';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
releases,
|
|
7
|
+
title = 'Changelog',
|
|
8
|
+
description,
|
|
9
|
+
feed = '/changelog.xml'
|
|
10
|
+
}: {
|
|
11
|
+
/** Releases from the generated `svelte-docsmith/changelog` index. */
|
|
12
|
+
releases: ChangelogRelease[];
|
|
13
|
+
/** Page heading. */
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Line below the heading. */
|
|
16
|
+
description?: string;
|
|
17
|
+
/** Path to the Atom feed, or `false` to hide the subscribe link. */
|
|
18
|
+
feed?: string | false;
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="mx-auto max-w-3xl px-4 py-16 md:px-6 lg:py-20">
|
|
23
|
+
<header class="mb-12">
|
|
24
|
+
<h1 class="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">{title}</h1>
|
|
25
|
+
{#if description}
|
|
26
|
+
<p class="text-muted-foreground mt-4 text-lg leading-relaxed text-pretty">{description}</p>
|
|
27
|
+
{/if}
|
|
28
|
+
{#if feed}
|
|
29
|
+
<a
|
|
30
|
+
href={feed}
|
|
31
|
+
class="text-muted-foreground hover:text-foreground mt-4 inline-flex items-center gap-2 text-sm transition-colors"
|
|
32
|
+
>
|
|
33
|
+
<svg viewBox="0 0 24 24" fill="currentColor" class="size-3.5" aria-hidden="true">
|
|
34
|
+
<path
|
|
35
|
+
d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16M6 19a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"
|
|
36
|
+
fill="none"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-width="2"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
Subscribe via Atom
|
|
43
|
+
</a>
|
|
44
|
+
{/if}
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
{#if releases.length === 0}
|
|
48
|
+
<p class="text-muted-foreground">No releases yet.</p>
|
|
49
|
+
{:else}
|
|
50
|
+
<div class="space-y-14">
|
|
51
|
+
{#each releases as release (release.version)}
|
|
52
|
+
<ChangelogEntry {release} />
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChangelogRelease } from '../../core/changelog.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Releases from the generated `svelte-docsmith/changelog` index. */
|
|
4
|
+
releases: ChangelogRelease[];
|
|
5
|
+
/** Page heading. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Line below the heading. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Path to the Atom feed, or `false` to hide the subscribe link. */
|
|
10
|
+
feed?: string | false;
|
|
11
|
+
};
|
|
12
|
+
declare const Changelog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type Changelog = ReturnType<typeof Changelog>;
|
|
14
|
+
export default Changelog;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} = $props();
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
|
-
<Button {onclick} variant="ghost" size="icon" class="size-8 {className ?? ''}">
|
|
19
|
+
<Button {onclick} variant="ghost" size="icon" aria-label="Copy" class="size-8 {className ?? ''}">
|
|
20
20
|
{#if copied}
|
|
21
21
|
<div in:fade={{ duration: 80 }}>
|
|
22
22
|
<Check class="text-emerald-500" />
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
<Copy />
|
|
27
27
|
</div>
|
|
28
28
|
{/if}
|
|
29
|
-
|
|
29
|
+
<!-- Announce success to screen readers without altering the button's name. -->
|
|
30
|
+
<span class="sr-only" aria-live="polite">{copied ? 'Copied to clipboard' : ''}</span>
|
|
30
31
|
</Button>
|
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
* Lazily provide the generated search records. Wired by the consumer so the
|
|
18
18
|
* index is code-split and only fetched when the palette first opens, e.g.
|
|
19
19
|
* `() => import('svelte-docsmith/search').then((m) => m.docs)`.
|
|
20
|
+
*
|
|
21
|
+
* Receives the active version id on a versioned site, so a loader may
|
|
22
|
+
* return just that version's records; results are scoped either way.
|
|
20
23
|
*/
|
|
21
|
-
load: () => Promise<SearchDoc[]>;
|
|
24
|
+
load: (versionId?: string) => Promise<SearchDoc[]>;
|
|
22
25
|
placeholder?: string;
|
|
23
26
|
} = $props();
|
|
24
27
|
|
|
@@ -27,20 +30,29 @@
|
|
|
27
30
|
let query = $state('');
|
|
28
31
|
let engine = $state<SearchEngine | null>(null);
|
|
29
32
|
let status = $state<'idle' | 'loading' | 'error'>('idle');
|
|
33
|
+
// The version the cached engine was built for, so a loader that returns only
|
|
34
|
+
// one version's records is rebuilt when the reader switches versions. Stays
|
|
35
|
+
// constant (and the engine cached) on unversioned and single-version sites.
|
|
36
|
+
let engineVersion: string | undefined;
|
|
30
37
|
|
|
31
38
|
const trimmed = $derived(query.trim());
|
|
32
|
-
|
|
39
|
+
// Scope to the active version when the shell set one (versioned sites).
|
|
40
|
+
const results = $derived(
|
|
41
|
+
engine && trimmed ? engine.search(query, undefined, search?.version) : []
|
|
42
|
+
);
|
|
33
43
|
|
|
34
44
|
// Build the index the first time the palette opens; keep it for later opens.
|
|
35
45
|
async function ensureEngine() {
|
|
36
|
-
|
|
46
|
+
const version = search?.version;
|
|
47
|
+
if ((engine && engineVersion === version) || status === 'loading') return;
|
|
37
48
|
status = 'loading';
|
|
38
49
|
try {
|
|
39
50
|
const [{ createSearchEngine }, docs] = await Promise.all([
|
|
40
51
|
import('../../search/create-search.js'),
|
|
41
|
-
load()
|
|
52
|
+
load(version)
|
|
42
53
|
]);
|
|
43
54
|
engine = createSearchEngine(docs);
|
|
55
|
+
engineVersion = version;
|
|
44
56
|
status = 'idle';
|
|
45
57
|
} catch (error) {
|
|
46
58
|
console.error('[svelte-docsmith] failed to load the search index', error);
|
|
@@ -4,8 +4,11 @@ type $$ComponentProps = {
|
|
|
4
4
|
* Lazily provide the generated search records. Wired by the consumer so the
|
|
5
5
|
* index is code-split and only fetched when the palette first opens, e.g.
|
|
6
6
|
* `() => import('svelte-docsmith/search').then((m) => m.docs)`.
|
|
7
|
+
*
|
|
8
|
+
* Receives the active version id on a versioned site, so a loader may
|
|
9
|
+
* return just that version's records; results are scoped either way.
|
|
7
10
|
*/
|
|
8
|
-
load: () => Promise<SearchDoc[]>;
|
|
11
|
+
load: (versionId?: string) => Promise<SearchDoc[]>;
|
|
9
12
|
placeholder?: string;
|
|
10
13
|
};
|
|
11
14
|
declare const Search: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
|
|
3
|
+
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
|
4
|
+
import {
|
|
5
|
+
mapPathToVersion,
|
|
6
|
+
scopeContent,
|
|
7
|
+
type ResolvedVersion,
|
|
8
|
+
type DocsContentItem
|
|
9
|
+
} from '../../core/index.js';
|
|
10
|
+
|
|
11
|
+
// Rendered by DocsShell only on an archived version, to warn a reader who
|
|
12
|
+
// landed there (usually from a search engine) and give them a one-click path
|
|
13
|
+
// to the current equivalent. A real <a> (not client nav) so it works without
|
|
14
|
+
// JS and stays crawlable.
|
|
15
|
+
const {
|
|
16
|
+
active,
|
|
17
|
+
current,
|
|
18
|
+
pathname,
|
|
19
|
+
content
|
|
20
|
+
}: {
|
|
21
|
+
active: ResolvedVersion;
|
|
22
|
+
current: ResolvedVersion;
|
|
23
|
+
pathname: string;
|
|
24
|
+
content: DocsContentItem[];
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
const currentHref = $derived(
|
|
28
|
+
mapPathToVersion(
|
|
29
|
+
pathname,
|
|
30
|
+
active,
|
|
31
|
+
current,
|
|
32
|
+
scopeContent(content, current.id).map((c) => c.path)
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div class="docsmith-version-banner" role="note">
|
|
38
|
+
<TriangleAlert class="banner-icon" size={18} aria-hidden="true" />
|
|
39
|
+
<p>
|
|
40
|
+
You're reading the <strong>{active.label}</strong> docs. The current version is
|
|
41
|
+
<strong>{current.label}</strong>.
|
|
42
|
+
<a href={currentHref}>View this page in {current.label} <ArrowRight size={14} /></a>
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
.docsmith-version-banner {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: 0.6rem;
|
|
50
|
+
align-items: flex-start;
|
|
51
|
+
margin-bottom: 1.75rem;
|
|
52
|
+
padding: 0.75rem 1rem;
|
|
53
|
+
border: 1px solid;
|
|
54
|
+
border-radius: var(--radius);
|
|
55
|
+
font-size: 0.9rem;
|
|
56
|
+
line-height: 1.5;
|
|
57
|
+
color: var(--foreground);
|
|
58
|
+
border-color: oklch(0.62 0.14 75 / 0.3);
|
|
59
|
+
background: oklch(0.62 0.14 75 / 0.1);
|
|
60
|
+
}
|
|
61
|
+
:global(.docsmith-version-banner .banner-icon) {
|
|
62
|
+
margin-top: 0.1rem;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
color: oklch(0.62 0.14 75);
|
|
65
|
+
}
|
|
66
|
+
:global(.dark) .docsmith-version-banner :global(.banner-icon) {
|
|
67
|
+
color: oklch(0.82 0.13 80);
|
|
68
|
+
}
|
|
69
|
+
.docsmith-version-banner p {
|
|
70
|
+
margin: 0;
|
|
71
|
+
}
|
|
72
|
+
.docsmith-version-banner a {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 0.2rem;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
color: var(--primary);
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
text-decoration: underline;
|
|
80
|
+
text-underline-offset: 2px;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ResolvedVersion, type DocsContentItem } from '../../core/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
active: ResolvedVersion;
|
|
4
|
+
current: ResolvedVersion;
|
|
5
|
+
pathname: string;
|
|
6
|
+
content: DocsContentItem[];
|
|
7
|
+
};
|
|
8
|
+
declare const VersionBanner: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type VersionBanner = ReturnType<typeof VersionBanner>;
|
|
10
|
+
export default VersionBanner;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
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 ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
|
|
7
|
+
import {
|
|
8
|
+
mapPathToVersion,
|
|
9
|
+
scopeContent,
|
|
10
|
+
type ResolvedVersion,
|
|
11
|
+
type DocsContentItem
|
|
12
|
+
} from '../../core/index.js';
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
versions,
|
|
16
|
+
active,
|
|
17
|
+
pathname,
|
|
18
|
+
content
|
|
19
|
+
}: {
|
|
20
|
+
/** All declared versions, in switcher order. */
|
|
21
|
+
versions: ResolvedVersion[];
|
|
22
|
+
/** The version the current page belongs to. */
|
|
23
|
+
active: ResolvedVersion;
|
|
24
|
+
/** The current normalized pathname, e.g. `/docs/v2/intro`. */
|
|
25
|
+
pathname: string;
|
|
26
|
+
/** Full content index, to map the current page into the chosen version. */
|
|
27
|
+
content: DocsContentItem[];
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
function select(target: ResolvedVersion) {
|
|
31
|
+
if (target.id === active.id) return;
|
|
32
|
+
const paths = scopeContent(content, target.id).map((c) => c.path);
|
|
33
|
+
goto(mapPathToVersion(pathname, active, target, paths));
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<DropdownMenu.Root>
|
|
38
|
+
<DropdownMenu.Trigger>
|
|
39
|
+
{#snippet child({ props })}
|
|
40
|
+
<Button
|
|
41
|
+
variant="outline"
|
|
42
|
+
size="sm"
|
|
43
|
+
class="h-8 gap-1.5 px-2.5 font-medium"
|
|
44
|
+
aria-label={`Documentation version: ${active.label}. Change version`}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
<span class="truncate">{active.label}</span>
|
|
48
|
+
<ChevronsUpDown class="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
|
|
49
|
+
</Button>
|
|
50
|
+
{/snippet}
|
|
51
|
+
</DropdownMenu.Trigger>
|
|
52
|
+
<DropdownMenu.Content align="start" class="w-44">
|
|
53
|
+
{#each versions as version (version.id)}
|
|
54
|
+
<DropdownMenu.Item onSelect={() => select(version)} class="gap-2">
|
|
55
|
+
<Check
|
|
56
|
+
class="size-4 {version.id === active.id ? 'opacity-100' : 'opacity-0'}"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
/>
|
|
59
|
+
<span class="flex-1 truncate">{version.label}</span>
|
|
60
|
+
</DropdownMenu.Item>
|
|
61
|
+
{/each}
|
|
62
|
+
</DropdownMenu.Content>
|
|
63
|
+
</DropdownMenu.Root>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ResolvedVersion, type DocsContentItem } from '../../core/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** All declared versions, in switcher order. */
|
|
4
|
+
versions: ResolvedVersion[];
|
|
5
|
+
/** The version the current page belongs to. */
|
|
6
|
+
active: ResolvedVersion;
|
|
7
|
+
/** The current normalized pathname, e.g. `/docs/v2/intro`. */
|
|
8
|
+
pathname: string;
|
|
9
|
+
/** Full content index, to map the current page into the chosen version. */
|
|
10
|
+
content: DocsContentItem[];
|
|
11
|
+
};
|
|
12
|
+
declare const VersionSwitcher: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type VersionSwitcher = ReturnType<typeof VersionSwitcher>;
|
|
14
|
+
export default VersionSwitcher;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
type Mermaid = (typeof import('mermaid'))['default'];
|
|
3
|
+
|
|
4
|
+
// One shared import; the bundler dedupes the chunk, this dedupes the promise.
|
|
5
|
+
let loaded: Promise<Mermaid> | undefined;
|
|
6
|
+
const loadMermaid = () => (loaded ??= import('mermaid').then((m) => m.default));
|
|
7
|
+
|
|
8
|
+
// `mermaid.initialize()` writes global config and `render()` is async, so two
|
|
9
|
+
// diagrams rendering concurrently can apply each other's theme — most visibly
|
|
10
|
+
// when one falls back to a built-in theme and reconfigures for everyone. Every
|
|
11
|
+
// initialize/render pair runs through this queue so they never interleave.
|
|
12
|
+
let chain: Promise<unknown> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
function serialize<T>(job: () => Promise<T>): Promise<T> {
|
|
15
|
+
const result = chain.then(job, job);
|
|
16
|
+
// Swallow rejections on the chain itself so one bad diagram doesn't stall
|
|
17
|
+
// the queue for the rest of the page.
|
|
18
|
+
chain = result.catch(() => {});
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import { onMount } from 'svelte';
|
|
25
|
+
|
|
26
|
+
// Rendered from a ```mermaid code fence: the preprocessor emits a lazy
|
|
27
|
+
// `import('svelte-docsmith/mermaid')` so `mermaid` (an optional peer dep) is
|
|
28
|
+
// only pulled into pages that actually contain a diagram. Rendering is
|
|
29
|
+
// client-side — mermaid needs a DOM — with a `<pre>` source fallback.
|
|
30
|
+
const { code }: { code: string } = $props();
|
|
31
|
+
|
|
32
|
+
// Graph ids must be unique among elements currently in the document: mermaid
|
|
33
|
+
// clears whatever already carries the id it's given, so a shared id silently
|
|
34
|
+
// wipes another diagram that's already on the page. `$props.id()` is unique per
|
|
35
|
+
// instance and stays unique even if two copies of this module end up loaded.
|
|
36
|
+
const uid = $props.id();
|
|
37
|
+
// Bumped per render, because the theme toggle re-renders while this instance's
|
|
38
|
+
// previous SVG is still mounted — reusing the id would delete it mid-swap.
|
|
39
|
+
let renders = 0;
|
|
40
|
+
|
|
41
|
+
let svg = $state('');
|
|
42
|
+
let failed = $state(false);
|
|
43
|
+
// Whether the last render targeted dark mode, so we only re-render when the
|
|
44
|
+
// site theme actually flips (not on every unrelated <html> class change).
|
|
45
|
+
let lastDark: boolean | undefined;
|
|
46
|
+
|
|
47
|
+
// Reused canvas for normalizing color tokens (see resolveColor); `null` once
|
|
48
|
+
// we've determined this browser gives us no 2d context to work with.
|
|
49
|
+
let colorCanvas: CanvasRenderingContext2D | null | undefined;
|
|
50
|
+
|
|
51
|
+
// Assigning an unparseable colour to `fillStyle` is silently ignored rather
|
|
52
|
+
// than throwing, so we set this first and check whether it survived. Any
|
|
53
|
+
// colour works as long as it's an unlikely design token, since an unchanged
|
|
54
|
+
// `fillStyle` is what tells us the assignment was rejected.
|
|
55
|
+
const SENTINEL = '#010203';
|
|
56
|
+
|
|
57
|
+
// Resolve a CSS color to a concrete `#rrggbb` string mermaid's color math
|
|
58
|
+
// (khroma) can parse. Our design tokens are `oklch(...)`, which khroma can't
|
|
59
|
+
// read. Reading `fillStyle` back is not enough to convert them: a browser
|
|
60
|
+
// serializes a wide-gamut colour unchanged, so Chrome hands back the same
|
|
61
|
+
// `oklch(...)` string it was given. Painting a pixel and reading its bytes
|
|
62
|
+
// forces the conversion to sRGB. Returns null when the value can't be
|
|
63
|
+
// resolved, which also covers an engine that doesn't understand oklch at all.
|
|
64
|
+
function resolveColor(value: string): string | null {
|
|
65
|
+
colorCanvas ??= document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
66
|
+
const ctx = colorCanvas;
|
|
67
|
+
const input = value.trim();
|
|
68
|
+
if (!ctx || !input) return null;
|
|
69
|
+
ctx.fillStyle = SENTINEL;
|
|
70
|
+
ctx.fillStyle = input;
|
|
71
|
+
if (ctx.fillStyle === SENTINEL && input.toLowerCase() !== SENTINEL) return null;
|
|
72
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
73
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
74
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
75
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Map the site's design tokens onto mermaid's documented theme variables, so a
|
|
79
|
+
// diagram reads as part of the site (its palette, borders, text) instead of
|
|
80
|
+
// mermaid's stock look. Read fresh each render so it follows the light/dark
|
|
81
|
+
// toggle. Returns null when the tokens can't be resolved — the caller then
|
|
82
|
+
// falls back to a built-in theme rather than shipping broken colors.
|
|
83
|
+
function themeVariables(dark: boolean): Record<string, string | boolean> | null {
|
|
84
|
+
const root = getComputedStyle(document.documentElement);
|
|
85
|
+
const background = resolveColor(root.getPropertyValue('--background'));
|
|
86
|
+
const foreground = resolveColor(root.getPropertyValue('--foreground'));
|
|
87
|
+
const muted = resolveColor(root.getPropertyValue('--muted'));
|
|
88
|
+
const mutedForeground = resolveColor(root.getPropertyValue('--muted-foreground'));
|
|
89
|
+
const border = resolveColor(root.getPropertyValue('--border'));
|
|
90
|
+
const accent = resolveColor(root.getPropertyValue('--accent'));
|
|
91
|
+
const card = resolveColor(root.getPropertyValue('--card'));
|
|
92
|
+
if (!background || !foreground || !muted || !mutedForeground || !border || !accent || !card) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Only mermaid's documented base variables — it derives node/cluster/edge
|
|
96
|
+
// colors from these, which keeps us off its more fragile internals.
|
|
97
|
+
return {
|
|
98
|
+
darkMode: dark,
|
|
99
|
+
background,
|
|
100
|
+
primaryColor: muted, // node fill
|
|
101
|
+
primaryTextColor: foreground,
|
|
102
|
+
primaryBorderColor: border,
|
|
103
|
+
lineColor: mutedForeground, // edges/arrows
|
|
104
|
+
secondaryColor: accent,
|
|
105
|
+
tertiaryColor: card
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function render() {
|
|
110
|
+
let mermaid: Mermaid;
|
|
111
|
+
try {
|
|
112
|
+
mermaid = await loadMermaid();
|
|
113
|
+
} catch {
|
|
114
|
+
// `mermaid` isn't installed — fall back to the source.
|
|
115
|
+
failed = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const dark = document.documentElement.classList.contains('dark');
|
|
119
|
+
lastDark = dark;
|
|
120
|
+
const vars = themeVariables(dark);
|
|
121
|
+
// Try the token-themed `base` render first; if mermaid's color math rejects
|
|
122
|
+
// anything, retry with a built-in theme so a valid diagram never silently
|
|
123
|
+
// disappears. Only a genuine syntax error reaches the `<pre>` fallback.
|
|
124
|
+
const attempts = [
|
|
125
|
+
vars && { theme: 'base' as const, themeVariables: vars },
|
|
126
|
+
{ theme: dark ? ('dark' as const) : ('default' as const) }
|
|
127
|
+
];
|
|
128
|
+
for (const attempt of attempts) {
|
|
129
|
+
if (!attempt) continue;
|
|
130
|
+
try {
|
|
131
|
+
// Configure and render as one unit so a concurrent diagram can't
|
|
132
|
+
// re-initialize mermaid between the two calls.
|
|
133
|
+
const result = await serialize(() => {
|
|
134
|
+
mermaid.initialize({
|
|
135
|
+
startOnLoad: false,
|
|
136
|
+
securityLevel: 'strict',
|
|
137
|
+
fontFamily: 'inherit',
|
|
138
|
+
...attempt
|
|
139
|
+
});
|
|
140
|
+
return mermaid.render(`${uid}-${(renders += 1)}`, code);
|
|
141
|
+
});
|
|
142
|
+
svg = result.svg;
|
|
143
|
+
failed = false;
|
|
144
|
+
return;
|
|
145
|
+
} catch {
|
|
146
|
+
// Try the next (safer) config.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Bad diagram syntax — fall back to source.
|
|
150
|
+
failed = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onMount(() => {
|
|
154
|
+
render();
|
|
155
|
+
// Re-render when the theme toggles so the diagram follows light/dark.
|
|
156
|
+
const observer = new MutationObserver(() => {
|
|
157
|
+
if (document.documentElement.classList.contains('dark') !== lastDark) render();
|
|
158
|
+
});
|
|
159
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
160
|
+
return () => observer.disconnect();
|
|
161
|
+
});
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
{#if failed}
|
|
165
|
+
<pre class="docsmith-mermaid-fallback not-prose"><code>{code}</code></pre>
|
|
166
|
+
{:else if svg}
|
|
167
|
+
<!-- mermaid sanitizes its own output (securityLevel: strict) -->
|
|
168
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
169
|
+
<figure class="docsmith-mermaid not-prose">{@html svg}</figure>
|
|
170
|
+
{:else}
|
|
171
|
+
<!-- Shares the global .docsmith-mermaid-skeleton with the server-rendered
|
|
172
|
+
placeholder, so swapping to the diagram doesn't change the height. -->
|
|
173
|
+
<div class="docsmith-mermaid-skeleton not-prose" role="status" aria-label="Loading diagram"></div>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
<style>
|
|
177
|
+
/* The loading skeleton is global (theme.css), shared with the server-rendered
|
|
178
|
+
placeholder. Here we only style the resolved diagram and the fallback. */
|
|
179
|
+
.docsmith-mermaid {
|
|
180
|
+
margin: 1.5rem 0;
|
|
181
|
+
display: flex;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
animation: docsmith-mermaid-in 0.25s ease-out;
|
|
184
|
+
}
|
|
185
|
+
.docsmith-mermaid :global(svg) {
|
|
186
|
+
max-width: 100%;
|
|
187
|
+
height: auto;
|
|
188
|
+
}
|
|
189
|
+
@keyframes docsmith-mermaid-in {
|
|
190
|
+
from {
|
|
191
|
+
opacity: 0;
|
|
192
|
+
}
|
|
193
|
+
to {
|
|
194
|
+
opacity: 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
@media (prefers-reduced-motion: reduce) {
|
|
198
|
+
.docsmith-mermaid {
|
|
199
|
+
animation: none;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.docsmith-mermaid-fallback {
|
|
204
|
+
margin: 1.5rem 0;
|
|
205
|
+
padding: 1rem;
|
|
206
|
+
border-radius: var(--radius, 0.75rem);
|
|
207
|
+
border: 1px solid var(--border);
|
|
208
|
+
background: var(--muted);
|
|
209
|
+
overflow-x: auto;
|
|
210
|
+
font-size: 0.85rem;
|
|
211
|
+
line-height: 1.5;
|
|
212
|
+
}
|
|
213
|
+
</style>
|