@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.
- package/dist/lib/components/Breadcrumbs.svelte +55 -0
- package/dist/lib/components/Breadcrumbs.svelte.d.ts +14 -0
- package/dist/lib/components/CategoryIndex.svelte +51 -0
- package/dist/lib/components/CategoryIndex.svelte.d.ts +13 -0
- package/dist/lib/components/DocPage.svelte +172 -0
- package/dist/lib/components/DocPage.svelte.d.ts +60 -0
- package/dist/lib/components/DocPager.svelte +49 -0
- package/dist/lib/components/DocPager.svelte.d.ts +12 -0
- package/dist/lib/components/MarkdownPage.svelte +3 -1
- package/dist/lib/components/MermaidDiagram.svelte +2 -0
- package/dist/lib/components/MermaidInit.svelte +2 -0
- package/dist/lib/components/TableOfContents.svelte +114 -125
- package/dist/lib/components/TableOfContents.svelte.d.ts +11 -4
- package/dist/lib/components/TagIndex.svelte +42 -0
- package/dist/lib/components/TagIndex.svelte.d.ts +14 -0
- package/dist/lib/components/TagList.svelte +45 -0
- package/dist/lib/components/TagList.svelte.d.ts +15 -0
- package/dist/lib/components/TocPanel.svelte +27 -9
- package/dist/lib/components/TocPanel.svelte.d.ts +10 -3
- package/dist/lib/components/enhance.d.ts +29 -0
- package/dist/lib/components/enhance.js +179 -0
- package/dist/lib/components/mount.d.ts +14 -0
- package/dist/lib/components/mount.js +36 -0
- package/dist/lib/components/sidebar.d.ts +33 -0
- package/dist/lib/components/sidebar.js +84 -0
- package/dist/lib/content/browser.d.ts +6 -0
- package/dist/lib/content/browser.js +5 -0
- package/dist/lib/content/index.d.ts +13 -0
- package/dist/lib/content/index.js +12 -0
- package/dist/lib/content/load.d.ts +77 -0
- package/dist/lib/content/load.js +366 -0
- package/dist/lib/content/nav.d.ts +36 -0
- package/dist/lib/content/nav.js +81 -0
- package/dist/lib/content/render.d.ts +38 -0
- package/dist/lib/content/render.js +84 -0
- package/dist/lib/content/types.d.ts +90 -0
- package/dist/lib/content/types.js +5 -0
- package/dist/lib/index.d.ts +16 -2
- package/dist/lib/index.js +16 -2
- package/dist/lib/parser/api.d.ts +12 -0
- package/dist/lib/parser/api.js +10 -0
- package/dist/lib/parser/extensions.d.ts +51 -17
- package/dist/lib/parser/extensions.js +133 -60
- package/dist/lib/parser/index.d.ts +9 -2
- package/dist/lib/parser/index.js +125 -35
- package/dist/lib/parser/slug.d.ts +19 -0
- package/dist/lib/parser/slug.js +55 -0
- package/dist/lib/sanitize/index.d.ts +11 -0
- package/dist/lib/sanitize/index.js +122 -0
- package/dist/lib/search/index.d.ts +57 -0
- package/dist/lib/search/index.js +82 -0
- package/dist/lib/styles/markdown.css +161 -1
- package/dist/lib/types/frontmatter.d.ts +32 -0
- package/dist/lib/vite/index.d.ts +17 -0
- package/dist/lib/vite/index.js +38 -0
- package/package.json +51 -4
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* TableOfContents -
|
|
3
|
+
* TableOfContents - "On this page" navigation with scroll-spy
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Pass `toc` (from `parseMarkdown`/`renderDocs`) to render on the server, or `html` to
|
|
6
|
+
* extract headings in the browser. The heading being read is marked `aria-current`.
|
|
7
|
+
* Entries are plain `#id` links, so the browser (or your router) scrolls and records the
|
|
8
|
+
* hash; give headings a `scroll-margin-top` to clear a fixed header (`.markdown-content`
|
|
9
|
+
* does, from `--docs-header-offset`). Styled after Docusaurus.
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
11
|
+
import { untrack } from 'svelte';
|
|
9
12
|
import { cn } from '../utils.js';
|
|
10
13
|
import type { TocEntry } from '../types/docs.js';
|
|
11
14
|
|
|
12
15
|
interface Props {
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
+
/** Prebuilt entries, e.g. `page.toc` from `renderDocs`. Takes precedence over `html`. */
|
|
17
|
+
toc?: TocEntry[];
|
|
18
|
+
/** HTML to extract headings from in the browser, when there is no `toc` */
|
|
19
|
+
html?: string;
|
|
20
|
+
/** Shallowest heading level to include (default: 1) */
|
|
21
|
+
minDepth?: number;
|
|
22
|
+
/** Deepest heading level to include (default: 3) */
|
|
16
23
|
maxDepth?: number;
|
|
24
|
+
/** Height of any fixed header, in px: where a heading counts as reached (default: 80) */
|
|
25
|
+
offset?: number;
|
|
17
26
|
/** Custom class */
|
|
18
27
|
class?: string;
|
|
19
|
-
/** Title for the TOC section */
|
|
28
|
+
/** Title for the TOC section. Empty hides it (the nav is then labelled "On this page"). */
|
|
20
29
|
title?: string;
|
|
21
30
|
/** Make the TOC sticky */
|
|
22
31
|
sticky?: boolean;
|
|
@@ -24,151 +33,131 @@
|
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
let {
|
|
36
|
+
toc,
|
|
27
37
|
html,
|
|
38
|
+
minDepth = 1,
|
|
28
39
|
maxDepth = 3,
|
|
40
|
+
offset = 80,
|
|
29
41
|
class: className,
|
|
30
42
|
title = 'On this page',
|
|
31
43
|
sticky = false,
|
|
32
44
|
...restProps
|
|
33
45
|
}: Props = $props();
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
let activeId = $state<string | null>(null);
|
|
37
|
-
let ignoreObserver = false;
|
|
38
|
-
|
|
39
|
-
onMount(async () => {
|
|
40
|
-
const { extractToc } = await import('../parser/index.js');
|
|
41
|
-
toc = extractToc(html, maxDepth);
|
|
42
|
-
|
|
43
|
-
// Set up intersection observer to track active heading
|
|
44
|
-
// 100ms delay ensures DOM is fully rendered after markdown parsing completes
|
|
45
|
-
// This is necessary because markdown content is rendered asynchronously
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
const headings = document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]');
|
|
48
|
-
if (headings.length === 0) return;
|
|
49
|
-
|
|
50
|
-
// Set initial active heading based on scroll position
|
|
51
|
-
const firstVisibleHeading = Array.from(headings).find((heading) => {
|
|
52
|
-
const rect = heading.getBoundingClientRect();
|
|
53
|
-
return rect.top >= 0 && rect.top < window.innerHeight / 2;
|
|
54
|
-
});
|
|
55
|
-
if (firstVisibleHeading) {
|
|
56
|
-
activeId = firstVisibleHeading.id;
|
|
57
|
-
} else if (headings[0]) {
|
|
58
|
-
activeId = headings[0].id;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const observer = new IntersectionObserver(
|
|
62
|
-
(entries) => {
|
|
63
|
-
// Skip if we recently clicked a TOC link
|
|
64
|
-
if (ignoreObserver) return;
|
|
47
|
+
const uid = $props.id();
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
(a, b) => a.boundingClientRect.top - b.boundingClientRect.top
|
|
72
|
-
)[0];
|
|
73
|
-
activeId = topmost.target.id;
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
// rootMargin: top right bottom left
|
|
78
|
-
// -80px top: accounts for fixed header height (~80px)
|
|
79
|
-
// -70% bottom: heading becomes "active" when it enters the top 30% of viewport
|
|
80
|
-
// This creates a natural reading experience where the heading is highlighted
|
|
81
|
-
// before the user has scrolled past it
|
|
82
|
-
rootMargin: '-80px 0px -70% 0px',
|
|
83
|
-
threshold: 0,
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
headings.forEach((heading) => observer.observe(heading));
|
|
88
|
-
|
|
89
|
-
return () => {
|
|
90
|
-
headings.forEach((heading) => observer.unobserve(heading));
|
|
91
|
-
};
|
|
92
|
-
}, 100);
|
|
93
|
-
});
|
|
49
|
+
let extracted = $state<TocEntry[]>([]);
|
|
50
|
+
const entries = $derived(
|
|
51
|
+
(toc ?? extracted).filter((entry) => entry.level >= minDepth && entry.level <= maxDepth)
|
|
52
|
+
);
|
|
53
|
+
let activeId = $state<string | null>(null);
|
|
94
54
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
55
|
+
// Without a prebuilt toc, extract one from the HTML. The parser loads lazily so pages
|
|
56
|
+
// that pass `toc` never download it.
|
|
57
|
+
$effect(() => {
|
|
58
|
+
if (toc || !html) return;
|
|
59
|
+
const source = html;
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
void import('../parser/index.js').then(({ extractToc }) => {
|
|
62
|
+
if (!cancelled) extracted = extractToc(source, 6);
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
cancelled = true;
|
|
104
66
|
};
|
|
105
|
-
|
|
106
|
-
}
|
|
67
|
+
});
|
|
107
68
|
|
|
108
|
-
|
|
109
|
-
|
|
69
|
+
// Scroll-spy over this TOC's own headings, as Docusaurus does it: on scroll, the current
|
|
70
|
+
// heading is the last one above the header line. At the bottom of the page, where the
|
|
71
|
+
// last headings can't reach that line, a clicked heading that's on screen stays current.
|
|
72
|
+
$effect(() => {
|
|
73
|
+
const ids = entries.map((entry) => entry.id);
|
|
74
|
+
const line = offset + 24;
|
|
75
|
+
if (ids.length === 0) return;
|
|
110
76
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
77
|
+
let headings: HTMLElement[] = [];
|
|
78
|
+
let frame = 0;
|
|
114
79
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
80
|
+
function update() {
|
|
81
|
+
frame = 0;
|
|
82
|
+
if (headings.length === 0) return;
|
|
83
|
+
let current = headings[0];
|
|
84
|
+
for (const heading of headings) {
|
|
85
|
+
if (heading.getBoundingClientRect().top > line) break;
|
|
86
|
+
current = heading;
|
|
87
|
+
}
|
|
88
|
+
const atBottom =
|
|
89
|
+
window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
|
|
90
|
+
const chosen = activeId ? document.getElementById(activeId) : null;
|
|
91
|
+
if (atBottom && chosen && headings.includes(chosen)) {
|
|
92
|
+
const top = chosen.getBoundingClientRect().top;
|
|
93
|
+
if (top >= 0 && top < window.innerHeight) return;
|
|
94
|
+
}
|
|
95
|
+
activeId = current.id;
|
|
118
96
|
}
|
|
97
|
+
// Throttle to one update per frame; hidden documents get no frames, so update directly.
|
|
98
|
+
const schedule = () => {
|
|
99
|
+
if (document.hidden) update();
|
|
100
|
+
else if (!frame) frame = requestAnimationFrame(update);
|
|
101
|
+
};
|
|
119
102
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
setTimeout(() => {
|
|
137
|
-
ignoreObserver = false;
|
|
138
|
-
}, 1000); // Longer timeout to cover scroll animation
|
|
139
|
-
|
|
140
|
-
// Find the heading element and scroll to it with offset
|
|
141
|
-
const element = document.getElementById(id);
|
|
142
|
-
if (element) {
|
|
143
|
-
const headerOffset = 100; // Account for fixed header + some padding
|
|
144
|
-
const elementPosition = element.getBoundingClientRect().top;
|
|
145
|
-
const offsetPosition = elementPosition + window.scrollY - headerOffset;
|
|
103
|
+
// Effects run after the DOM update, so the headings these entries describe are in place.
|
|
104
|
+
headings = ids
|
|
105
|
+
.map((id) => document.getElementById(id))
|
|
106
|
+
.filter((el): el is HTMLElement => el !== null);
|
|
107
|
+
// untrack: update() reads activeId, which must not become a dependency of this effect
|
|
108
|
+
untrack(update);
|
|
109
|
+
// Capture, so scrolling inside a layout's own scroll container counts too.
|
|
110
|
+
window.addEventListener('scroll', schedule, { passive: true, capture: true });
|
|
111
|
+
window.addEventListener('resize', schedule, { passive: true });
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
cancelAnimationFrame(frame);
|
|
115
|
+
window.removeEventListener('scroll', schedule, { capture: true });
|
|
116
|
+
window.removeEventListener('resize', schedule);
|
|
117
|
+
};
|
|
118
|
+
});
|
|
146
119
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
120
|
+
const indents: Record<number, string> = {
|
|
121
|
+
3: 'pl-3',
|
|
122
|
+
4: 'pl-6',
|
|
123
|
+
5: 'pl-9',
|
|
124
|
+
6: 'pl-12',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function linkClass(level: number, isActive: boolean): string {
|
|
128
|
+
const base = 'block py-1 rounded-sm';
|
|
129
|
+
if (isActive) return cn(base, 'text-primary font-medium');
|
|
130
|
+
if (level <= 2)
|
|
131
|
+
return cn(base, 'text-foreground/80 hover:text-foreground hover:bg-muted-hover');
|
|
132
|
+
return cn(base, 'text-muted-foreground text-[12px] hover:text-foreground hover:bg-muted-hover');
|
|
152
133
|
}
|
|
153
134
|
</script>
|
|
154
135
|
|
|
155
|
-
{#if
|
|
156
|
-
<nav
|
|
136
|
+
{#if entries.length > 0}
|
|
137
|
+
<nav
|
|
138
|
+
class={cn('text-sm', sticky && 'sticky top-4', className)}
|
|
139
|
+
aria-labelledby={title ? `${uid}-title` : undefined}
|
|
140
|
+
aria-label={title ? undefined : 'On this page'}
|
|
141
|
+
{...restProps}
|
|
142
|
+
>
|
|
157
143
|
{#if title}
|
|
158
|
-
<
|
|
144
|
+
<p
|
|
145
|
+
id="{uid}-title"
|
|
146
|
+
class="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2"
|
|
147
|
+
>
|
|
159
148
|
{title}
|
|
160
|
-
</
|
|
149
|
+
</p>
|
|
161
150
|
{/if}
|
|
162
151
|
|
|
163
152
|
<ul class="space-y-0.5">
|
|
164
|
-
{#each
|
|
153
|
+
{#each entries as entry (entry.id)}
|
|
165
154
|
{@const isActive = activeId === entry.id}
|
|
166
|
-
<li class={
|
|
155
|
+
<li class={indents[entry.level] ?? ''}>
|
|
167
156
|
<a
|
|
168
157
|
href="#{entry.id}"
|
|
169
|
-
onclick={(
|
|
170
|
-
class={
|
|
171
|
-
aria-current={isActive ? '
|
|
158
|
+
onclick={() => (activeId = entry.id)}
|
|
159
|
+
class={linkClass(entry.level, isActive)}
|
|
160
|
+
aria-current={isActive ? 'location' : undefined}
|
|
172
161
|
>
|
|
173
162
|
{entry.text}
|
|
174
163
|
</a>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import type { TocEntry } from '../types/docs.js';
|
|
1
2
|
interface Props {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
/**
|
|
3
|
+
/** Prebuilt entries, e.g. `page.toc` from `renderDocs`. Takes precedence over `html`. */
|
|
4
|
+
toc?: TocEntry[];
|
|
5
|
+
/** HTML to extract headings from in the browser, when there is no `toc` */
|
|
6
|
+
html?: string;
|
|
7
|
+
/** Shallowest heading level to include (default: 1) */
|
|
8
|
+
minDepth?: number;
|
|
9
|
+
/** Deepest heading level to include (default: 3) */
|
|
5
10
|
maxDepth?: number;
|
|
11
|
+
/** Height of any fixed header, in px: where a heading counts as reached (default: 80) */
|
|
12
|
+
offset?: number;
|
|
6
13
|
/** Custom class */
|
|
7
14
|
class?: string;
|
|
8
|
-
/** Title for the TOC section */
|
|
15
|
+
/** Title for the TOC section. Empty hides it (the nav is then labelled "On this page"). */
|
|
9
16
|
title?: string;
|
|
10
17
|
/** Make the TOC sticky */
|
|
11
18
|
sticky?: boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TagIndex - every tag in use, grouped by first letter (the Docusaurus `/tags` page).
|
|
4
|
+
*
|
|
5
|
+
* Pass `tagCounts(tags, readablePages)` from `@classic-homes/theme-docs/nav`.
|
|
6
|
+
*/
|
|
7
|
+
import { cn } from '../utils.js';
|
|
8
|
+
import TagList from './TagList.svelte';
|
|
9
|
+
import type { Tag } from '../content/types.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
tags: (Tag & { count: number })[];
|
|
13
|
+
href?: (tag: Tag) => string;
|
|
14
|
+
/** Heading level for each letter (default: `h2`) */
|
|
15
|
+
as?: 'h2' | 'h3';
|
|
16
|
+
class?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { tags, href, as = 'h2', class: className, ...restProps }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const groups = $derived.by(() => {
|
|
23
|
+
const byLetter = new Map<string, (Tag & { count: number })[]>();
|
|
24
|
+
for (const tag of [...tags].sort((a, b) => a.label.localeCompare(b.label))) {
|
|
25
|
+
const letter = tag.label.charAt(0).toUpperCase();
|
|
26
|
+
if (!byLetter.has(letter)) byLetter.set(letter, []);
|
|
27
|
+
byLetter.get(letter)!.push(tag);
|
|
28
|
+
}
|
|
29
|
+
return [...byLetter];
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class={cn('space-y-6', className)} {...restProps}>
|
|
34
|
+
{#each groups as [letter, group] (letter)}
|
|
35
|
+
<section>
|
|
36
|
+
<svelte:element this={as} class="mb-2 text-lg font-semibold text-foreground"
|
|
37
|
+
>{letter}</svelte:element
|
|
38
|
+
>
|
|
39
|
+
<TagList tags={group} {href} label="Tags starting with {letter}" />
|
|
40
|
+
</section>
|
|
41
|
+
{/each}
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Tag } from '../content/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
tags: (Tag & {
|
|
4
|
+
count: number;
|
|
5
|
+
})[];
|
|
6
|
+
href?: (tag: Tag) => string;
|
|
7
|
+
/** Heading level for each letter (default: `h2`) */
|
|
8
|
+
as?: 'h2' | 'h3';
|
|
9
|
+
class?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
declare const TagIndex: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type TagIndex = ReturnType<typeof TagIndex>;
|
|
14
|
+
export default TagIndex;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TagList - a page's tags as links to their tag pages.
|
|
4
|
+
*/
|
|
5
|
+
import { Badge } from '@classic-homes/theme-svelte';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
7
|
+
import type { Tag } from '../content/types.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
tags: (Tag & { count?: number })[];
|
|
11
|
+
/** Route of a tag's page (default: `/docs/tags/<id>`) */
|
|
12
|
+
href?: (tag: Tag) => string;
|
|
13
|
+
/** Accessible name of the list (default: "Tags") */
|
|
14
|
+
label?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
tags,
|
|
21
|
+
href = (tag) => `/docs/tags/${encodeURIComponent(tag.id)}`,
|
|
22
|
+
label = 'Tags',
|
|
23
|
+
class: className,
|
|
24
|
+
...restProps
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
{#if tags.length}
|
|
29
|
+
<ul aria-label={label} class={cn('flex flex-wrap gap-2', className)} {...restProps}>
|
|
30
|
+
{#each tags as tag (tag.id)}
|
|
31
|
+
<li>
|
|
32
|
+
<a
|
|
33
|
+
href={href(tag)}
|
|
34
|
+
title={tag.description || undefined}
|
|
35
|
+
class="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
36
|
+
>
|
|
37
|
+
<Badge variant="secondary" clickable>
|
|
38
|
+
{tag.label}{#if tag.count !== undefined}<span class="ml-1 opacity-70">{tag.count}</span
|
|
39
|
+
>{/if}
|
|
40
|
+
</Badge>
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
{/each}
|
|
44
|
+
</ul>
|
|
45
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Tag } from '../content/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
tags: (Tag & {
|
|
4
|
+
count?: number;
|
|
5
|
+
})[];
|
|
6
|
+
/** Route of a tag's page (default: `/docs/tags/<id>`) */
|
|
7
|
+
href?: (tag: Tag) => string;
|
|
8
|
+
/** Accessible name of the list (default: "Tags") */
|
|
9
|
+
label?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
declare const TagList: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type TagList = ReturnType<typeof TagList>;
|
|
15
|
+
export default TagList;
|
|
@@ -5,19 +5,28 @@
|
|
|
5
5
|
* Provides a mobile flyout panel and optional desktop sidebar for the
|
|
6
6
|
* TableOfContents component. Features a paper folder tab trigger on mobile
|
|
7
7
|
* that slides out a panel from the right side. The tab is physically attached
|
|
8
|
-
* to the panel so they move together as one unit.
|
|
8
|
+
* to the panel so they move together as one unit. While open, the panel is a modal
|
|
9
|
+
* dialog that keeps focus inside; while closed, its links are inert.
|
|
9
10
|
*/
|
|
10
11
|
import { onMount } from 'svelte';
|
|
11
12
|
import { fade } from 'svelte/transition';
|
|
13
|
+
import { useFocusTrap } from '@classic-homes/theme-svelte';
|
|
12
14
|
import { cn } from '../utils.js';
|
|
13
15
|
import TableOfContents from './TableOfContents.svelte';
|
|
16
|
+
import type { TocEntry } from '../types/docs.js';
|
|
14
17
|
|
|
15
18
|
interface Props {
|
|
16
|
-
/**
|
|
17
|
-
|
|
19
|
+
/** Prebuilt entries, e.g. `page.toc` from `renderDocs`. Takes precedence over `html`. */
|
|
20
|
+
toc?: TocEntry[];
|
|
21
|
+
/** HTML to extract headings from in the browser, when there is no `toc` */
|
|
22
|
+
html?: string;
|
|
23
|
+
/** Shallowest heading level to include (default: 1) */
|
|
24
|
+
minDepth?: number;
|
|
18
25
|
/** Maximum heading depth to include (default: 3) */
|
|
19
26
|
maxDepth?: number;
|
|
20
|
-
/**
|
|
27
|
+
/** Height of any fixed header, in px (default: 80) */
|
|
28
|
+
offset?: number;
|
|
29
|
+
/** Breakpoint for desktop view (default: 'lg') */
|
|
21
30
|
breakpoint?: 'lg' | 'xl' | '2xl';
|
|
22
31
|
/** Show desktop sidebar (default: true) */
|
|
23
32
|
showDesktop?: boolean;
|
|
@@ -31,8 +40,11 @@
|
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
let {
|
|
43
|
+
toc,
|
|
34
44
|
html,
|
|
45
|
+
minDepth = 1,
|
|
35
46
|
maxDepth = 3,
|
|
47
|
+
offset = 80,
|
|
36
48
|
breakpoint = 'lg',
|
|
37
49
|
showDesktop = true,
|
|
38
50
|
mobileClass,
|
|
@@ -42,6 +54,8 @@
|
|
|
42
54
|
}: Props = $props();
|
|
43
55
|
|
|
44
56
|
let open = $state(false);
|
|
57
|
+
const { action: trapFocus } = useFocusTrap({ active: () => open });
|
|
58
|
+
const hasContent = $derived(toc ? toc.length > 0 : Boolean(html));
|
|
45
59
|
let tabButtonRef = $state<HTMLButtonElement | null>(null);
|
|
46
60
|
let previousActiveElement: HTMLElement | null = null;
|
|
47
61
|
|
|
@@ -135,7 +149,7 @@
|
|
|
135
149
|
</script>
|
|
136
150
|
|
|
137
151
|
<!-- Mobile TOC Panel with attached tab - always rendered, slides via CSS transform -->
|
|
138
|
-
{#if
|
|
152
|
+
{#if hasContent}
|
|
139
153
|
<!-- Backdrop - only shown when open -->
|
|
140
154
|
{#if open}
|
|
141
155
|
<button
|
|
@@ -148,7 +162,11 @@
|
|
|
148
162
|
|
|
149
163
|
<!-- Panel container with attached tab -->
|
|
150
164
|
<div
|
|
165
|
+
use:trapFocus
|
|
151
166
|
class={cn(breakpointHide, 'toc-panel-container', open && 'toc-panel-open', mobileClass)}
|
|
167
|
+
role={open ? 'dialog' : undefined}
|
|
168
|
+
aria-modal={open ? 'true' : undefined}
|
|
169
|
+
aria-label={open ? title : undefined}
|
|
152
170
|
{...restProps}
|
|
153
171
|
>
|
|
154
172
|
<!-- Tab - physically attached to panel -->
|
|
@@ -165,7 +183,7 @@
|
|
|
165
183
|
</button>
|
|
166
184
|
|
|
167
185
|
<!-- Panel content -->
|
|
168
|
-
<div class="toc-panel-content">
|
|
186
|
+
<div class="toc-panel-content" inert={!open}>
|
|
169
187
|
<div class="toc-panel-header">
|
|
170
188
|
<span class="text-sm font-medium text-foreground">{title}</span>
|
|
171
189
|
<button class={closeButtonClasses} onclick={close} aria-label="Close table of contents">
|
|
@@ -186,14 +204,14 @@
|
|
|
186
204
|
</button>
|
|
187
205
|
</div>
|
|
188
206
|
<div class="p-5">
|
|
189
|
-
<TableOfContents {html} {maxDepth} title="" />
|
|
207
|
+
<TableOfContents {toc} {html} {minDepth} {maxDepth} {offset} title="" />
|
|
190
208
|
</div>
|
|
191
209
|
</div>
|
|
192
210
|
</div>
|
|
193
211
|
{/if}
|
|
194
212
|
|
|
195
213
|
<!-- Desktop TOC Sidebar -->
|
|
196
|
-
{#if
|
|
214
|
+
{#if hasContent && showDesktop}
|
|
197
215
|
<aside
|
|
198
216
|
class={cn(
|
|
199
217
|
breakpointShow,
|
|
@@ -201,7 +219,7 @@
|
|
|
201
219
|
desktopClass
|
|
202
220
|
)}
|
|
203
221
|
>
|
|
204
|
-
<TableOfContents {html} {maxDepth} {title} />
|
|
222
|
+
<TableOfContents {toc} {html} {minDepth} {maxDepth} {offset} {title} />
|
|
205
223
|
</aside>
|
|
206
224
|
{/if}
|
|
207
225
|
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import type { TocEntry } from '../types/docs.js';
|
|
1
2
|
interface Props {
|
|
2
|
-
/**
|
|
3
|
-
|
|
3
|
+
/** Prebuilt entries, e.g. `page.toc` from `renderDocs`. Takes precedence over `html`. */
|
|
4
|
+
toc?: TocEntry[];
|
|
5
|
+
/** HTML to extract headings from in the browser, when there is no `toc` */
|
|
6
|
+
html?: string;
|
|
7
|
+
/** Shallowest heading level to include (default: 1) */
|
|
8
|
+
minDepth?: number;
|
|
4
9
|
/** Maximum heading depth to include (default: 3) */
|
|
5
10
|
maxDepth?: number;
|
|
6
|
-
/**
|
|
11
|
+
/** Height of any fixed header, in px (default: 80) */
|
|
12
|
+
offset?: number;
|
|
13
|
+
/** Breakpoint for desktop view (default: 'lg') */
|
|
7
14
|
breakpoint?: 'lg' | 'xl' | '2xl';
|
|
8
15
|
/** Show desktop sidebar (default: true) */
|
|
9
16
|
showDesktop?: boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressive enhancements for rendered markdown. Call them once the HTML is in the DOM
|
|
3
|
+
* (e.g. from an `$effect`); each returns a cleanup function that undoes its changes.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Add a "Copy code" button to every highlighted code block under `root`, as Docusaurus
|
|
7
|
+
* does. A polite live region announces "Copied" for screen readers.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* $effect(() => enhanceCodeBlocks(article));
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function enhanceCodeBlocks(root: ParentNode): () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Wrap occurrences of `terms` in text under `root` with `<mark class="search-highlight">`,
|
|
17
|
+
* as Docusaurus does for `?_highlight=` after a search. Code blocks are left alone.
|
|
18
|
+
* Returns a cleanup that removes the marks. Scrolls nothing; the first mark is returned
|
|
19
|
+
* for the caller to scroll to if it wants.
|
|
20
|
+
*/
|
|
21
|
+
export declare function highlightTerms(root: ParentNode, terms: string[]): {
|
|
22
|
+
cleanup: () => void;
|
|
23
|
+
first: HTMLElement | null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Give a rendered mermaid SVG an image role and a name: its `accTitle`/`<title>` when it
|
|
27
|
+
* has one, else "Diagram". Mermaid's own `aria-labelledby` (from `accTitle`) is kept.
|
|
28
|
+
*/
|
|
29
|
+
export declare function labelDiagram(container: Element): void;
|