@loidolt/theme-docs 0.0.0-stage → 0.8.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -2
  3. package/dist/components/CodeSnippet.svelte +86 -0
  4. package/dist/components/CodeSnippet.svelte.d.ts +21 -0
  5. package/dist/components/DocsCard.svelte +50 -0
  6. package/dist/components/DocsCard.svelte.d.ts +19 -0
  7. package/dist/components/DocsHub.svelte +51 -0
  8. package/dist/components/DocsHub.svelte.d.ts +14 -0
  9. package/dist/components/Markdown.svelte +90 -0
  10. package/dist/components/Markdown.svelte.d.ts +23 -0
  11. package/dist/components/MarkdownPage.svelte +121 -0
  12. package/dist/components/MarkdownPage.svelte.d.ts +34 -0
  13. package/dist/components/MermaidDiagram.svelte +75 -0
  14. package/dist/components/MermaidDiagram.svelte.d.ts +16 -0
  15. package/dist/components/TableOfContents.svelte +84 -0
  16. package/dist/components/TableOfContents.svelte.d.ts +24 -0
  17. package/dist/components/TocPanel.svelte +56 -0
  18. package/dist/components/TocPanel.svelte.d.ts +19 -0
  19. package/dist/core/escape.d.ts +6 -0
  20. package/dist/core/escape.js +16 -0
  21. package/dist/core/frontmatter.d.ts +11 -0
  22. package/dist/core/frontmatter.js +110 -0
  23. package/dist/core/highlight.d.ts +32 -0
  24. package/dist/core/highlight.js +52 -0
  25. package/dist/core/index.d.ts +11 -0
  26. package/dist/core/index.js +12 -0
  27. package/dist/core/mermaid.d.ts +17 -0
  28. package/dist/core/mermaid.js +49 -0
  29. package/dist/core/render.d.ts +12 -0
  30. package/dist/core/render.js +261 -0
  31. package/dist/core/slug.d.ts +7 -0
  32. package/dist/core/slug.js +21 -0
  33. package/dist/core/types.d.ts +83 -0
  34. package/dist/core/types.js +1 -0
  35. package/dist/core/url.d.ts +7 -0
  36. package/dist/core/url.js +26 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.js +9 -0
  39. package/dist/internal/enhance.d.ts +42 -0
  40. package/dist/internal/enhance.js +96 -0
  41. package/package.json +75 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Loidolt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,103 @@
