@prudentbird/voxx 1.0.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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.mjs +1346 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
- package/templates/blog/hello-world.md.tpl +42 -0
- package/templates/blog/layout.tsx.tpl +44 -0
- package/templates/blog/page.tsx.tpl +27 -0
- package/templates/blog/post-list.tsx.tpl +45 -0
- package/templates/blog/post-page.tsx.tpl +41 -0
- package/templates/blog/slug-page.tsx.tpl +40 -0
- package/templates/changelog/layout.tsx.tpl +44 -0
- package/templates/changelog/page.tsx.tpl +27 -0
- package/templates/changelog/release-list.tsx.tpl +33 -0
- package/templates/changelog/release.md.tpl +15 -0
- package/templates/docs/doc-page.tsx.tpl +65 -0
- package/templates/docs/getting-started-index.md.tpl +9 -0
- package/templates/docs/index.md.tpl +16 -0
- package/templates/docs/installation.md.tpl +17 -0
- package/templates/docs/layout-root.tsx.tpl +52 -0
- package/templates/docs/layout.tsx.tpl +37 -0
- package/templates/docs/mobile-nav.tsx.tpl +90 -0
- package/templates/docs/page.tsx.tpl +50 -0
- package/templates/docs/sidebar-nav.tsx.tpl +39 -0
- package/templates/shared/content-version.ts.tpl +1 -0
- package/templates/shared/data.ts.tpl +34 -0
- package/templates/shared/instrumentation.ts.tpl +5 -0
- package/templates/shared/llms-full-route.ts.tpl +9 -0
- package/templates/shared/llms-route.ts.tpl +9 -0
- package/templates/shared/metadata.ts.tpl +39 -0
- package/templates/shared/on-this-page.tsx.tpl +213 -0
- package/templates/shared/robots.ts.tpl +13 -0
- package/templates/shared/rss-route.ts.tpl +9 -0
- package/templates/shared/sitemap.ts.tpl +23 -0
- package/templates/shared/theme-toggle.tsx.tpl +64 -0
- package/templates/shared/voxx.json.tpl +26 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import type { TocItem } from "@prudentbird/voxx-core";
|
|
5
|
+
|
|
6
|
+
type TrackSvg = { path: string; width: number; height: number };
|
|
7
|
+
|
|
8
|
+
function getItemOffset(depth: number): number {
|
|
9
|
+
if (depth <= 2) return 14;
|
|
10
|
+
if (depth === 3) return 26;
|
|
11
|
+
return 36;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getLineOffset(depth: number): number {
|
|
15
|
+
return depth >= 3 ? 10 : 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function OnThisPage({ toc }: { toc: TocItem[] }) {
|
|
19
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const thumbRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const [active, setActive] = useState<string[]>([]);
|
|
22
|
+
const [svg, setSvg] = useState<TrackSvg | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const ids = toc.map((t) => t.id);
|
|
26
|
+
const visible = new Set<string>();
|
|
27
|
+
const observer = new IntersectionObserver(
|
|
28
|
+
(entries) => {
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.isIntersecting) visible.add(entry.target.id);
|
|
31
|
+
else visible.delete(entry.target.id);
|
|
32
|
+
}
|
|
33
|
+
if (visible.size > 0) {
|
|
34
|
+
setActive(ids.filter((id) => visible.has(id)));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const viewTop = entries[0]?.rootBounds?.top ?? 0;
|
|
38
|
+
let fallback: string | undefined;
|
|
39
|
+
let min = -1;
|
|
40
|
+
for (const id of ids) {
|
|
41
|
+
const el = document.getElementById(id);
|
|
42
|
+
if (!el) continue;
|
|
43
|
+
const d = Math.abs(viewTop - el.getBoundingClientRect().top);
|
|
44
|
+
if (min === -1 || d < min) {
|
|
45
|
+
fallback = id;
|
|
46
|
+
min = d;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setActive(fallback ? [fallback] : []);
|
|
50
|
+
},
|
|
51
|
+
{ rootMargin: "0px", threshold: 0.98 },
|
|
52
|
+
);
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
const el = document.getElementById(id);
|
|
55
|
+
if (el) observer.observe(el);
|
|
56
|
+
}
|
|
57
|
+
return () => observer.disconnect();
|
|
58
|
+
}, [toc]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const container = containerRef.current;
|
|
62
|
+
if (!container) return;
|
|
63
|
+
|
|
64
|
+
function onResize() {
|
|
65
|
+
if (!container || container.clientHeight === 0) return;
|
|
66
|
+
let width = 0;
|
|
67
|
+
let height = 0;
|
|
68
|
+
const path: string[] = [];
|
|
69
|
+
for (let i = 0; i < toc.length; i++) {
|
|
70
|
+
const item = toc[i];
|
|
71
|
+
if (!item) continue;
|
|
72
|
+
const el = container.querySelector<HTMLAnchorElement>(
|
|
73
|
+
`a[href="#${item.id}"]`,
|
|
74
|
+
);
|
|
75
|
+
if (!el) continue;
|
|
76
|
+
const styles = getComputedStyle(el);
|
|
77
|
+
const offset = getLineOffset(item.depth) + 1;
|
|
78
|
+
const top = el.offsetTop + parseFloat(styles.paddingTop);
|
|
79
|
+
const bottom =
|
|
80
|
+
el.offsetTop + el.clientHeight - parseFloat(styles.paddingBottom);
|
|
81
|
+
width = Math.max(offset, width);
|
|
82
|
+
height = Math.max(height, bottom);
|
|
83
|
+
path.push(`${i === 0 ? "M" : "L"}${offset} ${top}`);
|
|
84
|
+
path.push(`L${offset} ${bottom}`);
|
|
85
|
+
}
|
|
86
|
+
setSvg({ path: path.join(" "), width: width + 1, height });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const observer = new ResizeObserver(onResize);
|
|
90
|
+
onResize();
|
|
91
|
+
observer.observe(container);
|
|
92
|
+
return () => observer.disconnect();
|
|
93
|
+
}, [toc]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const container = containerRef.current;
|
|
97
|
+
const thumb = thumbRef.current;
|
|
98
|
+
if (!container || !thumb) return;
|
|
99
|
+
let top = Number.MAX_VALUE;
|
|
100
|
+
let bottom = 0;
|
|
101
|
+
for (const id of active) {
|
|
102
|
+
const el = container.querySelector<HTMLAnchorElement>(`a[href="#${id}"]`);
|
|
103
|
+
if (!el) continue;
|
|
104
|
+
const styles = getComputedStyle(el);
|
|
105
|
+
top = Math.min(top, el.offsetTop + parseFloat(styles.paddingTop));
|
|
106
|
+
bottom = Math.max(
|
|
107
|
+
bottom,
|
|
108
|
+
el.offsetTop + el.clientHeight - parseFloat(styles.paddingBottom),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (active.length === 0 || container.clientHeight === 0) {
|
|
112
|
+
top = 0;
|
|
113
|
+
bottom = 0;
|
|
114
|
+
}
|
|
115
|
+
thumb.style.setProperty("--voxx-toc-thumb-top", `${top}px`);
|
|
116
|
+
thumb.style.setProperty(
|
|
117
|
+
"--voxx-toc-thumb-height",
|
|
118
|
+
`${Math.max(bottom - top, 0)}px`,
|
|
119
|
+
);
|
|
120
|
+
}, [active, svg]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<nav className="voxx-toc" aria-label="On this page">
|
|
124
|
+
<p className="voxx-toc__title">
|
|
125
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
126
|
+
<path
|
|
127
|
+
d="M2.5 4h7M2.5 8h11M2.5 12h7"
|
|
128
|
+
stroke="currentColor"
|
|
129
|
+
strokeWidth="1.5"
|
|
130
|
+
strokeLinecap="round"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
On this page
|
|
134
|
+
</p>
|
|
135
|
+
<div className="voxx-toc__body">
|
|
136
|
+
{svg ? (
|
|
137
|
+
<div
|
|
138
|
+
className="voxx-toc__mask"
|
|
139
|
+
style={{
|
|
140
|
+
width: svg.width,
|
|
141
|
+
height: svg.height,
|
|
142
|
+
maskImage: `url("data:image/svg+xml,${encodeURIComponent(
|
|
143
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`,
|
|
144
|
+
)}")`,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<div
|
|
148
|
+
ref={thumbRef}
|
|
149
|
+
className="voxx-toc__thumb"
|
|
150
|
+
data-hidden={active.length === 0 || undefined}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
<div ref={containerRef} className="voxx-toc__items">
|
|
155
|
+
{toc.map((item, i) => (
|
|
156
|
+
<TocLink
|
|
157
|
+
key={item.id}
|
|
158
|
+
item={item}
|
|
159
|
+
upper={toc[i - 1]?.depth ?? item.depth}
|
|
160
|
+
lower={toc[i + 1]?.depth ?? item.depth}
|
|
161
|
+
active={active.includes(item.id)}
|
|
162
|
+
/>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</nav>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function TocLink({
|
|
171
|
+
item,
|
|
172
|
+
upper,
|
|
173
|
+
lower,
|
|
174
|
+
active,
|
|
175
|
+
}: {
|
|
176
|
+
item: TocItem;
|
|
177
|
+
upper: number;
|
|
178
|
+
lower: number;
|
|
179
|
+
active: boolean;
|
|
180
|
+
}) {
|
|
181
|
+
const offset = getLineOffset(item.depth);
|
|
182
|
+
const upperOffset = getLineOffset(upper);
|
|
183
|
+
const lowerOffset = getLineOffset(lower);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<a
|
|
187
|
+
href={`#${item.id}`}
|
|
188
|
+
className="voxx-toc__link"
|
|
189
|
+
data-active={active || undefined}
|
|
190
|
+
style={{ paddingInlineStart: getItemOffset(item.depth) }}
|
|
191
|
+
>
|
|
192
|
+
{offset !== upperOffset ? (
|
|
193
|
+
<svg
|
|
194
|
+
className="voxx-toc__connector"
|
|
195
|
+
viewBox="0 0 16 16"
|
|
196
|
+
aria-hidden="true"
|
|
197
|
+
>
|
|
198
|
+
<line x1={upperOffset} y1="0" x2={offset} y2="12" />
|
|
199
|
+
</svg>
|
|
200
|
+
) : null}
|
|
201
|
+
<span
|
|
202
|
+
className="voxx-toc__line"
|
|
203
|
+
aria-hidden="true"
|
|
204
|
+
style={{
|
|
205
|
+
insetInlineStart: offset,
|
|
206
|
+
top: offset !== upperOffset ? "0.375rem" : 0,
|
|
207
|
+
bottom: offset !== lowerOffset ? "0.375rem" : 0,
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
{item.text}
|
|
211
|
+
</a>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { absoluteUrl } from "@prudentbird/voxx-core";
|
|
3
|
+
import { getConfig } from "{{DATA_IMPORT}}";
|
|
4
|
+
|
|
5
|
+
export default async function robots(): Promise<MetadataRoute.Robots> {
|
|
6
|
+
const config = await getConfig();
|
|
7
|
+
return {
|
|
8
|
+
rules: { userAgent: "*", allow: "/" },
|
|
9
|
+
...(config.features.sitemap
|
|
10
|
+
? { sitemap: absoluteUrl(config.site.url, "/sitemap.xml") }
|
|
11
|
+
: {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { renderRss } from "@prudentbird/voxx-core";
|
|
2
|
+
import { getConfig, getPosts } from "{{DATA_IMPORT}}";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const [posts, config] = await Promise.all([getPosts(), getConfig()]);
|
|
6
|
+
return new Response(renderRss(posts, config), {
|
|
7
|
+
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { absoluteUrl } from "@prudentbird/voxx-core";
|
|
3
|
+
import { getConfig, getPosts } from "{{DATA_IMPORT}}";
|
|
4
|
+
|
|
5
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
6
|
+
const [posts, config] = await Promise.all([getPosts(), getConfig()]);
|
|
7
|
+
|
|
8
|
+
const index = {
|
|
9
|
+
url: absoluteUrl(config.site.url, config.content.basePath),
|
|
10
|
+
lastModified: posts[0]?.updated ?? posts[0]?.date,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
{ url: config.site.url, lastModified: index.lastModified },
|
|
15
|
+
index,
|
|
16
|
+
...posts
|
|
17
|
+
.filter((post) => post.path.length > 0)
|
|
18
|
+
.map((post) => ({
|
|
19
|
+
url: absoluteUrl(config.site.url, post.url),
|
|
20
|
+
lastModified: post.updated ?? post.date,
|
|
21
|
+
})),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = "voxx-theme";
|
|
6
|
+
|
|
7
|
+
export function ThemeToggle() {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
10
|
+
if (stored === "dark" || stored === "light") {
|
|
11
|
+
document.documentElement.classList.add(stored);
|
|
12
|
+
}
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const toggle = () => {
|
|
16
|
+
const root = document.documentElement;
|
|
17
|
+
const isDark =
|
|
18
|
+
root.classList.contains("dark") ||
|
|
19
|
+
(!root.classList.contains("light") &&
|
|
20
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
21
|
+
const next = isDark ? "light" : "dark";
|
|
22
|
+
root.classList.remove("dark", "light");
|
|
23
|
+
root.classList.add(next);
|
|
24
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
className="voxx-icon-button voxx-theme-toggle"
|
|
31
|
+
aria-label="Toggle theme"
|
|
32
|
+
onClick={toggle}
|
|
33
|
+
>
|
|
34
|
+
<svg
|
|
35
|
+
className="voxx-icon-moon"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
>
|
|
40
|
+
<path
|
|
41
|
+
d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8Z"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
strokeWidth="2"
|
|
44
|
+
strokeLinecap="round"
|
|
45
|
+
strokeLinejoin="round"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
<svg
|
|
49
|
+
className="voxx-icon-sun"
|
|
50
|
+
viewBox="0 0 24 24"
|
|
51
|
+
fill="none"
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" />
|
|
55
|
+
<path
|
|
56
|
+
d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
strokeWidth="2"
|
|
59
|
+
strokeLinecap="round"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@prudentbird/voxx-core/voxx.schema.json",
|
|
3
|
+
"site": {
|
|
4
|
+
"title": "{{SITE_TITLE}}",
|
|
5
|
+
"description": "{{SITE_DESCRIPTION}}",
|
|
6
|
+
"url": "{{SITE_URL}}",
|
|
7
|
+
"locale": "en-US"
|
|
8
|
+
},
|
|
9
|
+
"content": {
|
|
10
|
+
"type": "{{TYPE}}",
|
|
11
|
+
"dir": "{{CONTENT_DIR}}",
|
|
12
|
+
"basePath": "{{BASE_PATH}}",
|
|
13
|
+
"drafts": false
|
|
14
|
+
},
|
|
15
|
+
"theme": {
|
|
16
|
+
"preset": "shadcn",
|
|
17
|
+
"css": null,
|
|
18
|
+
"codeTheme": "github-light github-dark"
|
|
19
|
+
},
|
|
20
|
+
"seo": {
|
|
21
|
+
"openGraph": true,
|
|
22
|
+
"twitter": null,
|
|
23
|
+
"jsonLd": true,
|
|
24
|
+
"defaultImage": null
|
|
25
|
+
}
|
|
26
|
+
}
|