@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.
@@ -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={() => setSearchOpen(true)}
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 class="flex items-center gap-0.5">
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((s) => s.id === category || s.label === category);
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
- <Show when={props.open}>
60
- <div
61
- class="fixed z-50 min-w-44 rounded-md border border-border bg-surface shadow-lg py-1"
62
- style={{ left: `${props.x}px`, top: `${props.y}px` }}
63
- role="menu"
64
- >
65
- <For each={items()}>
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
- <nav
173
- class="hidden md:flex flex-1 items-center justify-center gap-1"
174
- aria-label="Site"
175
- >
176
- <DocsDropdown />
177
- <For each={allNav()}>
178
- {(link) => (
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={link.to}
181
- 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"
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
- <Show when={link.icon}>
184
- <span class={link.icon} aria-hidden="true" />
185
- </Show>
186
- {link.label}
224
+ <span class="i-mdi:plus" aria-hidden="true" />
225
+ Create docs
187
226
  </Link>
188
- )}
189
- </For>
190
- <Link
191
- to="/create"
192
- 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"
193
- >
194
- <span class="i-mdi:plus" aria-hidden="true" />
195
- Create docs
196
- </Link>
197
- </nav>
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={() => setSearchOpen(true)}
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
  >
@@ -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. */