@classic-homes/theme-docs 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/lib/components/Breadcrumbs.svelte +55 -0
  2. package/dist/lib/components/Breadcrumbs.svelte.d.ts +14 -0
  3. package/dist/lib/components/CategoryIndex.svelte +51 -0
  4. package/dist/lib/components/CategoryIndex.svelte.d.ts +13 -0
  5. package/dist/lib/components/DocPage.svelte +179 -0
  6. package/dist/lib/components/DocPage.svelte.d.ts +62 -0
  7. package/dist/lib/components/DocPager.svelte +49 -0
  8. package/dist/lib/components/DocPager.svelte.d.ts +12 -0
  9. package/dist/lib/components/MarkdownPage.svelte +3 -1
  10. package/dist/lib/components/MermaidDiagram.svelte +2 -0
  11. package/dist/lib/components/MermaidInit.svelte +44 -3
  12. package/dist/lib/components/MermaidInit.svelte.d.ts +5 -2
  13. package/dist/lib/components/TableOfContents.svelte +114 -125
  14. package/dist/lib/components/TableOfContents.svelte.d.ts +11 -4
  15. package/dist/lib/components/TagIndex.svelte +42 -0
  16. package/dist/lib/components/TagIndex.svelte.d.ts +14 -0
  17. package/dist/lib/components/TagList.svelte +45 -0
  18. package/dist/lib/components/TagList.svelte.d.ts +15 -0
  19. package/dist/lib/components/TocPanel.svelte +27 -9
  20. package/dist/lib/components/TocPanel.svelte.d.ts +10 -3
  21. package/dist/lib/components/enhance.d.ts +29 -0
  22. package/dist/lib/components/enhance.js +179 -0
  23. package/dist/lib/components/sidebar.d.ts +33 -0
  24. package/dist/lib/components/sidebar.js +84 -0
  25. package/dist/lib/content/browser.d.ts +6 -0
  26. package/dist/lib/content/browser.js +5 -0
  27. package/dist/lib/content/index.d.ts +13 -0
  28. package/dist/lib/content/index.js +12 -0
  29. package/dist/lib/content/load.d.ts +77 -0
  30. package/dist/lib/content/load.js +366 -0
  31. package/dist/lib/content/nav.d.ts +36 -0
  32. package/dist/lib/content/nav.js +81 -0
  33. package/dist/lib/content/render.d.ts +38 -0
  34. package/dist/lib/content/render.js +89 -0
  35. package/dist/lib/content/types.d.ts +96 -0
  36. package/dist/lib/content/types.js +5 -0
  37. package/dist/lib/index.d.ts +14 -2
  38. package/dist/lib/index.js +14 -2
  39. package/dist/lib/parser/api.d.ts +12 -0
  40. package/dist/lib/parser/api.js +10 -0
  41. package/dist/lib/parser/extensions.d.ts +27 -15
  42. package/dist/lib/parser/extensions.js +58 -53
  43. package/dist/lib/parser/index.d.ts +4 -1
  44. package/dist/lib/parser/index.js +104 -27
  45. package/dist/lib/sanitize/index.d.ts +11 -0
  46. package/dist/lib/sanitize/index.js +122 -0
  47. package/dist/lib/search/index.d.ts +57 -0
  48. package/dist/lib/search/index.js +107 -0
  49. package/dist/lib/styles/markdown.css +138 -0
  50. package/dist/lib/types/frontmatter.d.ts +21 -0
  51. package/dist/lib/vite/index.d.ts +17 -0
  52. package/dist/lib/vite/index.js +38 -0
  53. package/package.json +51 -4
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Breadcrumbs - the path to the current docs page, as Docusaurus shows above a doc.
4
+ *
5
+ * Pass `breadcrumbs(sidebar.items, route)` from `@classic-homes/theme-docs/nav`, plus an
6
+ * optional `home` link. The last crumb is the current page and is not a link.
7
+ */
8
+ import { cn } from '../utils.js';
9
+ import type { Crumb } from '../content/nav.js';
10
+
11
+ interface Props {
12
+ crumbs: Crumb[];
13
+ /** Optional first crumb, e.g. `{ label: 'Docs', href: '/' }` */
14
+ home?: { label: string; href: string };
15
+ class?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ let { crumbs, home, class: className, ...restProps }: Props = $props();
20
+
21
+ const trail = $derived([...(home ? [{ label: home.label, route: home.href }] : []), ...crumbs]);
22
+ </script>
23
+
24
+ {#if trail.length > 1}
25
+ <nav aria-label="Breadcrumbs" class={cn('mb-4 text-sm', className)} {...restProps}>
26
+ <ol class="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-muted-foreground">
27
+ {#each trail as crumb, i (i)}
28
+ {@const last = i === trail.length - 1}
29
+ <li class="flex min-w-0 items-center gap-1.5">
30
+ {#if i > 0}
31
+ <svg
32
+ aria-hidden="true"
33
+ class="h-3.5 w-3.5 shrink-0"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="2"><path d="m9 18 6-6-6-6" /></svg
38
+ >
39
+ {/if}
40
+ {#if last}
41
+ <span aria-current="page" class="truncate font-medium text-foreground"
42
+ >{crumb.label}</span
43
+ >
44
+ {:else if crumb.route}
45
+ <a href={crumb.route} class="truncate hover:text-foreground hover:underline"
46
+ >{crumb.label}</a
47
+ >
48
+ {:else}
49
+ <span class="truncate">{crumb.label}</span>
50
+ {/if}
51
+ </li>
52
+ {/each}
53
+ </ol>
54
+ </nav>
55
+ {/if}
@@ -0,0 +1,14 @@
1
+ import type { Crumb } from '../content/nav.js';
2
+ interface Props {
3
+ crumbs: Crumb[];
4
+ /** Optional first crumb, e.g. `{ label: 'Docs', href: '/' }` */
5
+ home?: {
6
+ label: string;
7
+ href: string;
8
+ };
9
+ class?: string;
10
+ [key: string]: unknown;
11
+ }
12
+ declare const Breadcrumbs: import("svelte").Component<Props, {}, "">;
13
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
14
+ export default Breadcrumbs;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CategoryIndex - a generated-index page: the category's title and description, then a
4
+ * card for each of its items (Docusaurus `link: { type: 'generated-index' }`).
5
+ *
6
+ * Pass a `CategoryPage` from `loadDocs`. Page cards show `descriptions[route]` when
7
+ * given; category cards show how many entries they hold.
8
+ */
9
+ import { ActionHeader } from '@classic-homes/theme-svelte';
10
+ import DocsHub from './DocsHub.svelte';
11
+ import { countPages, flattenNav } from '../content/nav.js';
12
+ import type { CategoryPage } from '../content/types.js';
13
+ import type { DocsItem } from '../types/docs.js';
14
+
15
+ interface Props {
16
+ category: Pick<CategoryPage, 'title' | 'description' | 'items'>;
17
+ /** Page descriptions by route, e.g. from the rendered pages */
18
+ descriptions?: Record<string, string> | Map<string, string>;
19
+ columns?: 1 | 2 | 3 | 4;
20
+ /** "N items" text for a category card */
21
+ countLabel?: (count: number) => string;
22
+ class?: string;
23
+ }
24
+
25
+ let {
26
+ category,
27
+ descriptions,
28
+ columns = 2,
29
+ countLabel = (n) => `${n} ${n === 1 ? 'item' : 'items'}`,
30
+ class: className,
31
+ }: Props = $props();
32
+
33
+ const describe = (route: string) =>
34
+ descriptions instanceof Map ? descriptions.get(route) : descriptions?.[route];
35
+
36
+ const items = $derived(
37
+ category.items.flatMap((item): DocsItem[] => {
38
+ if (item.type === 'page') {
39
+ return [{ title: item.title, href: item.route, description: describe(item.route) }];
40
+ }
41
+ const href = item.route ?? flattenNav(item.items)[0]?.route;
42
+ if (!href) return [];
43
+ return [{ title: item.label, href, description: countLabel(countPages(item.items)) }];
44
+ })
45
+ );
46
+ </script>
47
+
48
+ <div class={className}>
49
+ <ActionHeader class="mb-6" title={category.title} subtitle={category.description || undefined} />
50
+ <DocsHub {columns} config={{ sections: [{ id: 'items', items }] }} />
51
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { CategoryPage } from '../content/types.js';
2
+ interface Props {
3
+ category: Pick<CategoryPage, 'title' | 'description' | 'items'>;
4
+ /** Page descriptions by route, e.g. from the rendered pages */
5
+ descriptions?: Record<string, string> | Map<string, string>;
6
+ columns?: 1 | 2 | 3 | 4;
7
+ /** "N items" text for a category card */
8
+ countLabel?: (count: number) => string;
9
+ class?: string;
10
+ }
11
+ declare const CategoryIndex: import("svelte").Component<Props, {}, "">;
12
+ type CategoryIndex = ReturnType<typeof CategoryIndex>;
13
+ export default CategoryIndex;
@@ -0,0 +1,179 @@
1
+ <script lang="ts">
2
+ /**
3
+ * DocPage - one docs page, laid out the way Docusaurus lays out a doc.
4
+ *
5
+ * Takes HTML rendered at build time (`renderDocs`), so it renders on the server with no
6
+ * client-side parsing. Around the content: breadcrumbs, the title as an `ActionHeader`
7
+ * (like app pages) when the body has no `<h1>`, tags, an edit link, previous/next links and the table of contents. In
8
+ * the browser it mounts allowlisted components, renders mermaid diagrams, adds copy
9
+ * buttons to code blocks and highlights `?highlight=` search terms.
10
+ */
11
+ import type { Component, Snippet } from 'svelte';
12
+ import { ActionHeader, Button } from '@classic-homes/theme-svelte';
13
+ import { cn } from '../utils.js';
14
+ import Breadcrumbs from './Breadcrumbs.svelte';
15
+ import DocPager from './DocPager.svelte';
16
+ import MermaidInit from './MermaidInit.svelte';
17
+ import TagList from './TagList.svelte';
18
+ import TocPanel from './TocPanel.svelte';
19
+ import { mountComponents } from './mount.js';
20
+ import { enhanceCodeBlocks, highlightTerms } from './enhance.js';
21
+ import type { Crumb, NavLink } from '../content/nav.js';
22
+ import type { RenderedDoc, Tag } from '../content/types.js';
23
+
24
+ interface Props {
25
+ page: Pick<RenderedDoc, 'title' | 'heading' | 'html' | 'toc' | 'hasH1'> & {
26
+ description?: string;
27
+ };
28
+ /** `breadcrumbs(sidebar.items, route)` */
29
+ breadcrumbs?: Crumb[];
30
+ /** First breadcrumb, e.g. `{ label: 'Docs', href: '/' }` */
31
+ home?: { label: string; href: string };
32
+ previous?: NavLink | null;
33
+ next?: NavLink | null;
34
+ /** The page's tags, resolved to labels */
35
+ tags?: Tag[];
36
+ tagHref?: (tag: Tag) => string;
37
+ /** "Edit this page" target, e.g. the file on GitHub */
38
+ editUrl?: string;
39
+ editLabel?: string;
40
+ /** Components for `<Name … />` placeholders (see `parseMarkdown`'s `components`) */
41
+ components?: Record<string, Component<any>>;
42
+ /** Render mermaid diagrams (needs the optional `mermaid` peer). Default: true */
43
+ mermaid?: boolean;
44
+ /** Mermaid theme. Default: `brand` (colours from the theme tokens) */
45
+ mermaidTheme?: 'brand' | 'default' | 'dark' | 'forest' | 'neutral';
46
+ /** Copy buttons on code blocks. Default: true */
47
+ copyButtons?: boolean;
48
+ /** Query parameter whose words are highlighted in the page. Default: `highlight` */
49
+ highlightParam?: string | null;
50
+ /** Heading levels in the table of contents. Default: 2 to 3, as Docusaurus */
51
+ tocMinDepth?: number;
52
+ tocMaxDepth?: number;
53
+ /** Width at which the TOC moves from a slide-out panel to a column. Default: `xl` */
54
+ tocBreakpoint?: 'lg' | 'xl' | '2xl';
55
+ /** Height of the fixed header, in px, for scroll offsets. Default: 80 */
56
+ headerOffset?: number;
57
+ /** Appended to the document title: `Page | Site`. Omit to leave `<head>` alone. */
58
+ siteTitle?: string;
59
+ /**
60
+ * Content between the title and the body (e.g. a lead paragraph or actions). Pages
61
+ * rendered with `hoistTitle` put it right under the title.
62
+ */
63
+ header?: Snippet;
64
+ /** Extra content in the page footer, next to the edit link (e.g. a "last verified" badge) */
65
+ meta?: Snippet;
66
+ class?: string;
67
+ }
68
+
69
+ let {
70
+ page,
71
+ breadcrumbs = [],
72
+ home,
73
+ previous = null,
74
+ next = null,
75
+ tags = [],
76
+ tagHref,
77
+ editUrl,
78
+ editLabel = 'Edit this page',
79
+ components,
80
+ mermaid = true,
81
+ mermaidTheme = 'brand',
82
+ copyButtons = true,
83
+ highlightParam = 'highlight',
84
+ tocMinDepth = 2,
85
+ tocMaxDepth = 3,
86
+ tocBreakpoint = 'xl',
87
+ headerOffset = 80,
88
+ siteTitle,
89
+ header,
90
+ meta,
91
+ class: className,
92
+ }: Props = $props();
93
+
94
+ let content = $state<HTMLElement>();
95
+
96
+ const showToc = $derived(
97
+ page.toc.filter((e) => e.level >= tocMinDepth && e.level <= tocMaxDepth).length > 1
98
+ );
99
+
100
+ // Enhance the rendered HTML. `content` is keyed on the HTML below, so each page gets a
101
+ // fresh element and these DOM changes never meet Svelte's own bookkeeping.
102
+ $effect(() => {
103
+ if (!content) return;
104
+ const root = content;
105
+ const cleanups: (() => void)[] = [];
106
+ if (components) cleanups.push(mountComponents(root, components));
107
+ if (copyButtons) cleanups.push(enhanceCodeBlocks(root));
108
+ if (highlightParam) {
109
+ const query = new URL(location.href).searchParams.get(highlightParam);
110
+ if (query) {
111
+ const { cleanup, first } = highlightTerms(root, query.split(/\s+/));
112
+ cleanups.push(cleanup);
113
+ if (first && !location.hash) first.scrollIntoView({ block: 'center' });
114
+ }
115
+ }
116
+ return () => {
117
+ for (const cleanup of cleanups.reverse()) cleanup();
118
+ };
119
+ });
120
+ </script>
121
+
122
+ <svelte:head>
123
+ {#if siteTitle}
124
+ <title>{page.title} | {siteTitle}</title>
125
+ {#if page.description}<meta name="description" content={page.description} />{/if}
126
+ {/if}
127
+ </svelte:head>
128
+
129
+ <div class={cn('flex gap-8', className)} style="--docs-header-offset: {headerOffset + 16}px">
130
+ <div class="min-w-0 max-w-3xl flex-1">
131
+ <Breadcrumbs crumbs={breadcrumbs} {home} />
132
+
133
+ <!-- The title as app pages show theirs (ActionHeader); render with `hoistTitle` so
134
+ the body's own `# Title` becomes this rather than a markdown-styled h1. -->
135
+ {#if !page.hasH1}<ActionHeader class="mb-6" title={page.heading ?? page.title} />{/if}
136
+ <article class="markdown-content">
137
+ {@render header?.()}
138
+ {#key page.html}
139
+ <div bind:this={content}>{@html page.html}</div>
140
+ {/key}
141
+ </article>
142
+ {#if mermaid}<MermaidInit watch theme={mermaidTheme} />{/if}
143
+
144
+ <footer class="mt-12 space-y-6">
145
+ {#if tags.length}<TagList {tags} href={tagHref} />{/if}
146
+ {#if editUrl || meta}
147
+ <div class="flex flex-wrap items-center justify-between gap-2">
148
+ {#if editUrl}
149
+ <Button variant="link" href={editUrl} class="px-0">
150
+ <svg
151
+ aria-hidden="true"
152
+ class="mr-1.5 h-4 w-4"
153
+ viewBox="0 0 24 24"
154
+ fill="none"
155
+ stroke="currentColor"
156
+ stroke-width="2"
157
+ stroke-linecap="round"
158
+ stroke-linejoin="round"
159
+ ><path d="M12 20h9" /><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" /></svg
160
+ >{editLabel}
161
+ </Button>
162
+ {:else}<span></span>{/if}
163
+ {@render meta?.()}
164
+ </div>
165
+ {/if}
166
+ <DocPager {previous} {next} />
167
+ </footer>
168
+ </div>
169
+
170
+ {#if showToc}
171
+ <TocPanel
172
+ toc={page.toc}
173
+ minDepth={tocMinDepth}
174
+ maxDepth={tocMaxDepth}
175
+ breakpoint={tocBreakpoint}
176
+ offset={headerOffset}
177
+ />
178
+ {/if}
179
+ </div>
@@ -0,0 +1,62 @@
1
+ /**
2
+ * DocPage - one docs page, laid out the way Docusaurus lays out a doc.
3
+ *
4
+ * Takes HTML rendered at build time (`renderDocs`), so it renders on the server with no
5
+ * client-side parsing. Around the content: breadcrumbs, the title as an `ActionHeader`
6
+ * (like app pages) when the body has no `<h1>`, tags, an edit link, previous/next links and the table of contents. In
7
+ * the browser it mounts allowlisted components, renders mermaid diagrams, adds copy
8
+ * buttons to code blocks and highlights `?highlight=` search terms.
9
+ */
10
+ import type { Component, Snippet } from 'svelte';
11
+ import type { Crumb, NavLink } from '../content/nav.js';
12
+ import type { RenderedDoc, Tag } from '../content/types.js';
13
+ interface Props {
14
+ page: Pick<RenderedDoc, 'title' | 'heading' | 'html' | 'toc' | 'hasH1'> & {
15
+ description?: string;
16
+ };
17
+ /** `breadcrumbs(sidebar.items, route)` */
18
+ breadcrumbs?: Crumb[];
19
+ /** First breadcrumb, e.g. `{ label: 'Docs', href: '/' }` */
20
+ home?: {
21
+ label: string;
22
+ href: string;
23
+ };
24
+ previous?: NavLink | null;
25
+ next?: NavLink | null;
26
+ /** The page's tags, resolved to labels */
27
+ tags?: Tag[];
28
+ tagHref?: (tag: Tag) => string;
29
+ /** "Edit this page" target, e.g. the file on GitHub */
30
+ editUrl?: string;
31
+ editLabel?: string;
32
+ /** Components for `<Name … />` placeholders (see `parseMarkdown`'s `components`) */
33
+ components?: Record<string, Component<any>>;
34
+ /** Render mermaid diagrams (needs the optional `mermaid` peer). Default: true */
35
+ mermaid?: boolean;
36
+ /** Mermaid theme. Default: `brand` (colours from the theme tokens) */
37
+ mermaidTheme?: 'brand' | 'default' | 'dark' | 'forest' | 'neutral';
38
+ /** Copy buttons on code blocks. Default: true */
39
+ copyButtons?: boolean;
40
+ /** Query parameter whose words are highlighted in the page. Default: `highlight` */
41
+ highlightParam?: string | null;
42
+ /** Heading levels in the table of contents. Default: 2 to 3, as Docusaurus */
43
+ tocMinDepth?: number;
44
+ tocMaxDepth?: number;
45
+ /** Width at which the TOC moves from a slide-out panel to a column. Default: `xl` */
46
+ tocBreakpoint?: 'lg' | 'xl' | '2xl';
47
+ /** Height of the fixed header, in px, for scroll offsets. Default: 80 */
48
+ headerOffset?: number;
49
+ /** Appended to the document title: `Page | Site`. Omit to leave `<head>` alone. */
50
+ siteTitle?: string;
51
+ /**
52
+ * Content between the title and the body (e.g. a lead paragraph or actions). Pages
53
+ * rendered with `hoistTitle` put it right under the title.
54
+ */
55
+ header?: Snippet;
56
+ /** Extra content in the page footer, next to the edit link (e.g. a "last verified" badge) */
57
+ meta?: Snippet;
58
+ class?: string;
59
+ }
60
+ declare const DocPage: Component<Props, {}, "">;
61
+ type DocPage = ReturnType<typeof DocPage>;
62
+ export default DocPage;
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ /**
3
+ * DocPager - previous / next links at the foot of a docs page (Docusaurus pagination).
4
+ *
5
+ * Pass `neighbours(sidebar.items, route)` from `@classic-homes/theme-docs/nav`.
6
+ */
7
+ import { cn } from '../utils.js';
8
+ import type { NavLink } from '../content/nav.js';
9
+
10
+ interface Props {
11
+ previous?: NavLink | null;
12
+ next?: NavLink | null;
13
+ previousLabel?: string;
14
+ nextLabel?: string;
15
+ class?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ let {
20
+ previous = null,
21
+ next = null,
22
+ previousLabel = 'Previous',
23
+ nextLabel = 'Next',
24
+ class: className,
25
+ ...restProps
26
+ }: Props = $props();
27
+
28
+ const card =
29
+ 'group flex flex-col gap-1 rounded-lg border border-border p-4 transition-colors hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
30
+ </script>
31
+
32
+ {#if previous || next}
33
+ <nav aria-label="Docs pages" class={cn('grid gap-4 sm:grid-cols-2', className)} {...restProps}>
34
+ {#if previous}
35
+ <a href={previous.route} rel="prev" class={card}>
36
+ <span class="text-xs text-muted-foreground">{previousLabel}</span>
37
+ <span class="font-medium text-primary">« {previous.title}</span>
38
+ </a>
39
+ {:else}
40
+ <span class="hidden sm:block"></span>
41
+ {/if}
42
+ {#if next}
43
+ <a href={next.route} rel="next" class={cn(card, 'sm:items-end sm:text-right')}>
44
+ <span class="text-xs text-muted-foreground">{nextLabel}</span>
45
+ <span class="font-medium text-primary">{next.title} »</span>
46
+ </a>
47
+ {/if}
48
+ </nav>
49
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { NavLink } from '../content/nav.js';
2
+ interface Props {
3
+ previous?: NavLink | null;
4
+ next?: NavLink | null;
5
+ previousLabel?: string;
6
+ nextLabel?: string;
7
+ class?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ declare const DocPager: import("svelte").Component<Props, {}, "">;
11
+ type DocPager = ReturnType<typeof DocPager>;
12
+ export default DocPager;
@@ -86,10 +86,12 @@
86
86
  {/if}
87
87
 
88
88
  {#if loading}
89
- <div class="flex items-center justify-center py-12">
89
+ <div class="flex items-center justify-center py-12" role="status">
90
90
  <div
91
91
  class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
92
+ aria-hidden="true"
92
93
  ></div>
94
+ <span class="docs-sr-only">Loading…</span>
93
95
  </div>
94
96
  {/if}
95
97
 
@@ -8,6 +8,7 @@
8
8
  * Requires mermaid to be installed in the consuming application:
9
9
  * npm install mermaid
10
10
  */
11
+ import { labelDiagram } from './enhance.js';
11
12
  import { cn } from '../utils.js';
12
13
 
13
14
  interface Props {
@@ -60,6 +61,7 @@
60
61
  const { svg } = await mermaid.default.render(`${id}-${++renderCount}`, currentCode);
61
62
  if (cancelled) return;
62
63
  container.innerHTML = svg;
64
+ labelDiagram(container);
63
65
  rendered = true;
64
66
  error = null;
65
67
  } catch (err) {
@@ -8,11 +8,15 @@
8
8
  * Requires mermaid to be installed in the consuming application:
9
9
  * npm install mermaid
10
10
  */
11
+ import { labelDiagram } from './enhance.js';
11
12
  import { onMount } from 'svelte';
12
13
 
13
14
  interface Props {
14
- /** Mermaid theme (default: 'default') */
15
- theme?: 'default' | 'dark' | 'forest' | 'neutral';
15
+ /**
16
+ * Mermaid theme (default: 'default'). `brand` colours diagrams from the theme tokens
17
+ * on the page (muted node fills, foreground text and lines, Figtree).
18
+ */
19
+ theme?: 'default' | 'dark' | 'forest' | 'neutral' | 'brand';
16
20
  /** Whether to watch for new diagrams via MutationObserver */
17
21
  watch?: boolean;
18
22
  }
@@ -21,6 +25,42 @@
21
25
 
22
26
  let initialized = $state(false);
23
27
 
28
+ /** A token's HSL triplet (`357 72% 39%`) as a colour mermaid's colour math can parse */
29
+ function token(styles: CSSStyleDeclaration, name: string, fallback: string): string {
30
+ const parts = styles.getPropertyValue(`--${name}`).trim().split(/\s+/);
31
+ return parts.length === 3 ? `hsl(${parts.join(', ')})` : fallback;
32
+ }
33
+
34
+ function brandConfig() {
35
+ const s = getComputedStyle(document.documentElement);
36
+ const foreground = token(s, 'foreground', '#4f4f4f');
37
+ return {
38
+ theme: 'base' as const,
39
+ themeVariables: {
40
+ fontFamily: 'Figtree, ui-sans-serif, system-ui, sans-serif',
41
+ fontSize: '14px',
42
+ background: token(s, 'background', '#ffffff'),
43
+ primaryColor: token(s, 'muted', '#e8e9e4'),
44
+ primaryTextColor: foreground,
45
+ primaryBorderColor: token(s, 'secondary', '#5f6150'),
46
+ secondaryColor: token(s, 'sidebar-background', '#f5f5f3'),
47
+ secondaryTextColor: foreground,
48
+ secondaryBorderColor: token(s, 'outline', '#8b8e7b'),
49
+ tertiaryColor: token(s, 'content-bg', '#fafafa'),
50
+ tertiaryTextColor: foreground,
51
+ tertiaryBorderColor: token(s, 'outline', '#8b8e7b'),
52
+ clusterBkg: token(s, 'content-bg', '#fafafa'),
53
+ clusterBorder: token(s, 'outline', '#8b8e7b'),
54
+ lineColor: token(s, 'muted-foreground', '#666666'),
55
+ textColor: foreground,
56
+ noteBkgColor: token(s, 'muted', '#e8e9e4'),
57
+ noteTextColor: foreground,
58
+ noteBorderColor: token(s, 'outline', '#8b8e7b'),
59
+ edgeLabelBackground: token(s, 'background', '#ffffff'),
60
+ },
61
+ };
62
+ }
63
+
24
64
  async function renderDiagrams() {
25
65
  try {
26
66
  const mermaid = await import('mermaid');
@@ -28,7 +68,7 @@
28
68
  // Initialize mermaid
29
69
  mermaid.default.initialize({
30
70
  startOnLoad: false,
31
- theme: theme,
71
+ ...(theme === 'brand' ? brandConfig() : { theme }),
32
72
  securityLevel: 'strict',
33
73
  });
34
74
 
@@ -45,6 +85,7 @@
45
85
 
46
86
  // Replace the placeholder with the rendered SVG
47
87
  diagram.innerHTML = svg;
88
+ labelDiagram(diagram);
48
89
  diagram.classList.add('mermaid-rendered');
49
90
  diagram.removeAttribute('data-mermaid');
50
91
  } catch (err) {
@@ -1,6 +1,9 @@
1
1
  interface Props {
2
- /** Mermaid theme (default: 'default') */
3
- theme?: 'default' | 'dark' | 'forest' | 'neutral';
2
+ /**
3
+ * Mermaid theme (default: 'default'). `brand` colours diagrams from the theme tokens
4
+ * on the page (muted node fills, foreground text and lines, Figtree).
5
+ */
6
+ theme?: 'default' | 'dark' | 'forest' | 'neutral' | 'brand';
4
7
  /** Whether to watch for new diagrams via MutationObserver */
5
8
  watch?: boolean;
6
9
  }