@classic-homes/theme-docs 0.0.50 → 0.2.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 (56) 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 +172 -0
  6. package/dist/lib/components/DocPage.svelte.d.ts +60 -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 +2 -0
  12. package/dist/lib/components/TableOfContents.svelte +114 -125
  13. package/dist/lib/components/TableOfContents.svelte.d.ts +11 -4
  14. package/dist/lib/components/TagIndex.svelte +42 -0
  15. package/dist/lib/components/TagIndex.svelte.d.ts +14 -0
  16. package/dist/lib/components/TagList.svelte +45 -0
  17. package/dist/lib/components/TagList.svelte.d.ts +15 -0
  18. package/dist/lib/components/TocPanel.svelte +27 -9
  19. package/dist/lib/components/TocPanel.svelte.d.ts +10 -3
  20. package/dist/lib/components/enhance.d.ts +29 -0
  21. package/dist/lib/components/enhance.js +179 -0
  22. package/dist/lib/components/mount.d.ts +14 -0
  23. package/dist/lib/components/mount.js +36 -0
  24. package/dist/lib/components/sidebar.d.ts +33 -0
  25. package/dist/lib/components/sidebar.js +84 -0
  26. package/dist/lib/content/browser.d.ts +6 -0
  27. package/dist/lib/content/browser.js +5 -0
  28. package/dist/lib/content/index.d.ts +13 -0
  29. package/dist/lib/content/index.js +12 -0
  30. package/dist/lib/content/load.d.ts +77 -0
  31. package/dist/lib/content/load.js +366 -0
  32. package/dist/lib/content/nav.d.ts +36 -0
  33. package/dist/lib/content/nav.js +81 -0
  34. package/dist/lib/content/render.d.ts +38 -0
  35. package/dist/lib/content/render.js +84 -0
  36. package/dist/lib/content/types.d.ts +90 -0
  37. package/dist/lib/content/types.js +5 -0
  38. package/dist/lib/index.d.ts +16 -2
  39. package/dist/lib/index.js +16 -2
  40. package/dist/lib/parser/api.d.ts +12 -0
  41. package/dist/lib/parser/api.js +10 -0
  42. package/dist/lib/parser/extensions.d.ts +51 -17
  43. package/dist/lib/parser/extensions.js +133 -60
  44. package/dist/lib/parser/index.d.ts +9 -2
  45. package/dist/lib/parser/index.js +125 -35
  46. package/dist/lib/parser/slug.d.ts +19 -0
  47. package/dist/lib/parser/slug.js +55 -0
  48. package/dist/lib/sanitize/index.d.ts +11 -0
  49. package/dist/lib/sanitize/index.js +122 -0
  50. package/dist/lib/search/index.d.ts +57 -0
  51. package/dist/lib/search/index.js +82 -0
  52. package/dist/lib/styles/markdown.css +161 -1
  53. package/dist/lib/types/frontmatter.d.ts +32 -0
  54. package/dist/lib/vite/index.d.ts +17 -0
  55. package/dist/lib/vite/index.js +38 -0
  56. 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 { PageHeader } 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
