@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,338 @@
1
+ import { Link } from "@tanstack/solid-router";
2
+ import { createMemo, createResource, createSignal, For, Show } from "solid-js";
3
+ import type { LocaleInfo } from "../config";
4
+ import { useDocs } from "../context";
5
+ import { useCollections } from "../data";
6
+ import type { DocEntry } from "../types";
7
+
8
+ interface DocRow {
9
+ collection: string;
10
+ collectionLabel: string;
11
+ entry: DocEntry;
12
+ }
13
+
14
+ export function TranslatePage() {
15
+ const config = useDocs();
16
+ const collections = useCollections();
17
+ const [copied, setCopied] = createSignal<string | null>(null);
18
+ const [collectionFilter, setCollectionFilter] = createSignal<string | null>(
19
+ null,
20
+ );
21
+
22
+ const locales = createMemo((): LocaleInfo[] => {
23
+ const fromI18n = config.i18n?.list ?? [];
24
+ const extra = config.translate?.locales ?? [];
25
+ const seen = new Set(fromI18n.map((l) => l.id));
26
+ return [...fromI18n, ...extra.filter((l) => !seen.has(l.id))];
27
+ });
28
+ const currentLocale = () => config.i18n?.current ?? "en";
29
+ const targets = () => locales().filter((l) => l.id !== currentLocale());
30
+ const workflow = () => config.translate?.workflow ?? "translate.yml";
31
+ const provider = () => config.translate?.provider ?? "ai";
32
+
33
+ const [docs] = createResource(
34
+ () => collections()?.map((c) => c.id) ?? [],
35
+ async (ids) => {
36
+ const rows: DocRow[] = [];
37
+ for (const id of ids) {
38
+ try {
39
+ const list = await config.dataSource.list(id);
40
+ const label = collections()?.find((c) => c.id === id)?.label ?? id;
41
+ for (const entry of list) {
42
+ rows.push({ collection: id, collectionLabel: label, entry });
43
+ }
44
+ } catch {
45
+ // skip collections that fail to list
46
+ }
47
+ }
48
+ return rows;
49
+ },
50
+ );
51
+
52
+ const rows = createMemo(() => {
53
+ const f = collectionFilter();
54
+ const all = docs() ?? [];
55
+ return f ? all.filter((r) => r.collection === f) : all;
56
+ });
57
+
58
+ /** A doc counts as translated for a locale when its id carries the locale marker. */
59
+ const isTranslated = (row: DocRow, locale: string) =>
60
+ row.entry.id.includes(`.${locale}`) ||
61
+ row.entry.id.includes(`--${locale}`) ||
62
+ (row.entry.tags ?? []).includes(`lang:${locale}`) ||
63
+ (row.entry.tags ?? []).includes(`locale:${locale}`);
64
+
65
+ const coverage = (locale: string) => {
66
+ const all = docs() ?? [];
67
+ if (!all.length) return 0;
68
+ const done = all.filter((r) => isTranslated(r, locale)).length;
69
+ return Math.round((done / all.length) * 100);
70
+ };
71
+
72
+ const ciCommand = () =>
73
+ `gh workflow run ${workflow()} -f provider=${provider()} -f locale=${targets()[0]?.id ?? "th"}`;
74
+
75
+ const cliCommand = () =>
76
+ `bunx @wrikka/create-docs translate --provider ${provider()} --locales ${targets()
77
+ .map((l) => l.id)
78
+ .join(",")}`;
79
+
80
+ const copy = (key: string, text: string) => {
81
+ navigator.clipboard.writeText(text).catch(() => {});
82
+ setCopied(key);
83
+ setTimeout(() => setCopied(null), 1500);
84
+ };
85
+
86
+ return (
87
+ <div class="max-w-5xl mx-auto px-6 py-10 pb-24">
88
+ <div class="flex items-start gap-3 mb-2">
89
+ <span
90
+ class="i-mdi:translate text-3xl text-primary"
91
+ aria-hidden="true"
92
+ />
93
+ <div>
94
+ <h1 class="text-3xl font-bold m-0">Translations</h1>
95
+ <p class="text-muted m-0">
96
+ AI-powered documentation translation, generated in CI.
97
+ </p>
98
+ </div>
99
+ <span class="ml-auto text-[10px] px-2 py-1 rounded-full border border-border bg-surface text-muted uppercase tracking-wide">
100
+ Preview
101
+ </span>
102
+ </div>
103
+
104
+ {/* Locale coverage cards */}
105
+ <div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-8">
106
+ <For each={locales()}>
107
+ {(locale) => {
108
+ const isSource = () => locale.id === currentLocale();
109
+ const pct = () => (isSource() ? 100 : coverage(locale.id));
110
+ return (
111
+ <div class="border border-border rounded-xl p-5 bg-surface/30 flex flex-col gap-3">
112
+ <div class="flex items-center gap-2">
113
+ <span
114
+ class={`${
115
+ isSource()
116
+ ? "i-mdi:check-decagram text-success"
117
+ : "i-mdi:translate text-primary"
118
+ } text-xl`}
119
+ aria-hidden="true"
120
+ />
121
+ <div class="min-w-0 flex-1">
122
+ <div class="font-semibold text-sm text-foreground truncate">
123
+ {locale.label}
124
+ </div>
125
+ <div class="text-[10px] text-muted font-mono">
126
+ {locale.id}
127
+ </div>
128
+ </div>
129
+ <Show when={isSource()}>
130
+ <span class="text-[10px] px-1.5 py-0.5 rounded bg-success/10 text-success">
131
+ Source
132
+ </span>
133
+ </Show>
134
+ </div>
135
+ <div>
136
+ <div class="flex items-center justify-between text-xs mb-1.5">
137
+ <span class="text-muted">Coverage</span>
138
+ <span class="font-medium text-foreground">{pct()}%</span>
139
+ </div>
140
+ <div class="h-2 rounded-full bg-background border border-border overflow-hidden">
141
+ <div
142
+ class={`h-full transition-all ${pct() === 100 ? "bg-success" : "bg-primary"}`}
143
+ style={{ width: `${pct()}%` }}
144
+ />
145
+ </div>
146
+ </div>
147
+ <Show when={!isSource()}>
148
+ <p class="text-xs text-muted m-0">
149
+ {pct() === 0
150
+ ? "No translations yet — generated by AI in CI."
151
+ : `${pct()}% of docs translated.`}
152
+ </p>
153
+ </Show>
154
+ </div>
155
+ );
156
+ }}
157
+ </For>
158
+ </div>
159
+
160
+ {/* How AI translation works */}
161
+ <section class="mt-10 border border-border rounded-xl bg-surface/30 p-6">
162
+ <h2 class="text-lg font-semibold m-0 mb-1 flex items-center gap-2">
163
+ <span class="i-mdi:robot-outline text-primary" aria-hidden="true" />
164
+ How AI translation works
165
+ </h2>
166
+ <p class="text-sm text-muted m-0 mb-4 leading-relaxed">
167
+ Translations are produced by an AI provider inside CI — not in the
168
+ browser. The workflow scans every collection, writes translated
169
+ markdown under a locale directory (e.g.{" "}
170
+ <code class="font-mono text-xs bg-background border border-border rounded px-1 py-0.5">
171
+ docs/{targets()[0]?.id ?? "th"}/…
172
+ </code>
173
+ ) and commits the result.
174
+ </p>
175
+ <ol class="m-0 p-0 list-none flex flex-col gap-2 text-sm text-foreground/90">
176
+ <For
177
+ each={[
178
+ "Run the translate command locally or trigger the CI workflow below.",
179
+ `The AI provider (${provider()}) translates each doc while preserving markdown, frontmatter, and code blocks.`,
180
+ "CI commits translated files and rebuilds — the locale switcher picks them up automatically.",
181
+ ]}
182
+ >
183
+ {(step, i) => (
184
+ <li class="flex items-start gap-2.5">
185
+ <span class="w-5 h-5 rounded-full bg-primary/15 text-primary text-[11px] font-semibold inline-flex items-center justify-center shrink-0 mt-0.5">
186
+ {i() + 1}
187
+ </span>
188
+ <span class="leading-relaxed">{step}</span>
189
+ </li>
190
+ )}
191
+ </For>
192
+ </ol>
193
+ <div class="grid sm:grid-cols-2 gap-3 mt-5">
194
+ <div class="border border-border rounded-lg bg-background p-3">
195
+ <div class="text-[10px] uppercase tracking-wide text-muted mb-1.5">
196
+ CI dispatch
197
+ </div>
198
+ <code class="block text-xs font-mono text-foreground break-all">
199
+ {ciCommand()}
200
+ </code>
201
+ <button
202
+ type="button"
203
+ onClick={() => copy("ci", ciCommand())}
204
+ class="mt-2 px-2.5 h-7 rounded-md border border-border text-xs text-muted hover:text-foreground hover:bg-surface transition-colors cursor-pointer"
205
+ >
206
+ {copied() === "ci" ? "Copied!" : "Copy"}
207
+ </button>
208
+ </div>
209
+ <div class="border border-border rounded-lg bg-background p-3">
210
+ <div class="text-[10px] uppercase tracking-wide text-muted mb-1.5">
211
+ Local CLI
212
+ </div>
213
+ <code class="block text-xs font-mono text-foreground break-all">
214
+ {cliCommand()}
215
+ </code>
216
+ <button
217
+ type="button"
218
+ onClick={() => copy("cli", cliCommand())}
219
+ class="mt-2 px-2.5 h-7 rounded-md border border-border text-xs text-muted hover:text-foreground hover:bg-surface transition-colors cursor-pointer"
220
+ >
221
+ {copied() === "cli" ? "Copied!" : "Copy"}
222
+ </button>
223
+ </div>
224
+ </div>
225
+ </section>
226
+
227
+ {/* Per-doc status matrix */}
228
+ <section class="mt-10">
229
+ <div class="flex items-center gap-2 mb-4 flex-wrap">
230
+ <h2 class="text-lg font-semibold m-0 mr-2">Docs status</h2>
231
+ <button
232
+ type="button"
233
+ onClick={() => setCollectionFilter(null)}
234
+ class={`px-3 h-7 rounded-full text-xs border transition-colors cursor-pointer ${
235
+ collectionFilter() === null
236
+ ? "bg-primary text-primary-foreground border-primary"
237
+ : "bg-surface text-muted border-border hover:border-focus"
238
+ }`}
239
+ >
240
+ All
241
+ </button>
242
+ <For each={collections() ?? []}>
243
+ {(c) => (
244
+ <button
245
+ type="button"
246
+ onClick={() => setCollectionFilter(c.id)}
247
+ class={`px-3 h-7 rounded-full text-xs border transition-colors cursor-pointer ${
248
+ collectionFilter() === c.id
249
+ ? "bg-primary text-primary-foreground border-primary"
250
+ : "bg-surface text-muted border-border hover:border-focus"
251
+ }`}
252
+ >
253
+ {c.label}
254
+ </button>
255
+ )}
256
+ </For>
257
+ </div>
258
+ <Show
259
+ when={!docs.loading}
260
+ fallback={<p class="text-sm text-muted">Loading docs…</p>}
261
+ >
262
+ <div class="border border-border rounded-xl overflow-hidden">
263
+ <table class="w-full text-sm">
264
+ <thead>
265
+ <tr class="bg-background/60 border-b border-border">
266
+ <th class="text-left px-4 py-2.5 font-medium text-muted text-xs uppercase tracking-wide">
267
+ Document
268
+ </th>
269
+ <th class="text-left px-4 py-2.5 font-medium text-muted text-xs uppercase tracking-wide hidden sm:table-cell">
270
+ Collection
271
+ </th>
272
+ <For each={targets()}>
273
+ {(l) => (
274
+ <th class="px-4 py-2.5 font-medium text-muted text-xs uppercase tracking-wide text-center">
275
+ {l.id}
276
+ </th>
277
+ )}
278
+ </For>
279
+ </tr>
280
+ </thead>
281
+ <tbody>
282
+ <For each={rows()}>
283
+ {(row) => (
284
+ <tr class="border-b border-border/60 last:border-0 hover:bg-surface/40 transition-colors">
285
+ <td class="px-4 py-2.5">
286
+ <Link
287
+ to="/$collection/$docId"
288
+ params={{
289
+ collection: row.collection,
290
+ docId: row.entry.id,
291
+ }}
292
+ class="text-foreground no-underline hover:text-primary transition-colors font-medium"
293
+ >
294
+ {row.entry.label}
295
+ </Link>
296
+ </td>
297
+ <td class="px-4 py-2.5 text-muted hidden sm:table-cell">
298
+ {row.collectionLabel}
299
+ </td>
300
+ <For each={targets()}>
301
+ {(l) => (
302
+ <td class="px-4 py-2.5 text-center">
303
+ <Show
304
+ when={isTranslated(row, l.id)}
305
+ fallback={
306
+ <span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-warning/10 text-warning">
307
+ <span
308
+ class="i-mdi:clock-outline"
309
+ aria-hidden="true"
310
+ />
311
+ Pending
312
+ </span>
313
+ }
314
+ >
315
+ <span class="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-success/10 text-success">
316
+ <span class="i-mdi:check" aria-hidden="true" />
317
+ Done
318
+ </span>
319
+ </Show>
320
+ </td>
321
+ )}
322
+ </For>
323
+ </tr>
324
+ )}
325
+ </For>
326
+ </tbody>
327
+ </table>
328
+ <Show when={rows().length === 0}>
329
+ <p class="p-6 text-sm text-muted text-center m-0">
330
+ No documents found.
331
+ </p>
332
+ </Show>
333
+ </div>
334
+ </Show>
335
+ </section>
336
+ </div>
337
+ );
338
+ }
@@ -0,0 +1,170 @@
1
+ import type { PluginInfo } from "./config";
2
+
3
+ /**
4
+ * Official plugin catalog for the `/plugins` marketplace page.
5
+ * `status`: "built-in" (shipped in the core package), "available"
6
+ * (installable package), or "planned" (on the roadmap).
7
+ */
8
+ export const officialPlugins: PluginInfo[] = [
9
+ {
10
+ name: "@wrikka/create-docs",
11
+ description: "Core docs runtime — SolidJS, TanStack Router, MiniSearch.",
12
+ icon: "i-mdi:book-open-page-variant",
13
+ category: "Core",
14
+ status: "built-in",
15
+ },
16
+ {
17
+ name: "Search & command palette",
18
+ description:
19
+ "MiniSearch full-text search, /search page, nav search, and Ctrl K palette.",
20
+ icon: "i-mdi:magnify",
21
+ category: "Core",
22
+ status: "built-in",
23
+ },
24
+ {
25
+ name: "SEO feeds",
26
+ description:
27
+ "Sitemap, RSS/Atom, JSON Feed, robots.txt, and Open Graph generation.",
28
+ icon: "i-mdi:rss",
29
+ category: "Core",
30
+ status: "built-in",
31
+ },
32
+ {
33
+ name: "PWA",
34
+ description:
35
+ "Service worker, web manifest, and offline support out of the box.",
36
+ icon: "i-mdi:cellphone-arrow-down",
37
+ category: "Core",
38
+ status: "built-in",
39
+ },
40
+ {
41
+ name: "OpenAPI adapter",
42
+ description:
43
+ "Render OpenAPI specs as interactive API reference pages with a live playground.",
44
+ icon: "i-mdi:api",
45
+ category: "Adapters",
46
+ status: "built-in",
47
+ url: "https://github.com/wrikka/create-docs",
48
+ },
49
+ {
50
+ name: "oRPC adapter",
51
+ description: "Generate docs from oRPC router definitions.",
52
+ icon: "i-mdi:lan-connect",
53
+ category: "Adapters",
54
+ status: "built-in",
55
+ },
56
+ {
57
+ name: "Elysia adapter",
58
+ description: "Auto-generate docs from Elysia Eden treaties.",
59
+ icon: "i-mdi:server",
60
+ category: "Adapters",
61
+ status: "built-in",
62
+ },
63
+ {
64
+ name: "Nitro adapter",
65
+ description: "Reference Nitro server handlers and API routes.",
66
+ icon: "i-mdi:fire",
67
+ category: "Adapters",
68
+ status: "built-in",
69
+ },
70
+ {
71
+ name: "GraphQL adapter",
72
+ description: "Render GraphQL queries, variables, and schema sections.",
73
+ icon: "i-mdi:graphql",
74
+ category: "Adapters",
75
+ status: "built-in",
76
+ },
77
+ {
78
+ name: "CLI adapter",
79
+ description: "Document CLI commands, subcommands, and arguments.",
80
+ icon: "i-mdi:console-line",
81
+ category: "Adapters",
82
+ status: "built-in",
83
+ },
84
+ {
85
+ name: "@wrikka/create-docs-translate",
86
+ description:
87
+ "AI-powered docs translation driven from CI — scan docs, produce a translation plan, and write locale-prefixed markdown.",
88
+ icon: "i-mdi:translate",
89
+ install: "bun add -D @wrikka/create-docs",
90
+ category: "AI",
91
+ status: "beta",
92
+ },
93
+ {
94
+ name: "MCP server",
95
+ description:
96
+ "Expose docs search and content as Model Context Protocol tools at /mcp.",
97
+ icon: "i-mdi:robot-outline",
98
+ category: "AI",
99
+ status: "built-in",
100
+ },
101
+ {
102
+ name: "LLM documentation",
103
+ description:
104
+ "Auto-generated /llms.txt and /llms-plugins.txt so AI agents can consume the docs.",
105
+ icon: "i-mdi:text-box-outline",
106
+ category: "AI",
107
+ status: "built-in",
108
+ },
109
+ {
110
+ name: "Ask AI",
111
+ description:
112
+ "Per-page AI assistant dialog that can answer questions about the current doc.",
113
+ icon: "i-mdi:robot-happy-outline",
114
+ category: "AI",
115
+ status: "built-in",
116
+ },
117
+ {
118
+ name: "GitHub integration",
119
+ description:
120
+ "Edit links, issue reporting, PR shortcuts, contributors, releases, and OAuth sign-in.",
121
+ icon: "i-mdi:github",
122
+ category: "Integrations",
123
+ status: "built-in",
124
+ },
125
+ {
126
+ name: "Analytics",
127
+ description:
128
+ "Local page-view tracking with a built-in /analytics dashboard and beacon endpoint support.",
129
+ icon: "i-mdi:chart-box-outline",
130
+ category: "Integrations",
131
+ status: "built-in",
132
+ },
133
+ {
134
+ name: "API diff",
135
+ description:
136
+ "Compare two API collections and render breaking changes at /api-diff.",
137
+ icon: "i-mdi:file-compare",
138
+ category: "Integrations",
139
+ status: "built-in",
140
+ },
141
+ {
142
+ name: "External search",
143
+ description:
144
+ "Algolia and MeiliSearch ports for replacing the built-in MiniSearch backend.",
145
+ icon: "i-mdi:cloud-search-outline",
146
+ category: "Integrations",
147
+ status: "planned",
148
+ },
149
+ {
150
+ name: "Comments",
151
+ description: "Per-page comments via GitHub discussions (giscus-style).",
152
+ icon: "i-mdi:comment-text-outline",
153
+ category: "Integrations",
154
+ status: "planned",
155
+ },
156
+ {
157
+ name: "Diagrams & math",
158
+ description: "Mermaid diagrams and KaTeX math blocks in markdown.",
159
+ icon: "i-mdi:chart-timeline-variant",
160
+ category: "Content",
161
+ status: "planned",
162
+ },
163
+ {
164
+ name: "PDF export",
165
+ description: "Export the whole docs site (or a section) as a single PDF.",
166
+ icon: "i-mdi:file-pdf-box",
167
+ category: "Content",
168
+ status: "planned",
169
+ },
170
+ ];
@@ -24,7 +24,9 @@ import { HomePage } from "./pages/HomePage";
24
24
  import { IssuesPage } from "./pages/IssuesPage";
