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.
Files changed (82) hide show
  1. package/bin/svelte-docsmith.mjs +161 -0
  2. package/dist/buildtime/archive.d.ts +31 -0
  3. package/dist/buildtime/archive.js +50 -0
  4. package/dist/buildtime/changelog.d.ts +20 -0
  5. package/dist/buildtime/changelog.js +74 -0
  6. package/dist/buildtime/fence-meta.d.ts +20 -0
  7. package/dist/buildtime/fence-meta.js +48 -0
  8. package/dist/buildtime/markdown-layout.svelte.d.ts +3 -0
  9. package/dist/buildtime/markdown-source.d.ts +31 -0
  10. package/dist/buildtime/markdown-source.js +90 -0
  11. package/dist/buildtime/preprocess.d.ts +13 -0
  12. package/dist/buildtime/preprocess.js +77 -17
  13. package/dist/buildtime/twoslash.d.ts +26 -0
  14. package/dist/buildtime/twoslash.js +40 -0
  15. package/dist/buildtime/vite/collect.d.ts +4 -3
  16. package/dist/buildtime/vite/collect.js +31 -12
  17. package/dist/buildtime/vite/extract.js +9 -35
  18. package/dist/buildtime/vite/frontmatter.js +4 -3
  19. package/dist/buildtime/vite/git.d.ts +7 -0
  20. package/dist/buildtime/vite/git.js +32 -0
  21. package/dist/buildtime/vite/index.d.ts +22 -0
  22. package/dist/buildtime/vite/index.js +27 -4
  23. package/dist/buildtime/vite/releases.d.ts +12 -0
  24. package/dist/buildtime/vite/releases.js +57 -0
  25. package/dist/components/changelog/changelog-entry.svelte +90 -0
  26. package/dist/components/changelog/changelog-entry.svelte.d.ts +7 -0
  27. package/dist/components/changelog/changelog.svelte +56 -0
  28. package/dist/components/changelog/changelog.svelte.d.ts +14 -0
  29. package/dist/components/chrome/copy-button.svelte +3 -2
  30. package/dist/components/chrome/search.svelte +16 -4
  31. package/dist/components/chrome/search.svelte.d.ts +4 -1
  32. package/dist/components/chrome/version-banner.svelte +82 -0
  33. package/dist/components/chrome/version-banner.svelte.d.ts +10 -0
  34. package/dist/components/chrome/version-switcher.svelte +63 -0
  35. package/dist/components/chrome/version-switcher.svelte.d.ts +14 -0
  36. package/dist/components/docs/mermaid.svelte +213 -0
  37. package/dist/components/docs/mermaid.svelte.d.ts +6 -0
  38. package/dist/components/landing/action.svelte +50 -0
  39. package/dist/components/landing/action.svelte.d.ts +13 -0
  40. package/dist/components/landing/cta.svelte +52 -0
  41. package/dist/components/landing/cta.svelte.d.ts +13 -0
  42. package/dist/components/landing/feature-grid.svelte +51 -0
  43. package/dist/components/landing/feature-grid.svelte.d.ts +19 -0
  44. package/dist/components/landing/feature.svelte +33 -0
  45. package/dist/components/landing/feature.svelte.d.ts +11 -0
  46. package/dist/components/landing/hero.svelte +70 -0
  47. package/dist/components/landing/hero.svelte.d.ts +19 -0
  48. package/dist/components/layouts/docs-header.svelte +61 -5
  49. package/dist/components/layouts/docs-header.svelte.d.ts +9 -1
  50. package/dist/components/layouts/docs-mobile-header.svelte +25 -2
  51. package/dist/components/layouts/docs-mobile-header.svelte.d.ts +9 -1
  52. package/dist/components/layouts/docs-shell.svelte +84 -66
  53. package/dist/components/layouts/docs-shell.svelte.d.ts +11 -2
  54. package/dist/components/layouts/error-page.svelte +23 -3
  55. package/dist/components/layouts/error-page.svelte.d.ts +8 -2
  56. package/dist/components/layouts/seo-head.svelte +8 -1
  57. package/dist/components/layouts/seo-head.svelte.d.ts +2 -0
  58. package/dist/components/markdown/pre.svelte +238 -8
  59. package/dist/components/markdown/pre.svelte.d.ts +6 -0
  60. package/dist/core/changelog.d.ts +31 -0
  61. package/dist/core/changelog.js +7 -0
  62. package/dist/core/content.d.ts +9 -0
  63. package/dist/core/docs-page.d.ts +70 -0
  64. package/dist/core/docs-page.js +55 -0
  65. package/dist/core/index.d.ts +2 -0
  66. package/dist/core/index.js +2 -0
  67. package/dist/core/version.d.ts +80 -0
  68. package/dist/core/version.js +85 -0
  69. package/dist/fallbacks/changelog.d.ts +10 -0
  70. package/dist/fallbacks/changelog.js +3 -0
  71. package/dist/fallbacks/content.d.ts +2 -0
  72. package/dist/fallbacks/content.js +1 -0
  73. package/dist/generate/feed.d.ts +35 -0
  74. package/dist/generate/feed.js +80 -0
  75. package/dist/index.d.ts +9 -0
  76. package/dist/index.js +14 -0
  77. package/dist/search/context.svelte.d.ts +2 -0
  78. package/dist/search/context.svelte.js +2 -0
  79. package/dist/search/create-search.d.ts +8 -2
  80. package/dist/search/create-search.js +11 -3
  81. package/dist/theme.css +53 -0
  82. 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
- <span class="sr-only">Copy</span>
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
- const results = $derived(engine && trimmed ? engine.search(query) : []);
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
- if (engine || status === 'loading') return;
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>
@@ -0,0 +1,6 @@
1
+ type Mermaid = (typeof import('mermaid'))['default'];
2
+ type $$ComponentProps = {
3
+ code: string;
4
+ };
5
+ declare const Mermaid: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ export default Mermaid;