@classic-homes/theme-docs 0.2.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.
- package/dist/lib/components/CategoryIndex.svelte +2 -2
- package/dist/lib/components/DocPage.svelte +13 -6
- package/dist/lib/components/DocPage.svelte.d.ts +5 -3
- package/dist/lib/components/MermaidInit.svelte +42 -3
- package/dist/lib/components/MermaidInit.svelte.d.ts +5 -2
- package/dist/lib/content/render.d.ts +2 -2
- package/dist/lib/content/render.js +6 -1
- package/dist/lib/content/types.d.ts +6 -0
- package/dist/lib/search/index.d.ts +1 -1
- package/dist/lib/search/index.js +30 -5
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Pass a `CategoryPage` from `loadDocs`. Page cards show `descriptions[route]` when
|
|
7
7
|
* given; category cards show how many entries they hold.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { ActionHeader } from '@classic-homes/theme-svelte';
|
|
10
10
|
import DocsHub from './DocsHub.svelte';
|
|
11
11
|
import { countPages, flattenNav } from '../content/nav.js';
|
|
12
12
|
import type { CategoryPage } from '../content/types.js';
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
</script>
|
|
47
47
|
|
|
48
48
|
<div class={className}>
|
|
49
|
-
<
|
|
49
|
+
<ActionHeader class="mb-6" title={category.title} subtitle={category.description || undefined} />
|
|
50
50
|
<DocsHub {columns} config={{ sections: [{ id: 'items', items }] }} />
|
|
51
51
|
</div>
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* DocPage - one docs page, laid out the way Docusaurus lays out a doc.
|
|
4
4
|
*
|
|
5
5
|
* Takes HTML rendered at build time (`renderDocs`), so it renders on the server with no
|
|
6
|
-
* client-side parsing. Around the content: breadcrumbs,
|
|
7
|
-
* body has
|
|
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
8
|
* the browser it mounts allowlisted components, renders mermaid diagrams, adds copy
|
|
9
9
|
* buttons to code blocks and highlights `?highlight=` search terms.
|
|
10
10
|
*/
|
|
11
11
|
import type { Component, Snippet } from 'svelte';
|
|
12
|
-
import { Button } from '@classic-homes/theme-svelte';
|
|
12
|
+
import { ActionHeader, Button } from '@classic-homes/theme-svelte';
|
|
13
13
|
import { cn } from '../utils.js';
|
|
14
14
|
import Breadcrumbs from './Breadcrumbs.svelte';
|
|
15
15
|
import DocPager from './DocPager.svelte';
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
import type { RenderedDoc, Tag } from '../content/types.js';
|
|
23
23
|
|
|
24
24
|
interface Props {
|
|
25
|
-
page: Pick<RenderedDoc, 'title' | 'html' | 'toc' | 'hasH1'> & {
|
|
25
|
+
page: Pick<RenderedDoc, 'title' | 'heading' | 'html' | 'toc' | 'hasH1'> & {
|
|
26
|
+
description?: string;
|
|
27
|
+
};
|
|
26
28
|
/** `breadcrumbs(sidebar.items, route)` */
|
|
27
29
|
breadcrumbs?: Crumb[];
|
|
28
30
|
/** First breadcrumb, e.g. `{ label: 'Docs', href: '/' }` */
|
|
@@ -39,6 +41,8 @@
|
|
|
39
41
|
components?: Record<string, Component<any>>;
|
|
40
42
|
/** Render mermaid diagrams (needs the optional `mermaid` peer). Default: true */
|
|
41
43
|
mermaid?: boolean;
|
|
44
|
+
/** Mermaid theme. Default: `brand` (colours from the theme tokens) */
|
|
45
|
+
mermaidTheme?: 'brand' | 'default' | 'dark' | 'forest' | 'neutral';
|
|
42
46
|
/** Copy buttons on code blocks. Default: true */
|
|
43
47
|
copyButtons?: boolean;
|
|
44
48
|
/** Query parameter whose words are highlighted in the page. Default: `highlight` */
|
|
@@ -74,6 +78,7 @@
|
|
|
74
78
|
editLabel = 'Edit this page',
|
|
75
79
|
components,
|
|
76
80
|
mermaid = true,
|
|
81
|
+
mermaidTheme = 'brand',
|
|
77
82
|
copyButtons = true,
|
|
78
83
|
highlightParam = 'highlight',
|
|
79
84
|
tocMinDepth = 2,
|
|
@@ -125,14 +130,16 @@
|
|
|
125
130
|
<div class="min-w-0 max-w-3xl flex-1">
|
|
126
131
|
<Breadcrumbs crumbs={breadcrumbs} {home} />
|
|
127
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}
|
|
128
136
|
<article class="markdown-content">
|
|
129
|
-
{#if !page.hasH1}<h1>{page.title}</h1>{/if}
|
|
130
137
|
{@render header?.()}
|
|
131
138
|
{#key page.html}
|
|
132
139
|
<div bind:this={content}>{@html page.html}</div>
|
|
133
140
|
{/key}
|
|
134
141
|
</article>
|
|
135
|
-
{#if mermaid}<MermaidInit watch />{/if}
|
|
142
|
+
{#if mermaid}<MermaidInit watch theme={mermaidTheme} />{/if}
|
|
136
143
|
|
|
137
144
|
<footer class="mt-12 space-y-6">
|
|
138
145
|
{#if tags.length}<TagList {tags} href={tagHref} />{/if}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* DocPage - one docs page, laid out the way Docusaurus lays out a doc.
|
|
3
3
|
*
|
|
4
4
|
* Takes HTML rendered at build time (`renderDocs`), so it renders on the server with no
|
|
5
|
-
* client-side parsing. Around the content: breadcrumbs,
|
|
6
|
-
* body has
|
|
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
7
|
* the browser it mounts allowlisted components, renders mermaid diagrams, adds copy
|
|
8
8
|
* buttons to code blocks and highlights `?highlight=` search terms.
|
|
9
9
|
*/
|
|
@@ -11,7 +11,7 @@ import type { Component, Snippet } from 'svelte';
|
|
|
11
11
|
import type { Crumb, NavLink } from '../content/nav.js';
|
|
12
12
|
import type { RenderedDoc, Tag } from '../content/types.js';
|
|
13
13
|
interface Props {
|
|
14
|
-
page: Pick<RenderedDoc, 'title' | 'html' | 'toc' | 'hasH1'> & {
|
|
14
|
+
page: Pick<RenderedDoc, 'title' | 'heading' | 'html' | 'toc' | 'hasH1'> & {
|
|
15
15
|
description?: string;
|
|
16
16
|
};
|
|
17
17
|
/** `breadcrumbs(sidebar.items, route)` */
|
|
@@ -33,6 +33,8 @@ interface Props {
|
|
|
33
33
|
components?: Record<string, Component<any>>;
|
|
34
34
|
/** Render mermaid diagrams (needs the optional `mermaid` peer). Default: true */
|
|
35
35
|
mermaid?: boolean;
|
|
36
|
+
/** Mermaid theme. Default: `brand` (colours from the theme tokens) */
|
|
37
|
+
mermaidTheme?: 'brand' | 'default' | 'dark' | 'forest' | 'neutral';
|
|
36
38
|
/** Copy buttons on code blocks. Default: true */
|
|
37
39
|
copyButtons?: boolean;
|
|
38
40
|
/** Query parameter whose words are highlighted in the page. Default: `highlight` */
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
import { onMount } from 'svelte';
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
|
-
/**
|
|
16
|
-
|
|
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';
|
|
17
20
|
/** Whether to watch for new diagrams via MutationObserver */
|
|
18
21
|
watch?: boolean;
|
|
19
22
|
}
|
|
@@ -22,6 +25,42 @@
|
|
|
22
25
|
|
|
23
26
|
let initialized = $state(false);
|
|
24
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
|
+
|
|
25
64
|
async function renderDiagrams() {
|
|
26
65
|
try {
|
|
27
66
|
const mermaid = await import('mermaid');
|
|
@@ -29,7 +68,7 @@
|
|
|
29
68
|
// Initialize mermaid
|
|
30
69
|
mermaid.default.initialize({
|
|
31
70
|
startOnLoad: false,
|
|
32
|
-
theme: theme,
|
|
71
|
+
...(theme === 'brand' ? brandConfig() : { theme }),
|
|
33
72
|
securityLevel: 'strict',
|
|
34
73
|
});
|
|
35
74
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
interface Props {
|
|
2
|
-
/**
|
|
3
|
-
|
|
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
|
}
|
|
@@ -13,8 +13,8 @@ export interface RenderDocsOptions {
|
|
|
13
13
|
onBrokenAnchors?: BrokenLinkPolicy;
|
|
14
14
|
/**
|
|
15
15
|
* Lift a page's leading `# Heading` out of the body: it becomes the title when the page
|
|
16
|
-
* has no `title` front matter
|
|
17
|
-
* paragraph can sit between title and body). Default: false
|
|
16
|
+
* has no `title` front matter (else `heading`, when it differs), and the layout renders
|
|
17
|
+
* it (so content such as a lead paragraph can sit between title and body). Default: false
|
|
18
18
|
*/
|
|
19
19
|
hoistTitle?: boolean;
|
|
20
20
|
}
|
|
@@ -13,13 +13,17 @@ export async function renderDocs(docs, options = {}) {
|
|
|
13
13
|
const rendered = await parseMarkdown(markdown, parse);
|
|
14
14
|
let html = rendered.html;
|
|
15
15
|
let { title } = page;
|
|
16
|
+
let heading;
|
|
16
17
|
let toc = rendered.toc;
|
|
17
18
|
if (hoistTitle) {
|
|
18
19
|
const leading = /^\s*<h1\b[^>]*>([\s\S]*?)<\/h1>\s*/.exec(html);
|
|
19
20
|
if (leading) {
|
|
20
21
|
html = html.slice(leading[0].length);
|
|
22
|
+
const text = plainText(leading[1]);
|
|
21
23
|
if (page.frontmatter.title === undefined)
|
|
22
|
-
title =
|
|
24
|
+
title = text;
|
|
25
|
+
else if (text && text !== title)
|
|
26
|
+
heading = text;
|
|
23
27
|
toc = toc.filter((entry, i) => !(i === 0 && entry.level === 1));
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -32,6 +36,7 @@ export async function renderDocs(docs, options = {}) {
|
|
|
32
36
|
return {
|
|
33
37
|
...page,
|
|
34
38
|
title,
|
|
39
|
+
...(heading && { heading }),
|
|
35
40
|
html,
|
|
36
41
|
toc,
|
|
37
42
|
hasH1: toc.some((entry) => entry.level === 1) || /<h1[\s>]/.test(html),
|
|
@@ -85,6 +85,12 @@ export interface RenderedDoc extends Omit<DocSource, 'markdown'> {
|
|
|
85
85
|
toc: TocEntry[];
|
|
86
86
|
/** Whether the body has its own `<h1>`. When it doesn't, the layout shows `title`. */
|
|
87
87
|
hasH1: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* With `hoistTitle`: the text of the `# Heading` lifted out of the body, when it differs
|
|
90
|
+
* from `title`. Docusaurus shows the body heading on the page and keeps `title` for the
|
|
91
|
+
* sidebar and `<title>`, so layouts show this in place of `title` when set.
|
|
92
|
+
*/
|
|
93
|
+
heading?: string;
|
|
88
94
|
/** Searchable plain text: the page without markup, code blocks or diagrams */
|
|
89
95
|
text: string;
|
|
90
96
|
}
|
|
@@ -52,6 +52,6 @@ export interface SearchQueryOptions {
|
|
|
52
52
|
export declare function createSearch(indexJson: string | object, texts?: Map<string, string> | Record<string, string>): {
|
|
53
53
|
search(query: string, options?: SearchQueryOptions): SearchHit[];
|
|
54
54
|
};
|
|
55
|
-
/** About 160 characters around the first matched term. */
|
|
55
|
+
/** About 160 characters around the first matched term, cut at word boundaries. */
|
|
56
56
|
export declare function snippet(text: string, terms: string[]): string;
|
|
57
57
|
export {};
|
package/dist/lib/search/index.js
CHANGED
|
@@ -62,7 +62,7 @@ export function createSearch(indexJson, texts) {
|
|
|
62
62
|
title: stored.title,
|
|
63
63
|
description: stored.description,
|
|
64
64
|
sidebar: stored.sidebar,
|
|
65
|
-
snippet: snippet(textOf(route) ?? stored.description ?? '', hit.terms),
|
|
65
|
+
snippet: snippet(withoutTitle(textOf(route), stored.title) ?? stored.description ?? '', hit.terms),
|
|
66
66
|
terms: hit.terms,
|
|
67
67
|
score: hit.score,
|
|
68
68
|
};
|
|
@@ -70,13 +70,38 @@ export function createSearch(indexJson, texts) {
|
|
|
70
70
|
},
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* A page's text minus its leading title: the body's `# Title` heading is the first thing in
|
|
75
|
+
* the text, and the result already shows the title, so a snippet would repeat it.
|
|
76
|
+
*/
|
|
77
|
+
function withoutTitle(text, title) {
|
|
78
|
+
if (!text || !title || !text.startsWith(title))
|
|
79
|
+
return text;
|
|
80
|
+
return text.slice(title.length).trimStart();
|
|
81
|
+
}
|
|
82
|
+
/** About 160 characters around the first matched term, cut at word boundaries. */
|
|
74
83
|
export function snippet(text, terms) {
|
|
75
84
|
const lower = text.toLowerCase();
|
|
76
85
|
const at = Math.min(...terms.map((term) => lower.indexOf(term.toLowerCase())).filter((i) => i >= 0));
|
|
77
86
|
if (!Number.isFinite(at))
|
|
78
|
-
return text
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
return clip(text, 0, 160);
|
|
88
|
+
return clip(text, Math.max(0, at - 60), Math.max(0, at - 60) + 160, at);
|
|
89
|
+
}
|
|
90
|
+
/** `text[start, end)` widened or narrowed to whole words (never past `keep`), with ellipses. */
|
|
91
|
+
function clip(text, start, end, keep = start) {
|
|
92
|
+
if (start > 0) {
|
|
93
|
+
// Start after the space before the cut, so the snippet opens on a whole word.
|
|
94
|
+
const space = text.indexOf(' ', start);
|
|
95
|
+
if (space >= 0 && space < keep)
|
|
96
|
+
start = space + 1;
|
|
97
|
+
}
|
|
98
|
+
if (end < text.length) {
|
|
99
|
+
const space = text.lastIndexOf(' ', end);
|
|
100
|
+
if (space > keep)
|
|
101
|
+
end = space;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
end = text.length;
|
|
105
|
+
}
|
|
81
106
|
return `${start > 0 ? '…' : ''}${text.slice(start, end).trim()}${end < text.length ? '…' : ''}`;
|
|
82
107
|
}
|