@wrikka/create-docs 0.1.0 → 0.2.1
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/docs/config.md +1 -1
- package/docs/data-sources.md +1 -1
- package/docs/deploy.md +1 -1
- package/docs/features.md +1 -1
- package/docs/markdown.md +1 -1
- package/docs/theming.md +1 -1
- package/docs/translate.md +46 -0
- package/package.json +7 -5
- package/scripts/generate-docs-index.ts +3 -3
- package/scripts/translate.ts +127 -0
- package/src/adapters/github/github-pull.ts +23 -0
- package/src/runtime/components/ApiReference.tsx +332 -0
- package/src/runtime/components/ContextMenu.tsx +88 -0
- package/src/runtime/components/MobileBottomNav.tsx +3 -3
- package/src/runtime/components/SidebarNav.tsx +190 -3
- package/src/runtime/components/TopNav.tsx +163 -49
- package/src/runtime/config.ts +24 -1
- package/src/runtime/content.ts +17 -15
- package/src/runtime/index.ts +6 -0
- package/src/runtime/pages/ApiEndpointPage.tsx +54 -141
- package/src/runtime/pages/DocPage.tsx +10 -1
- package/src/runtime/pages/PluginsPage.tsx +38 -13
- package/src/runtime/pages/SearchPage.tsx +374 -0
- package/src/runtime/pages/ShowcasePage.tsx +143 -15
- package/src/runtime/pages/TranslatePage.tsx +338 -0
- package/src/runtime/plugins-catalog.ts +170 -0
- package/src/runtime/router.tsx +23 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { For, onCleanup, onMount, Show } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface ContextMenuItem {
|
|
4
|
+
label: string;
|
|
5
|
+
icon: string;
|
|
6
|
+
action: () => void;
|
|
7
|
+
/** Render a divider above this item. */
|
|
8
|
+
divider?: boolean;
|
|
9
|
+
danger?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ContextMenu(props: {
|
|
13
|
+
open: boolean;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
items: ContextMenuItem[];
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
}) {
|
|
19
|
+
let rootEl: HTMLDivElement | undefined;
|
|
20
|
+
|
|
21
|
+
const onDocClick = (e: MouseEvent) => {
|
|
22
|
+
if (rootEl && !rootEl.contains(e.target as Node)) props.onClose();
|
|
23
|
+
};
|
|
24
|
+
const onKey = (e: KeyboardEvent) => {
|
|
25
|
+
if (e.key === "Escape") props.onClose();
|
|
26
|
+
};
|
|
27
|
+
const onScroll = () => props.onClose();
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
document.addEventListener("click", onDocClick, true);
|
|
31
|
+
document.addEventListener("keydown", onKey);
|
|
32
|
+
document.addEventListener("scroll", onScroll, true);
|
|
33
|
+
});
|
|
34
|
+
onCleanup(() => {
|
|
35
|
+
document.removeEventListener("click", onDocClick, true);
|
|
36
|
+
document.removeEventListener("keydown", onKey);
|
|
37
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const pos = () => {
|
|
41
|
+
const w = 200;
|
|
42
|
+
const h = props.items.length * 36 + 12;
|
|
43
|
+
const x = Math.min(props.x, window.innerWidth - w - 8);
|
|
44
|
+
const y =
|
|
45
|
+
props.y + h > window.innerHeight
|
|
46
|
+
? Math.max(8, window.innerHeight - h - 8)
|
|
47
|
+
: props.y;
|
|
48
|
+
return { x: Math.max(8, x), y };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Show when={props.open}>
|
|
53
|
+
<div
|
|
54
|
+
ref={rootEl}
|
|
55
|
+
class="fixed z-50 min-w-48 rounded-lg border border-border bg-surface shadow-xl py-1"
|
|
56
|
+
style={{ left: `${pos().x}px`, top: `${pos().y}px` }}
|
|
57
|
+
role="menu"
|
|
58
|
+
>
|
|
59
|
+
<For each={props.items}>
|
|
60
|
+
{(item) => (
|
|
61
|
+
<>
|
|
62
|
+
<Show when={item.divider}>
|
|
63
|
+
<div class="my-1 border-t border-border" aria-hidden="true" />
|
|
64
|
+
</Show>
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
role="menuitem"
|
|
68
|
+
class={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors cursor-pointer border-none bg-transparent hover:bg-background ${
|
|
69
|
+
item.danger ? "text-destructive" : "text-foreground"
|
|
70
|
+
}`}
|
|
71
|
+
onClick={() => {
|
|
72
|
+
item.action();
|
|
73
|
+
props.onClose();
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<span
|
|
77
|
+
class={`${item.icon} shrink-0 ${item.danger ? "" : "text-muted"}`}
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
/>
|
|
80
|
+
{item.label}
|
|
81
|
+
</button>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
</For>
|
|
85
|
+
</div>
|
|
86
|
+
</Show>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Link, useParams } from "@tanstack/solid-router";
|
|
1
|
+
import { Link, useNavigate, useParams } from "@tanstack/solid-router";
|
|
2
2
|
import { For } from "solid-js";
|
|
3
3
|
import { createDocsList, useCollections } from "../data";
|
|
4
|
-
import { setSearchOpen } from "./SearchPalette";
|
|
5
4
|
|
|
6
5
|
export function MobileBottomNav(props: { onMenuToggle: () => void }) {
|
|
7
6
|
const collections = useCollections();
|
|
8
7
|
const params = useParams({ strict: false });
|
|
8
|
+
const navigate = useNavigate();
|
|
9
9
|
|
|
10
10
|
const [docsList] = createDocsList(() => "docs");
|
|
11
11
|
const [apiList] = createDocsList(() => "api");
|
|
@@ -91,7 +91,7 @@ export function MobileBottomNav(props: { onMenuToggle: () => void }) {
|
|
|
91
91
|
type="button"
|
|
92
92
|
aria-label="Search"
|
|
93
93
|
class="flex flex-col items-center justify-center h-full gap-0.5 text-xs text-muted hover:text-foreground transition-colors"
|
|
94
|
-
onClick={() =>
|
|
94
|
+
onClick={() => navigate({ to: "/search" })}
|
|
95
95
|
>
|
|
96
96
|
<span class="i-mdi:magnify text-xl" aria-hidden="true" />
|
|
97
97
|
<span class="scale-90">Search</span>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Link, useParams } from "@tanstack/solid-router";
|
|
2
2
|
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
|
|
3
|
+
import { useDocs } from "../context";
|
|
3
4
|
import { createDocsList, useCollections } from "../data";
|
|
4
5
|
import { categoryIcon, typeIcon } from "../icons";
|
|
5
6
|
import type { DocEntry } from "../types";
|
|
7
|
+
import { ContextMenu, type ContextMenuItem } from "./ContextMenu";
|
|
6
8
|
|
|
7
9
|
function sortDocs(items: DocEntry[]): DocEntry[] {
|
|
8
10
|
return [...items].sort(
|
|
@@ -21,10 +23,79 @@ function SidebarDocItem(props: {
|
|
|
21
23
|
const isActive = () => props.activeId === props.doc.id;
|
|
22
24
|
const hasChildren = () => (props.doc.children?.length ?? 0) > 0;
|
|
23
25
|
const [open, setOpen] = createSignal(true);
|
|
26
|
+
const [menu, setMenu] = createSignal<{ x: number; y: number } | null>(null);
|
|
27
|
+
const config = useDocs();
|
|
28
|
+
const collections = useCollections();
|
|
29
|
+
|
|
30
|
+
const href = () => `/${props.collection}/${props.doc.id}`;
|
|
31
|
+
const absoluteUrl = () => `${location.origin}${href()}`;
|
|
32
|
+
const repoUrl = () =>
|
|
33
|
+
collections()?.find((c) => c.id === props.collection)?.repoUrl ??
|
|
34
|
+
config.site.repoUrl;
|
|
35
|
+
const editUrl = () =>
|
|
36
|
+
repoUrl()
|
|
37
|
+
? `${repoUrl()}/edit/${config.github?.branch ?? "main"}/${props.doc.path || `${props.collection}/${props.doc.id}.md`}`
|
|
38
|
+
: "";
|
|
39
|
+
|
|
40
|
+
const copy = (text: string) => {
|
|
41
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const menuItems = (): ContextMenuItem[] => [
|
|
45
|
+
{
|
|
46
|
+
label: "Open",
|
|
47
|
+
icon: "i-mdi:file-document-outline",
|
|
48
|
+
action: () => {
|
|
49
|
+
location.href = href();
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Open in new tab",
|
|
54
|
+
icon: "i-mdi:open-in-new",
|
|
55
|
+
action: () => window.open(href(), "_blank", "noopener"),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "Copy link",
|
|
59
|
+
icon: "i-mdi:link-variant",
|
|
60
|
+
action: () => copy(absoluteUrl()),
|
|
61
|
+
divider: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Copy path",
|
|
65
|
+
icon: "i-mdi:file-path",
|
|
66
|
+
action: () =>
|
|
67
|
+
copy(props.doc.path || `${props.collection}/${props.doc.id}.md`),
|
|
68
|
+
},
|
|
69
|
+
...(props.doc.description
|
|
70
|
+
? [
|
|
71
|
+
{
|
|
72
|
+
label: "Copy description",
|
|
73
|
+
icon: "i-mdi:text",
|
|
74
|
+
action: () => copy(props.doc.description ?? ""),
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
: []),
|
|
78
|
+
...(editUrl()
|
|
79
|
+
? [
|
|
80
|
+
{
|
|
81
|
+
label: "Edit on GitHub",
|
|
82
|
+
icon: "i-mdi:pencil-outline",
|
|
83
|
+
action: () => window.open(editUrl(), "_blank", "noopener"),
|
|
84
|
+
divider: true,
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
: []),
|
|
88
|
+
];
|
|
24
89
|
|
|
25
90
|
return (
|
|
26
91
|
<li>
|
|
27
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
class="group/item flex items-center gap-0.5"
|
|
94
|
+
onContextMenu={(e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setMenu({ x: e.clientX, y: e.clientY });
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
28
99
|
<Show when={hasChildren()}>
|
|
29
100
|
<button
|
|
30
101
|
type="button"
|
|
@@ -65,7 +136,27 @@ function SidebarDocItem(props: {
|
|
|
65
136
|
</span>
|
|
66
137
|
</Show>
|
|
67
138
|
</Link>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
aria-label={`More actions for ${props.doc.label}`}
|
|
142
|
+
title={props.doc.description || props.doc.label}
|
|
143
|
+
onClick={(e) => {
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
146
|
+
setMenu({ x: rect.right, y: rect.bottom + 4 });
|
|
147
|
+
}}
|
|
148
|
+
class="w-6 h-6 shrink-0 inline-flex items-center justify-center rounded text-muted opacity-0 group-hover/item:opacity-100 focus-visible:opacity-100 hover:text-foreground hover:bg-surface transition-opacity cursor-pointer border-none bg-transparent"
|
|
149
|
+
>
|
|
150
|
+
<span class="i-mdi:dots-vertical" aria-hidden="true" />
|
|
151
|
+
</button>
|
|
68
152
|
</div>
|
|
153
|
+
<ContextMenu
|
|
154
|
+
open={menu() !== null}
|
|
155
|
+
x={menu()?.x ?? 0}
|
|
156
|
+
y={menu()?.y ?? 0}
|
|
157
|
+
items={menuItems()}
|
|
158
|
+
onClose={() => setMenu(null)}
|
|
159
|
+
/>
|
|
69
160
|
<Show when={hasChildren() && open()}>
|
|
70
161
|
<ul class="list-none m-0 p-0 ml-2.5 border-l border-border/60">
|
|
71
162
|
<For each={sortDocs(props.doc.children ?? [])}>
|
|
@@ -85,8 +176,18 @@ function SidebarDocItem(props: {
|
|
|
85
176
|
);
|
|
86
177
|
}
|
|
87
178
|
|
|
179
|
+
function formatGroupLabel(category: string): string {
|
|
180
|
+
if (!category || category === "Docs") return category || "Docs";
|
|
181
|
+
return category
|
|
182
|
+
.split(/[-_\s/]+/)
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
185
|
+
.join(" ");
|
|
186
|
+
}
|
|
187
|
+
|
|
88
188
|
export function SidebarNav(props: { open: boolean; onNavigate: () => void }) {
|
|
89
189
|
const params = useParams({ strict: false });
|
|
190
|
+
const config = useDocs();
|
|
90
191
|
const [search, setSearch] = createSignal("");
|
|
91
192
|
const [docs] = createDocsList(() => params().collection);
|
|
92
193
|
const collections = useCollections();
|
|
@@ -108,7 +209,66 @@ export function SidebarNav(props: { open: boolean; onNavigate: () => void }) {
|
|
|
108
209
|
});
|
|
109
210
|
|
|
110
211
|
const sectionMeta = (category: string) =>
|
|
111
|
-
colMeta()?.sections?.find(
|
|
212
|
+
colMeta()?.sections?.find(
|
|
213
|
+
(s) =>
|
|
214
|
+
s.id === category ||
|
|
215
|
+
s.label === category ||
|
|
216
|
+
s.label === formatGroupLabel(category),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const groupLabel = (category: string) =>
|
|
220
|
+
sectionMeta(category)?.label ?? formatGroupLabel(category);
|
|
221
|
+
|
|
222
|
+
const resourceLinks = () => {
|
|
223
|
+
const links: { label: string; to: string; icon: string }[] = [
|
|
224
|
+
{ label: "Search", to: "/search", icon: "i-mdi:magnify" },
|
|
225
|
+
];
|
|
226
|
+
if (config.showcase?.length)
|
|
227
|
+
links.push({
|
|
228
|
+
label: "Showcase",
|
|
229
|
+
to: "/showcase",
|
|
230
|
+
icon: "i-mdi:view-dashboard",
|
|
231
|
+
});
|
|
232
|
+
if (config.plugins?.length)
|
|
233
|
+
links.push({ label: "Plugins", to: "/plugins", icon: "i-mdi:puzzle" });
|
|
234
|
+
if (config.features?.translate || config.translate)
|
|
235
|
+
links.push({
|
|
236
|
+
label: "Translate",
|
|
237
|
+
to: "/translate",
|
|
238
|
+
icon: "i-mdi:translate",
|
|
239
|
+
});
|
|
240
|
+
if (config.features?.analytics)
|
|
241
|
+
links.push({
|
|
242
|
+
label: "Analytics",
|
|
243
|
+
to: "/analytics",
|
|
244
|
+
icon: "i-mdi:chart-box-outline",
|
|
245
|
+
});
|
|
246
|
+
if (config.github?.releases)
|
|
247
|
+
links.push({
|
|
248
|
+
label: "Changelog",
|
|
249
|
+
to: "/changelog",
|
|
250
|
+
icon: "i-mdi:history",
|
|
251
|
+
});
|
|
252
|
+
if (config.github?.contributors)
|
|
253
|
+
links.push({
|
|
254
|
+
label: "Community",
|
|
255
|
+
to: "/community",
|
|
256
|
+
icon: "i-mdi:account-group",
|
|
257
|
+
});
|
|
258
|
+
if (config.github?.issues)
|
|
259
|
+
links.push({
|
|
260
|
+
label: "Issues",
|
|
261
|
+
to: "/issues",
|
|
262
|
+
icon: "i-mdi:alert-circle-outline",
|
|
263
|
+
});
|
|
264
|
+
if (config.apiDiff)
|
|
265
|
+
links.push({
|
|
266
|
+
label: "API diff",
|
|
267
|
+
to: "/api-diff",
|
|
268
|
+
icon: "i-mdi:file-compare",
|
|
269
|
+
});
|
|
270
|
+
return links;
|
|
271
|
+
};
|
|
112
272
|
|
|
113
273
|
const isCollapsed = (category: string) => {
|
|
114
274
|
const stored = collapsed()[category];
|
|
@@ -206,7 +366,7 @@ export function SidebarNav(props: { open: boolean; onNavigate: () => void }) {
|
|
|
206
366
|
class={meta()?.icon ?? categoryIcon(category)}
|
|
207
367
|
aria-hidden="true"
|
|
208
368
|
/>
|
|
209
|
-
{meta()?.label ?? category}
|
|
369
|
+
{meta()?.label ?? groupLabel(category)}
|
|
210
370
|
<span class="ml-auto font-normal">{items.length}</span>
|
|
211
371
|
<span
|
|
212
372
|
class={`i-mdi:chevron-down text-xs transition-transform ${collapsedNow() ? "-rotate-90" : ""}`}
|
|
@@ -231,6 +391,33 @@ export function SidebarNav(props: { open: boolean; onNavigate: () => void }) {
|
|
|
231
391
|
);
|
|
232
392
|
}}
|
|
233
393
|
</For>
|
|
394
|
+
<Show when={resourceLinks().length > 0}>
|
|
395
|
+
<div class="mt-6 pt-3 border-t border-border">
|
|
396
|
+
<div class="flex items-center gap-2 px-2 pb-1 text-[11px] uppercase tracking-wider font-semibold text-muted">
|
|
397
|
+
<span class="i-mdi:apps" aria-hidden="true" />
|
|
398
|
+
Resources
|
|
399
|
+
</div>
|
|
400
|
+
<ul class="list-none m-0 p-0">
|
|
401
|
+
<For each={resourceLinks()}>
|
|
402
|
+
{(link) => (
|
|
403
|
+
<li>
|
|
404
|
+
<Link
|
|
405
|
+
to={link.to}
|
|
406
|
+
onClick={props.onNavigate}
|
|
407
|
+
class="flex items-center gap-2 px-2 py-1.5 rounded-md text-sm no-underline text-muted hover:text-foreground hover:bg-surface transition-colors"
|
|
408
|
+
>
|
|
409
|
+
<span
|
|
410
|
+
class={`${link.icon} shrink-0 opacity-70`}
|
|
411
|
+
aria-hidden="true"
|
|
412
|
+
/>
|
|
413
|
+
{link.label}
|
|
414
|
+
</Link>
|
|
415
|
+
</li>
|
|
416
|
+
)}
|
|
417
|
+
</For>
|
|
418
|
+
</ul>
|
|
419
|
+
</div>
|
|
420
|
+
</Show>
|
|
234
421
|
</nav>
|
|
235
422
|
</aside>
|
|
236
423
|
);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { Link, useParams } from "@tanstack/solid-router";
|
|
1
|
+
import { Link, useNavigate, useParams } from "@tanstack/solid-router";
|
|
2
2
|
import { createSignal, For, onMount, Show } from "solid-js";
|
|
3
3
|
import type { SiteLink } from "../config";
|
|
4
4
|
import { useDocs } from "../context";
|
|
5
|
+
import { searchDocs } from "../data";
|
|
5
6
|
import { setTheme, useTheme } from "../theme";
|
|
7
|
+
import type { SearchResult } from "../types";
|
|
6
8
|
import { AccentPicker } from "./AccentPicker";
|
|
7
9
|
import { CollectionDropdown } from "./CollectionDropdown";
|
|
10
|
+
import { ContextMenu } from "./ContextMenu";
|
|
8
11
|
import { CustomizeDrawer } from "./CustomizeDrawer";
|
|
9
12
|
import { DocsDropdown } from "./DocsDropdown";
|
|
10
13
|
import { LocaleDropdown } from "./LocaleDropdown";
|
|
@@ -56,30 +59,13 @@ function LogoContextMenu(props: {
|
|
|
56
59
|
];
|
|
57
60
|
|
|
58
61
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{(item) => (
|
|
67
|
-
<button
|
|
68
|
-
type="button"
|
|
69
|
-
role="menuitem"
|
|
70
|
-
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left text-foreground hover:bg-background transition-colors"
|
|
71
|
-
onClick={() => {
|
|
72
|
-
item.action();
|
|
73
|
-
props.onClose();
|
|
74
|
-
}}
|
|
75
|
-
>
|
|
76
|
-
<span class={item.icon} aria-hidden="true" />
|
|
77
|
-
{item.label}
|
|
78
|
-
</button>
|
|
79
|
-
)}
|
|
80
|
-
</For>
|
|
81
|
-
</div>
|
|
82
|
-
</Show>
|
|
62
|
+
<ContextMenu
|
|
63
|
+
open={props.open}
|
|
64
|
+
x={props.x}
|
|
65
|
+
y={props.y}
|
|
66
|
+
items={items()}
|
|
67
|
+
onClose={props.onClose}
|
|
68
|
+
/>
|
|
83
69
|
);
|
|
84
70
|
}
|
|
85
71
|
|
|
@@ -94,6 +80,44 @@ export function TopNav(props: {
|
|
|
94
80
|
);
|
|
95
81
|
const [customizeOpen, setCustomizeOpen] = createSignal(false);
|
|
96
82
|
const [customNav, setCustomNav] = createSignal<SiteLink[] | null>(null);
|
|
83
|
+
const [navSearch, setNavSearch] = createSignal(false);
|
|
84
|
+
const [navQuery, setNavQuery] = createSignal("");
|
|
85
|
+
const [navResults, setNavResults] = createSignal<SearchResult[]>([]);
|
|
86
|
+
const navigate = useNavigate();
|
|
87
|
+
let navSearchEl: HTMLInputElement | undefined;
|
|
88
|
+
|
|
89
|
+
const openNavSearch = () => {
|
|
90
|
+
setNavSearch(true);
|
|
91
|
+
setNavQuery("");
|
|
92
|
+
setNavResults([]);
|
|
93
|
+
queueMicrotask(() => navSearchEl?.focus());
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const closeNavSearch = () => {
|
|
97
|
+
setNavSearch(false);
|
|
98
|
+
setNavQuery("");
|
|
99
|
+
setNavResults([]);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onNavSearchInput = async (value: string) => {
|
|
103
|
+
setNavQuery(value);
|
|
104
|
+
const term = value.trim();
|
|
105
|
+
if (term.length < 2) {
|
|
106
|
+
setNavResults([]);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
setNavResults((await searchDocs(config, term)).slice(0, 8));
|
|
111
|
+
} catch {
|
|
112
|
+
setNavResults([]);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const goSearchPage = () => {
|
|
117
|
+
const q = navQuery().trim();
|
|
118
|
+
closeNavSearch();
|
|
119
|
+
navigate({ to: "/search", search: q ? { q } : {} });
|
|
120
|
+
};
|
|
97
121
|
|
|
98
122
|
onMount(() => {
|
|
99
123
|
try {
|
|
@@ -126,6 +150,9 @@ export function TopNav(props: {
|
|
|
126
150
|
...(config.showcase?.length
|
|
127
151
|
? [{ label: "Showcase", to: "/showcase", icon: "i-mdi:view-dashboard" }]
|
|
128
152
|
: []),
|
|
153
|
+
...(config.features?.translate || config.translate
|
|
154
|
+
? [{ label: "Translate", to: "/translate", icon: "i-mdi:translate" }]
|
|
155
|
+
: []),
|
|
129
156
|
];
|
|
130
157
|
|
|
131
158
|
const defaultAllNav = () => [...topNav(), ...extraNav()];
|
|
@@ -169,38 +196,125 @@ export function TopNav(props: {
|
|
|
169
196
|
/>
|
|
170
197
|
<CollectionDropdown current={params().collection} />
|
|
171
198
|
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
199
|
+
<Show
|
|
200
|
+
when={navSearch()}
|
|
201
|
+
fallback={
|
|
202
|
+
<nav
|
|
203
|
+
class="hidden md:flex flex-1 items-center justify-center gap-1"
|
|
204
|
+
aria-label="Site"
|
|
205
|
+
>
|
|
206
|
+
<DocsDropdown />
|
|
207
|
+
<For each={allNav()}>
|
|
208
|
+
{(link) => (
|
|
209
|
+
<Link
|
|
210
|
+
to={link.to}
|
|
211
|
+
class="px-3 h-9 inline-flex items-center gap-1.5 rounded-md text-sm text-muted no-underline hover:text-foreground hover:bg-surface transition-colors"
|
|
212
|
+
>
|
|
213
|
+
<Show when={link.icon}>
|
|
214
|
+
<span class={link.icon} aria-hidden="true" />
|
|
215
|
+
</Show>
|
|
216
|
+
{link.label}
|
|
217
|
+
</Link>
|
|
218
|
+
)}
|
|
219
|
+
</For>
|
|
179
220
|
<Link
|
|
180
|
-
to=
|
|
181
|
-
class="px-3 h-9 inline-flex items-center gap-1.5 rounded-md text-sm
|
|
221
|
+
to="/create"
|
|
222
|
+
class="ml-2 px-3 h-9 inline-flex items-center gap-1.5 rounded-md text-sm font-medium bg-primary text-primary-foreground no-underline hover:bg-primary-hover transition-colors"
|
|
182
223
|
>
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
</Show>
|
|
186
|
-
{link.label}
|
|
224
|
+
<span class="i-mdi:plus" aria-hidden="true" />
|
|
225
|
+
Create docs
|
|
187
226
|
</Link>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class="
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
</nav>
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
<div class="hidden md:flex flex-1 items-center justify-center relative">
|
|
231
|
+
<div class="w-full max-w-xl flex items-center gap-2 px-3 h-10 rounded-lg border border-focus bg-surface shadow-sm">
|
|
232
|
+
<span
|
|
233
|
+
class="i-mdi:magnify text-muted shrink-0"
|
|
234
|
+
aria-hidden="true"
|
|
235
|
+
/>
|
|
236
|
+
<input
|
|
237
|
+
ref={navSearchEl}
|
|
238
|
+
type="search"
|
|
239
|
+
value={navQuery()}
|
|
240
|
+
onInput={(e) => onNavSearchInput(e.currentTarget.value)}
|
|
241
|
+
onKeyDown={(e) => {
|
|
242
|
+
if (e.key === "Escape") closeNavSearch();
|
|
243
|
+
if (e.key === "Enter") {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
const first = navResults()[0];
|
|
246
|
+
if (first && navQuery().trim()) {
|
|
247
|
+
closeNavSearch();
|
|
248
|
+
navigate({
|
|
249
|
+
to: "/$collection/$docId",
|
|
250
|
+
params: {
|
|
251
|
+
collection: first.collection,
|
|
252
|
+
docId: first.id,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
goSearchPage();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}}
|
|
260
|
+
placeholder="Search docs… (Enter for full search)"
|
|
261
|
+
aria-label="Search documentation"
|
|
262
|
+
class="flex-1 bg-transparent outline-none border-none text-sm text-foreground placeholder:text-muted"
|
|
263
|
+
/>
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
onClick={closeNavSearch}
|
|
267
|
+
aria-label="Close search"
|
|
268
|
+
class="w-6 h-6 inline-flex items-center justify-center rounded text-muted hover:text-foreground cursor-pointer border-none bg-transparent"
|
|
269
|
+
>
|
|
270
|
+
<span class="i-mdi:close" aria-hidden="true" />
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
<Show when={navResults().length > 0}>
|
|
274
|
+
<div class="absolute top-full mt-2 w-full max-w-xl rounded-lg border border-border bg-surface shadow-xl py-1 z-50 max-h-80 overflow-y-auto">
|
|
275
|
+
<For each={navResults()}>
|
|
276
|
+
{(r) => (
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
class="w-full text-left px-3 py-2 hover:bg-background transition-colors cursor-pointer border-none bg-transparent"
|
|
280
|
+
onClick={() => {
|
|
281
|
+
closeNavSearch();
|
|
282
|
+
navigate({
|
|
283
|
+
to: "/$collection/$docId",
|
|
284
|
+
params: { collection: r.collection, docId: r.id },
|
|
285
|
+
});
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<div class="text-sm font-medium text-foreground truncate">
|
|
289
|
+
{r.title}
|
|
290
|
+
</div>
|
|
291
|
+
<div class="text-xs text-muted truncate">{r.snippet}</div>
|
|
292
|
+
</button>
|
|
293
|
+
)}
|
|
294
|
+
</For>
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
onClick={goSearchPage}
|
|
298
|
+
class="w-full text-left px-3 py-2 text-xs text-primary hover:bg-background transition-colors cursor-pointer border-t border-border border-none bg-transparent"
|
|
299
|
+
>
|
|
300
|
+
View all results →
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
</Show>
|
|
304
|
+
</div>
|
|
305
|
+
</Show>
|
|
198
306
|
|
|
199
307
|
<div class="flex items-center gap-1 shrink-0">
|
|
200
308
|
<Show when={config.features?.search !== false}>
|
|
201
309
|
<button
|
|
202
310
|
type="button"
|
|
203
|
-
onClick={() =>
|
|
311
|
+
onClick={() => {
|
|
312
|
+
if (config.features?.searchPage !== false) {
|
|
313
|
+
openNavSearch();
|
|
314
|
+
} else {
|
|
315
|
+
setSearchOpen(true);
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
204
318
|
aria-label="Search documentation"
|
|
205
319
|
class="inline-flex items-center gap-2 px-3 h-9 rounded-md border border-border bg-surface text-sm text-muted hover:text-foreground hover:border-focus transition-colors cursor-pointer"
|
|
206
320
|
>
|
package/src/runtime/config.ts
CHANGED
|
@@ -49,6 +49,10 @@ export interface DocsAppFeatures {
|
|
|
49
49
|
frontmatterToggle?: boolean;
|
|
50
50
|
/** Show an "MCP available" badge linking to the JSON-RPC endpoint. */
|
|
51
51
|
mcp?: boolean;
|
|
52
|
+
/** Enable the /search interactive search page and nav search box. */
|
|
53
|
+
searchPage?: boolean;
|
|
54
|
+
/** Enable the /translate page and per-doc translate actions (AI via CI). */
|
|
55
|
+
translate?: boolean;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
export interface MarkdownConfig {
|
|
@@ -101,6 +105,20 @@ export interface PluginInfo {
|
|
|
101
105
|
version?: string;
|
|
102
106
|
/** Install command shown on the card, e.g. "bun add @wrikka/x". Omit for built-in integrations. */
|
|
103
107
|
install?: string;
|
|
108
|
+
/** Marketplace grouping, e.g. "Adapters", "AI", "Integrations". */
|
|
109
|
+
category?: string;
|
|
110
|
+
/** Availability badge: "built-in", "beta", "planned" or a version string. */
|
|
111
|
+
status?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** AI translation configuration shown on the /translate page. */
|
|
115
|
+
export interface TranslateConfig {
|
|
116
|
+
/** Locales that AI translation targets (in addition to i18n.list). */
|
|
117
|
+
locales?: LocaleInfo[];
|
|
118
|
+
/** Translation provider hint shown to users, e.g. "openai", "devin". */
|
|
119
|
+
provider?: string;
|
|
120
|
+
/** CI workflow filename used to run translation, e.g. "translate.yml". */
|
|
121
|
+
workflow?: string;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
export interface ShowcaseInfo {
|
|
@@ -114,8 +132,11 @@ export interface ShowcaseInfo {
|
|
|
114
132
|
image?: string;
|
|
115
133
|
/** Optional Iconify icon class used as fallback. */
|
|
116
134
|
icon?: string;
|
|
117
|
-
/** Optional link to the project/case. */
|
|
135
|
+
/** Optional link to the project/case. Internal paths ("/docs/x") route in-app. */
|
|
118
136
|
link?: string;
|
|
137
|
+
/** Internal doc target — renders a router link to /collection/docId. */
|
|
138
|
+
collection?: string;
|
|
139
|
+
docId?: string;
|
|
119
140
|
/** Optional tags. */
|
|
120
141
|
tags?: string[];
|
|
121
142
|
/** Optional badge. */
|
|
@@ -209,6 +230,8 @@ export interface DocsAppConfig {
|
|
|
209
230
|
versions?: VersionsConfig;
|
|
210
231
|
/** Multi-language docs: shows a language switcher and sets `<html lang>`. */
|
|
211
232
|
i18n?: I18nConfig;
|
|
233
|
+
/** AI translation UX — /translate page, per-doc translate menu, CI hints. */
|
|
234
|
+
translate?: TranslateConfig;
|
|
212
235
|
/** Optional analytics sink: page views are beaconed to this endpoint. */
|
|
213
236
|
analytics?: { endpoint?: string };
|
|
214
237
|
/** Plugin marketplace: entries listed on the `/plugins` page. */
|