+ <PageHeader class="mb-8" 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,172 @@
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, a `<h1>` from the title when the
7
+ * body has none, 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 { 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' | 'html' | 'toc' | 'hasH1'> & { description?: string };
26
+ /** `breadcrumbs(sidebar.items, route)` */
27
+ breadcrumbs?: Crumb[];
28
+ /** First breadcrumb, e.g. `{ label: 'Docs', href: '/' }` */
29
+ home?: { label: string; href: string };
30
+ previous?: NavLink | null;
31
+ next?: NavLink | null;
32
+ /** The page's tags, resolved to labels */
33
+ tags?: Tag[];
34
+ tagHref?: (tag: Tag) => string;
35
+ /** "Edit this page" target, e.g. the file on GitHub */
36
+ editUrl?: string;
37
+ editLabel?: string;
38
+ /** Components for `<Name … />` placeholders (see `parseMarkdown`'s `components`) */
39
+ components?: Record<string, Component<any>>;
40
+ /** Render mermaid diagrams (needs the optional `mermaid` peer). Default: true */
41
+ mermaid?: boolean;
42
+ /** Copy buttons on code blocks. Default: true */
43
+ copyButtons?: boolean;
44
+ /** Query parameter whose words are highlighted in the page. Default: `highlight` */
45
+ highlightParam?: string | null;
46
+ /** Heading levels in the table of contents. Default: 2 to 3, as Docusaurus */
47
+ tocMinDepth?: number;
48
+ tocMaxDepth?: number;
49
+ /** Width at which the TOC moves from a slide-out panel to a column. Default: `xl` */
50
+ tocBreakpoint?: 'lg' | 'xl' | '2xl';
51
+ /** Height of the fixed header, in px, for scroll offsets. Default: 80 */
52
+ headerOffset?: number;
53
+ /** Appended to the document title: `Page | Site`. Omit to leave `<head>` alone. */
54
+ siteTitle?: string;
55
+ /**
56
+ * Content between the title and the body (e.g. a lead paragraph or actions). Pages
57
+ * rendered with `hoistTitle` put it right under the title.
58
+ */
59
+ header?: Snippet;
60
+ /** Extra content in the page footer, next to the edit link (e.g. a "last verified" badge) */
61
+ meta?: Snippet;
62
+ class?: string;
63
+ }
64
+
65
+ let {
66
+ page,
67
+ breadcrumbs = [],
68
+ home,
69
+ previous = null,
70
+ next = null,
71
+ tags = [],
72
+ tagHref,
73
+ editUrl,
74
+ editLabel = 'Edit this page',
75
+ components,
76
+ mermaid = true,
77
+ copyButtons = true,
78
+ highlightParam = 'highlight',
79
+ tocMinDepth = 2,
80
+ tocMaxDepth = 3,
81
+ tocBreakpoint = 'xl',
82
+ headerOffset = 80,
83
+ siteTitle,
84
+ header,
85
+ meta,
86
+ class: className,
87
+ }: Props = $props();
88
+
89
+ let content = $state<HTMLElement>();
90
+
91
+ const showToc = $derived(
92
+ page.toc.filter((e) => e.level >= tocMinDepth && e.level <= tocMaxDepth).length > 1
93
+ );
94
+
95
+ // Enhance the rendered HTML. `content` is keyed on the HTML below, so each page gets a
96
+ // fresh element and these DOM changes never meet Svelte's own bookkeeping.
97
+ $effect(() => {
98
+ if (!content) return;
99
+ const root = content;
100
+ const cleanups: (() => void)[] = [];
101
+ if (components) cleanups.push(mountComponents(root, components));
102
+ if (copyButtons) cleanups.push(enhanceCodeBlocks(root));
103
+ if (highlightParam) {
104
+ const query = new URL(location.href).searchParams.get(highlightParam);
105
+ if (query) {
106
+ const { cleanup, first } = highlightTerms(root, query.split(/\s+/));
107
+ cleanups.push(cleanup);
108
+ if (first && !location.hash) first.scrollIntoView({ block: 'center' });
109
+ }
110
+ }
111
+ return () => {
112
+ for (const cleanup of cleanups.reverse()) cleanup();
113
+ };
114
+ });
115
+ </script>
116
+
117
+ <svelte:head>
118
+ {#if siteTitle}
119
+ <title>{page.title} | {siteTitle}</title>
120
+ {#if page.description}<meta name="description" content={page.description} />{/if}
121
+ {/if}
122
+ </svelte:head>
123
+
124
+ <div class={cn('flex gap-8', className)} style="--docs-header-offset: {headerOffset + 16}px">
125
+ <div class="min-w-0 max-w-3xl flex-1">
126
+ <Breadcrumbs crumbs={breadcrumbs} {home} />
127
+
128
+ <article class="markdown-content">
129
+ {#if !page.hasH1}<h1>{page.title}</h1>{/if}
130
+ {@render header?.()}
131
+ {#key page.html}
132
+ <div bind:this={content}>{@html page.html}</div>
133
+ {/key}
134
+ </article>
135
+ {#if mermaid}<MermaidInit watch />{/if}
136
+
137
+ <footer class="mt-12 space-y-6">
138
+ {#if tags.length}<TagList {tags} href={tagHref} />{/if}
139
+ {#if editUrl || meta}
140
+ <div class="flex flex-wrap items-center justify-between gap-2">
141
+ {#if editUrl}
142
+ <Button variant="link" href={editUrl} class="px-0">
143
+ <svg
144
+ aria-hidden="true"
145
+ class="mr-1.5 h-4 w-4"
146
+ viewBox="0 0 24 24"
147
+ fill="none"
148
+ stroke="currentColor"
149
+ stroke-width="2"
150
+ stroke-linecap="round"
151
+ stroke-linejoin="round"
152
+ ><path d="M12 20h9" /><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" /></svg
153
+ >{editLabel}
154
+ </Button>
155
+ {:else}<span></span>{/if}
156
+ {@render meta?.()}
157
+ </div>
158
+ {/if}
159
+ <DocPager {previous} {next} />
160
+ </footer>
161
+ </div>
162
+
163
+ {#if showToc}
164
+ <TocPanel
165
+ toc={page.toc}
166
+ minDepth={tocMinDepth}
167
+ maxDepth={tocMaxDepth}
168
+ breakpoint={tocBreakpoint}
169
+ offset={headerOffset}
170
+ />
171
+ {/if}
172
+ </div>
@@ -0,0 +1,60 @@
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, a `<h1>` from the title when the
6
+ * body has none, 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' | '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
+ /** Copy buttons on code blocks. Default: true */
37
+ copyButtons?: boolean;
38
+ /** Query parameter whose words are highlighted in the page. Default: `highlight` */
39
+ highlightParam?: string | null;
40
+ /** Heading levels in the table of contents. Default: 2 to 3, as Docusaurus */
41
+ tocMinDepth?: number;
42
+ tocMaxDepth?: number;
43
+ /** Width at which the TOC moves from a slide-out panel to a column. Default: `xl` */
44
+ tocBreakpoint?: 'lg' | 'xl' | '2xl';
45
+ /** Height of the fixed header, in px, for scroll offsets. Default: 80 */
46
+ headerOffset?: number;
47
+ /** Appended to the document title: `Page | Site`. Omit to leave `<head>` alone. */
48
+ siteTitle?: string;
49
+ /**
50
+ * Content between the title and the body (e.g. a lead paragraph or actions). Pages
51
+ * rendered with `hoistTitle` put it right under the title.
52
+ */
53
+ header?: Snippet;
54
+ /** Extra content in the page footer, next to the edit link (e.g. a "last verified" badge) */
55
+ meta?: Snippet;
56
+ class?: string;
57
+ }
58
+ declare const DocPage: Component<Props, {}, "">;
59
+ type DocPage = ReturnType<typeof DocPage>;
60
+ 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,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 { onMount } from 'svelte';
12
13
 
13
14
  interface Props {
@@ -45,6 +46,7 @@
45
46
 
46
47
  // Replace the placeholder with the rendered SVG
47
48
  diagram.innerHTML = svg;
49
+ labelDiagram(diagram);
48
50
  diagram.classList.add('mermaid-rendered');
49
51
  diagram.removeAttribute('data-mermaid');
50
52
  } catch (err) {