1
- # Temporary Holding Version
1
+ # @loidolt/theme-docs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Accessible Markdown documentation for Svelte 5, in the Loidolt look: pages, contents, code and
4
+ diagrams.
5
+
6
+ - **Safe by default.** Raw HTML in markdown is shown as text, links and images keep only safe
7
+ URLs, and every extension escapes what it writes. Markdown from a CMS or a pull request cannot
8
+ put a script on your page unless you let it (`allowHtml`, ideally with a `sanitize` hook).
9
+ - **Server-rendered.** The whole document — headings, tables, footnotes, the table of contents —
10
+ is in the first render, so pages prerender with their content and search engines see it.
11
+ Highlighting and diagrams arrive in the browser, or render on the server with
12
+ `renderMarkdown` in a `load`.
13
+ - **Themed.** Rendered markdown takes `.ldt-prose`. Code is highlighted with Shiki's
14
+ CSS-variables theme mapped to the `--loidolt-syntax-*` tokens, so it follows light and dark
15
+ without a second pass; Mermaid diagrams are drawn in the theme's colours and redrawn when it
16
+ changes.
17
+ - **Accessible.** One level-1 heading per page; headings link to themselves; code blocks are
18
+ named and keyboard-reachable; task-list checkboxes, footnotes and diagrams all carry names.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install @loidolt/theme-docs
24
+ # optional: highlighting and diagrams
25
+ npm install shiki mermaid
26
+ ```
27
+
28
+ The docs styles ship with `@loidolt/theme-styles`. Register the optional pieces once:
29
+
30
+ ```ts
31
+ import { setHighlighterLoader, setMermaidLoader } from '@loidolt/theme-docs';
32
+
33
+ setHighlighterLoader(() => import('shiki'));
34
+ setMermaidLoader(() => import('mermaid').then((module) => module.default));
35
+ ```
36
+
37
+ Without them, code is plain and diagrams show their source — nothing breaks.
38
+
39
+ ```svelte
40
+ <script lang="ts">
41
+ import { MarkdownPage } from '@loidolt/theme-docs';
42
+ let { data } = $props();
43
+ </script>
44
+
45
+ <MarkdownPage source={data.markdown} next={{ title: 'Tokens', href: '/docs/tokens' }} />
46
+ ```
47
+
48
+ ## Components
49
+
50
+ | Component | What it is |
51
+ | ----------------- | ---------------------------------------------------------------------------- |
52
+ | `Markdown` | A markdown document as loidolt prose, with highlighting, diagrams and copy |
53
+ | `MarkdownPage` | A full page: title and description from frontmatter, contents, previous/next |
54
+ | `CodeSnippet` | One highlighted code block with a filename and copy button |
55
+ | `MermaidDiagram` | A diagram as a named picture with a description and its source |
56
+ | `TableOfContents` | A page's headings, marking the one being read |
57
+ | `TocPanel` | Contents beside the page, or behind a button on narrow screens |
58
+ | `DocsHub` | An index of pages in sections |
59
+ | `DocsCard` | One page in an index; the whole card is the link |
60
+
61
+ ## Markdown
62
+
63
+ GitHub-flavoured markdown (tables, task lists, strikethrough, autolinks), plus:
64
+
65
+ - **Frontmatter** in a leading `---` block: strings, numbers, booleans, lists and one level of
66
+ maps, parsed strictly (errors name the line). `parseFrontmatter` takes a full YAML parser.
67
+ - **Admonitions**: `:::note|tip|important|warning|caution [title]` … `:::`, and GitHub's
68
+ `> [!NOTE]` alerts, rendered in the alert tones as notes.
69
+ - **Footnotes**: `text[^id]` and `[^id]: note`, numbered by first use, linked both ways.
70
+ - **Definition lists**: a term line followed by `: definition` lines.
71
+ - **Code**: ` ```ts title="file.ts" ` (or ` ```ts:file.ts `) adds a filename.
72
+ - **Diagrams**: ` ```mermaid ` fences.
73
+
74
+ ### Rendering ahead of time
75
+
76
+ ```ts
77
+ // +page.server.ts
78
+ import { renderMarkdown } from '@loidolt/theme-docs/core';
79
+ import { createShikiHighlight } from '@loidolt/theme-docs/core';
80
+ import * as shiki from 'shiki';
81
+
82
+ export async function load() {
83
+ const result = await renderMarkdown(source, {
84
+ stripTitle: true,
85
+ highlight: createShikiHighlight(shiki),
86
+ });
87
+ return { result };
88
+ }
89
+ ```
90
+
91
+ ```svelte
92
+ <MarkdownPage result={data.result} />
93
+ ```
94
+
95
+ `renderMarkdown` returns `{ html, toc, headings, frontmatter, title, highlighted }`.
96
+ `renderMarkdownSync` does the same without highlighting. Options include `allowHtml`,
97
+ `sanitize`, `idPrefix` (for two documents on one page), `headingLinks`, `tocDepth`,
98
+ `stripTitle` and every user-facing string (`admonitionLabels`, `copyLabel`, `codeLabel`,
99
+ `taskLabels`, `footnotesLabel`, `footnoteBackLabel`).
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ import type { Attachment } from 'svelte/attachments';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { LiveRegion, createAnnouncer, cx } from '@loidolt/theme-svelte';
5
+ import { loadHighlighter } from '../core/highlight.js';
6
+ import { handleCopy } from '../internal/enhance.js';
7
+
8
+ interface Props extends HTMLAttributes<HTMLElement> {
9
+ code: string;
10
+ /** Language for highlighting, e.g. `ts`, `svelte`, `css`. */
11
+ lang?: string;
12
+ /** Shown above the code, and names it. */
13
+ filename?: string;
14
+ /** Names the code for assistive tech when there is no filename. Defaults to "‹lang› code". */
15
+ label?: string;
16
+ /** Offer a copy button. */
17
+ copyable?: boolean;
18
+ copyLabel?: string;
19
+ copiedLabel?: string;
20
+ /** Highlight with the registered Shiki loader, when there is one. */
21
+ highlight?: boolean;
22
+ class?: string;
23
+ ref?: HTMLElement | null;
24
+ }
25
+
26
+ let {
27
+ code,
28
+ lang = '',
29
+ filename,
30
+ label,
31
+ copyable = true,
32
+ copyLabel = 'Copy',
33
+ copiedLabel = 'Copied',
34
+ highlight = true,
35
+ class: className,
36
+ ref = $bindable(null),
37
+ ...rest
38
+ }: Props = $props();
39
+
40
+ const announcer = createAnnouncer();
41
+ let highlighted = $state<string | null>(null);
42
+
43
+ $effect(() => {
44
+ const [text, language, wanted] = [code, lang, highlight];
45
+ highlighted = null;
46
+ if (!wanted) return;
47
+ let cancelled = false;
48
+ void loadHighlighter()
49
+ .then((run) => run?.(text, language))
50
+ .then((html) => {
51
+ if (!cancelled && html) highlighted = html;
52
+ })
53
+ .catch(() => {});
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ });
58
+
59
+ const name = $derived(filename ?? label ?? (lang ? `${lang} code` : 'Code'));
60
+ const copy: Attachment<HTMLElement> = (node) =>
61
+ handleCopy(node, { copiedLabel, onCopied: () => announcer.announce(copiedLabel) });
62
+ </script>
63
+
64
+ <!-- eslint-disable svelte/no-at-html-tags -- Shiki's spans, built from escaped source -->
65
+ <figure
66
+ bind:this={ref}
67
+ class={cx('ldt-code ldt-code-snippet', className)}
68
+ data-lang={lang || undefined}
69
+ {@attach copy}
70
+ {...rest}
71
+ >
72
+ {#if filename || copyable}
73
+ <figcaption class="ldt-code__header">
74
+ {#if filename}<span class="ldt-code__filename">{filename}</span>{/if}
75
+ {#if copyable}<button type="button" class="ldt-code__copy" data-ldt-copy>{copyLabel}</button
76
+ >{/if}
77
+ </figcaption>
78
+ {/if}
79
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
80
+ <pre class="ldt-code__pre" tabindex="0" aria-label={name}><code
81
+ class={lang ? `language-${lang}` : undefined}
82
+ >{#if highlighted}{@html highlighted}{:else}{code}{/if}</code
83
+ ></pre>
84
+ <LiveRegion message={announcer.message} politeness={announcer.politeness} />
85
+ </figure>
86
+ <!-- eslint-enable svelte/no-at-html-tags -->
@@ -0,0 +1,21 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLElement> {
3
+ code: string;
4
+ /** Language for highlighting, e.g. `ts`, `svelte`, `css`. */
5
+ lang?: string;
6
+ /** Shown above the code, and names it. */
7
+ filename?: string;
8
+ /** Names the code for assistive tech when there is no filename. Defaults to "‹lang› code". */
9
+ label?: string;
10
+ /** Offer a copy button. */
11
+ copyable?: boolean;
12
+ copyLabel?: string;
13
+ copiedLabel?: string;
14
+ /** Highlight with the registered Shiki loader, when there is one. */
15
+ highlight?: boolean;
16
+ class?: string;
17
+ ref?: HTMLElement | null;
18
+ }
19
+ declare const CodeSnippet: import("svelte").Component<Props, {}, "ref">;
20
+ type CodeSnippet = ReturnType<typeof CodeSnippet>;
21
+ export default CodeSnippet;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { Card, cx } from '@loidolt/theme-svelte';
4
+ import type { HeadingLevel } from '@loidolt/theme-svelte';
5
+
6
+ interface Props {
7
+ title: string;
8
+ href: string;
9
+ description?: string;
10
+ /** A short label above the title, e.g. a category. */
11
+ eyebrow?: string;
12
+ /** Opens in a new tab, and says so to assistive tech. */
13
+ external?: boolean;
14
+ externalLabel?: string;
15
+ headingLevel?: HeadingLevel;
16
+ /** Anything else inside the card, below the description. */
17
+ children?: Snippet;
18
+ class?: string;
19
+ }
20
+
21
+ let {
22
+ title,
23
+ href,
24
+ description,
25
+ eyebrow,
26
+ external = false,
27
+ externalLabel = '(opens in a new tab)',
28
+ headingLevel = 3,
29
+ children,
30
+ class: className,
31
+ }: Props = $props();
32
+ </script>
33
+
34
+ <!-- The whole card is the link, so the target is the card, not a line of text inside it. -->
35
+ <Card
36
+ {href}
37
+ {headingLevel}
38
+ class={cx('ldt-docs-card', className)}
39
+ target={external ? '_blank' : undefined}
40
+ rel={external ? 'noopener' : undefined}
41
+ >
42
+ {#snippet header()}
43
+ {#if eyebrow}<p class="ldt-docs-card__eyebrow">{eyebrow}</p>{/if}
44
+ <svelte:element this={`h${headingLevel}`} class="ldt-card__title"
45
+ >{title}{#if external}<span class="ldt-sr-only"> {externalLabel}</span>{/if}</svelte:element
46
+ >
47
+ {#if description}<p class="ldt-card__description">{description}</p>{/if}
48
+ {/snippet}
49
+ {@render children?.()}
50
+ </Card>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HeadingLevel } from '@loidolt/theme-svelte';
3
+ interface Props {
4
+ title: string;
5
+ href: string;
6
+ description?: string;
7
+ /** A short label above the title, e.g. a category. */
8
+ eyebrow?: string;
9
+ /** Opens in a new tab, and says so to assistive tech. */
10
+ external?: boolean;
11
+ externalLabel?: string;
12
+ headingLevel?: HeadingLevel;
13
+ /** Anything else inside the card, below the description. */
14
+ children?: Snippet;
15
+ class?: string;
16
+ }
17
+ declare const DocsCard: import("svelte").Component<Props, {}, "">;
18
+ type DocsCard = ReturnType<typeof DocsCard>;
19
+ export default DocsCard;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { cx } from '@loidolt/theme-svelte';
4
+ import type { HeadingLevel } from '@loidolt/theme-svelte';
5
+ import type { DocsSection } from '../core/types.js';
6
+ import DocsCard from './DocsCard.svelte';
7
+
8
+ interface Props extends HTMLAttributes<HTMLDivElement> {
9
+ /** Groups of pages, each with a heading and a grid of cards. */
10
+ sections: DocsSection[];
11
+ /** Level of the section headings; the cards take the next one. */
12
+ headingLevel?: HeadingLevel;
13
+ class?: string;
14
+ ref?: HTMLDivElement | null;
15
+ }
16
+
17
+ let {
18
+ sections,
19
+ headingLevel = 2,
20
+ class: className,
21
+ ref = $bindable(null),
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ const cardLevel = $derived(Math.min(headingLevel + 1, 6) as HeadingLevel);
26
+ </script>
27
+
28
+ <div bind:this={ref} class={cx('ldt-docs-hub', className)} {...rest}>
29
+ {#each sections as section (section.title)}
30
+ <section class="ldt-docs-hub__section">
31
+ <svelte:element this={`h${headingLevel}`} class="ldt-docs-hub__title"
32
+ >{section.title}</svelte:element
33
+ >
34
+ {#if section.description}<p class="ldt-docs-hub__description">{section.description}</p>{/if}
35
+ <ul class="ldt-docs-hub__grid">
36
+ {#each section.items as item (item.href)}
37
+ <li>
38
+ <DocsCard
39
+ title={item.title}
40
+ href={item.href}
41
+ description={item.description}
42
+ eyebrow={item.eyebrow}
43
+ external={item.external}
44
+ headingLevel={cardLevel}
45
+ />
46
+ </li>
47
+ {/each}
48
+ </ul>
49
+ </section>
50
+ {/each}
51
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { HeadingLevel } from '@loidolt/theme-svelte';
3
+ import type { DocsSection } from '../core/types.js';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ /** Groups of pages, each with a heading and a grid of cards. */
6
+ sections: DocsSection[];
7
+ /** Level of the section headings; the cards take the next one. */
8
+ headingLevel?: HeadingLevel;
9
+ class?: string;
10
+ ref?: HTMLDivElement | null;
11
+ }
12
+ declare const DocsHub: import("svelte").Component<Props, {}, "ref">;
13
+ type DocsHub = ReturnType<typeof DocsHub>;
14
+ export default DocsHub;
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { Attachment } from 'svelte/attachments';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import { LiveRegion, createAnnouncer, createTokenColors, cx } from '@loidolt/theme-svelte';
6
+ import { renderMarkdown, renderMarkdownSync } from '../core/render.js';
7
+ import type { MarkdownOptions, RenderedMarkdown } from '../core/types.js';
8
+ import { diagramRoles, enhanceDiagrams, handleCopy } from '../internal/enhance.js';
9
+
10
+ interface Props extends HTMLAttributes<HTMLElement> {
11
+ /** Markdown to render. The first render is synchronous, so it is in the server's HTML. */
12
+ source?: string;
13
+ /** Or a result from `renderMarkdown` — e.g. rendered and highlighted in a server `load`. */
14
+ result?: RenderedMarkdown;
15
+ /** Rendering options: HTML policy, heading links, labels… See `MarkdownOptions`. */
16
+ options?: MarkdownOptions;
17
+ /** What a copy button says, and what is announced, once its code is copied. */
18
+ copiedLabel?: string;
19
+ /** Names a drawn Mermaid diagram. */
20
+ diagramLabel?: string;
21
+ /** Labels the disclosure holding a diagram's source. */
22
+ diagramSourceLabel?: string;
23
+ /** Called with each render — the synchronous one, then the highlighted one if it differs. */
24
+ onRender?: (result: RenderedMarkdown) => void;
25
+ class?: string;
26
+ ref?: HTMLElement | null;
27
+ }
28
+
29
+ let {
30
+ source = '',
31
+ result,
32
+ options = {},
33
+ copiedLabel = 'Copied',
34
+ diagramLabel = 'Diagram',
35
+ diagramSourceLabel = 'Diagram source',
36
+ onRender,
37
+ class: className,
38
+ ref = $bindable(null),
39
+ ...rest
40
+ }: Props = $props();
41
+
42
+ const announcer = createAnnouncer();
43
+ const palette = createTokenColors(diagramRoles, { element: () => ref });
44
+
45
+ const initial = $derived(result ?? renderMarkdownSync(source, options));
46
+ let highlighted = $state.raw<RenderedMarkdown | null>(null);
47
+ const shown = $derived(highlighted ?? initial);
48
+
49
+ // In the browser, render again with highlighting when a highlighter is registered.
50
+ $effect(() => {
51
+ const [text, settings, given] = [source, options, result];
52
+ highlighted = null;
53
+ if (given || !text) return;
54
+ let cancelled = false;
55
+ void renderMarkdown(text, settings).then((next) => {
56
+ if (!cancelled && next.highlighted) highlighted = next;
57
+ });
58
+ return () => {
59
+ cancelled = true;
60
+ };
61
+ });
62
+
63
+ $effect(() => {
64
+ const current = shown;
65
+ untrack(() => onRender?.(current));
66
+ });
67
+
68
+ // Diagrams: drawn once the HTML is in place, and redrawn when the theme changes.
69
+ $effect(() => {
70
+ void shown.html;
71
+ const colors = palette.colors;
72
+ const root = ref;
73
+ if (root)
74
+ void enhanceDiagrams(root, colors, { label: diagramLabel, sourceLabel: diagramSourceLabel });
75
+ });
76
+
77
+ $effect(() => {
78
+ if (ref) untrack(() => palette.refresh());
79
+ });
80
+
81
+ const copy: Attachment<HTMLElement> = (node) =>
82
+ handleCopy(node, { copiedLabel, onCopied: () => announcer.announce(copiedLabel) });
83
+ </script>
84
+
85
+ <article bind:this={ref} class={cx('ldt-prose ldt-markdown', className)} {@attach copy} {...rest}>
86
+ <!-- Rendered by `renderMarkdown`: raw HTML escaped unless `allowHtml`, URLs allow-listed. -->
87
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
88
+ {@html shown.html}
89
+ </article>
90
+ <LiveRegion message={announcer.message} politeness={announcer.politeness} />
@@ -0,0 +1,23 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { MarkdownOptions, RenderedMarkdown } from '../core/types.js';
3
+ interface Props extends HTMLAttributes<HTMLElement> {
4
+ /** Markdown to render. The first render is synchronous, so it is in the server's HTML. */
5
+ source?: string;
6
+ /** Or a result from `renderMarkdown` — e.g. rendered and highlighted in a server `load`. */
7
+ result?: RenderedMarkdown;
8
+ /** Rendering options: HTML policy, heading links, labels… See `MarkdownOptions`. */
9
+ options?: MarkdownOptions;
10
+ /** What a copy button says, and what is announced, once its code is copied. */
11
+ copiedLabel?: string;
12
+ /** Names a drawn Mermaid diagram. */
13
+ diagramLabel?: string;
14
+ /** Labels the disclosure holding a diagram's source. */
15
+ diagramSourceLabel?: string;
16
+ /** Called with each render — the synchronous one, then the highlighted one if it differs. */
17
+ onRender?: (result: RenderedMarkdown) => void;
18
+ class?: string;
19
+ ref?: HTMLElement | null;
20
+ }
21
+ declare const Markdown: import("svelte").Component<Props, {}, "ref">;
22
+ type Markdown = ReturnType<typeof Markdown>;
23
+ export default Markdown;
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { PageHeader, cx } from '@loidolt/theme-svelte';
5
+ import { renderMarkdownSync } from '../core/render.js';
6
+ import type { DocsLink, MarkdownOptions, RenderedMarkdown } from '../core/types.js';
7
+ import Markdown from './Markdown.svelte';
8
+ import TocPanel from './TocPanel.svelte';
9
+
10
+ interface Props extends HTMLAttributes<HTMLDivElement> {
11
+ source?: string;
12
+ /** A result rendered ahead of time — pass `stripTitle: true` when rendering it. */
13
+ result?: RenderedMarkdown;
14
+ options?: MarkdownOptions;
15
+ /**
16
+ * The page title. Defaults to the frontmatter `title`, then the document's first `#`
17
+ * heading, which is left out of the body so the page has one level-1 heading.
18
+ */
19
+ title?: string;
20
+ /** Defaults to the frontmatter `description`. */
21
+ description?: string;
22
+ /** Defaults to the frontmatter `eyebrow` or `section`. */
23
+ eyebrow?: string;
24
+ /** Show the table of contents beside the page (behind a button on narrow screens). */
25
+ toc?: boolean;
26
+ tocTitle?: string;
27
+ previous?: DocsLink;
28
+ next?: DocsLink;
29
+ previousLabel?: string;
30
+ nextLabel?: string;
31
+ /** Names the previous/next navigation. */
32
+ pagerLabel?: string;
33
+ /** Content after the document, before the previous/next links. */
34
+ footer?: Snippet;
35
+ class?: string;
36
+ ref?: HTMLDivElement | null;
37
+ }
38
+
39
+ let {
40
+ source = '',
41
+ result,
42
+ options = {},
43
+ title,
44
+ description,
45
+ eyebrow,
46
+ toc = true,
47
+ tocTitle = 'On this page',
48
+ previous,
49
+ next,
50
+ previousLabel = 'Previous',
51
+ nextLabel = 'Next',
52
+ pagerLabel = 'More pages',
53
+ footer,
54
+ class: className,
55
+ ref = $bindable(null),
56
+ ...rest
57
+ }: Props = $props();
58
+
59
+ let article = $state<HTMLElement | null>(null);
60
+ const pageOptions = $derived({ ...options, stripTitle: true });
61
+ // The synchronous render gives the server its title and contents; `Markdown` reports any
62
+ // later (highlighted) render, which has the same headings.
63
+ let reported = $state.raw<RenderedMarkdown | null>(null);
64
+ const current = $derived(reported ?? result ?? renderMarkdownSync(source, pageOptions));
65
+
66
+ const text = (value: unknown) => (typeof value === 'string' ? value : undefined);
67
+ const meta = $derived(current.frontmatter ?? {});
68
+ const heading = $derived(title ?? current.title ?? '');
69
+ const hasToc = $derived(toc && current.toc.length > 0);
70
+ </script>
71
+
72
+ <div
73
+ bind:this={ref}
74
+ class={cx('ldt-markdown-page', hasToc && 'ldt-markdown-page--toc', className)}
75
+ {...rest}
76
+ >
77
+ <!-- First in reading order: the "On this page" button leads on narrow screens, and the
78
+ contents sit beside the page on wide ones. -->
79
+ {#if hasToc}
80
+ <TocPanel
81
+ class="ldt-markdown-page__toc"
82
+ items={current.toc}
83
+ title={tocTitle}
84
+ container={article}
85
+ />
86
+ {/if}
87
+ <div class="ldt-markdown-page__main">
88
+ {#if heading}
89
+ <PageHeader
90
+ title={heading}
91
+ description={description ?? text(meta.description)}
92
+ eyebrow={eyebrow ?? text(meta.eyebrow) ?? text(meta.section)}
93
+ headingLevel={1}
94
+ />
95
+ {/if}
96
+ <Markdown
97
+ bind:ref={article}
98
+ {source}
99
+ {result}
100
+ options={pageOptions}
101
+ onRender={(rendered) => (reported = rendered)}
102
+ />
103
+ {@render footer?.()}
104
+ {#if previous || next}
105
+ <nav class="ldt-markdown-page__pager" aria-label={pagerLabel}>
106
+ {#if previous}
107
+ <a class="ldt-markdown-page__previous" href={previous.href} rel="prev">
108
+ <span class="ldt-markdown-page__direction">{previousLabel}</span>
109
+ <span>{previous.title}</span>
110
+ </a>
111
+ {/if}
112
+ {#if next}
113
+ <a class="ldt-markdown-page__next" href={next.href} rel="next">
114
+ <span class="ldt-markdown-page__direction">{nextLabel}</span>
115
+ <span>{next.title}</span>
116
+ </a>
117
+ {/if}
118
+ </nav>
119
+ {/if}
120
+ </div>
121
+ </div>
@@ -0,0 +1,34 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { DocsLink, MarkdownOptions, RenderedMarkdown } from '../core/types.js';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ source?: string;
6
+ /** A result rendered ahead of time — pass `stripTitle: true` when rendering it. */
7
+ result?: RenderedMarkdown;
8
+ options?: MarkdownOptions;
9
+ /**
10
+ * The page title. Defaults to the frontmatter `title`, then the document's first `#`
11
+ * heading, which is left out of the body so the page has one level-1 heading.
12
+ */
13
+ title?: string;
14
+ /** Defaults to the frontmatter `description`. */
15
+ description?: string;
16
+ /** Defaults to the frontmatter `eyebrow` or `section`. */
17
+ eyebrow?: string;
18
+ /** Show the table of contents beside the page (behind a button on narrow screens). */
19
+ toc?: boolean;
20
+ tocTitle?: string;
21
+ previous?: DocsLink;
22
+ next?: DocsLink;
23
+ previousLabel?: string;
24
+ nextLabel?: string;
25
+ /** Names the previous/next navigation. */
26
+ pagerLabel?: string;
27
+ /** Content after the document, before the previous/next links. */
28
+ footer?: Snippet;
29
+ class?: string;
30
+ ref?: HTMLDivElement | null;
31
+ }
32
+ declare const MarkdownPage: import("svelte").Component<Props, {}, "ref">;
33
+ type MarkdownPage = ReturnType<typeof MarkdownPage>;
34
+ export default MarkdownPage;