25
25
  import { OAuthCallbackPage } from "./pages/OAuthCallbackPage";
26
26
  import { PluginsPage } from "./pages/PluginsPage";
27
+ import { SearchPage } from "./pages/SearchPage";
27
28
  import { ShowcasePage } from "./pages/ShowcasePage";
29
+ import { TranslatePage } from "./pages/TranslatePage";
28
30
 
29
31
  export function createDocsRouter(config: DocsAppConfig) {
30
32
  const rootRoute = createRootRoute({
@@ -157,6 +159,27 @@ export function createDocsRouter(config: DocsAppConfig) {
157
159
  });
158
160
  extraRoutes.push(showcaseRoute);
159
161
 
162
+ if (config.features?.search !== false) {
163
+ const searchRoute = createRoute({
164
+ getParentRoute: () => rootRoute,
165
+ path: "search",
166
+ component: SearchPage,
167
+ validateSearch: (search: Record<string, unknown>) => ({
168
+ q: typeof search.q === "string" ? search.q : "",
169
+ }),
170
+ });
171
+ extraRoutes.push(searchRoute);
172
+ }
173
+
174
+ if (config.features?.translate || config.translate) {
175
+ const translateRoute = createRoute({
176
+ getParentRoute: () => rootRoute,
177
+ path: "translate",
178
+ component: TranslatePage,
179
+ });
180
+ extraRoutes.push(translateRoute);
181
+ }
182
+
160
183
  const oauthCallbackRoute = createRoute({
161
184
  getParentRoute: () => rootRoute,
162
185
  path: "auth/github/callback",