@raystack/chronicle 0.7.4 → 0.9.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.
Files changed (50) hide show
  1. package/dist/cli/index.js +14 -2
  2. package/package.json +3 -4
  3. package/src/components/api/api-code-snippet.module.css +23 -0
  4. package/src/components/api/api-code-snippet.tsx +64 -0
  5. package/src/components/api/api-field-list.module.css +76 -0
  6. package/src/components/api/api-field-list.tsx +91 -0
  7. package/src/components/api/api-overview.module.css +65 -0
  8. package/src/components/api/api-overview.tsx +216 -0
  9. package/src/components/api/api-response-panel.module.css +62 -0
  10. package/src/components/api/api-response-panel.tsx +54 -0
  11. package/src/components/api/index.ts +5 -6
  12. package/src/components/api/json-editor.tsx +8 -8
  13. package/src/components/api/method-badge.tsx +2 -2
  14. package/src/components/api/playground-dialog.module.css +342 -0
  15. package/src/components/api/playground-dialog.tsx +583 -0
  16. package/src/components/ui/search.module.css +27 -5
  17. package/src/components/ui/search.tsx +28 -19
  18. package/src/lib/api-routes.ts +37 -8
  19. package/src/lib/openapi.ts +26 -0
  20. package/src/lib/page-context.tsx +1 -1
  21. package/src/lib/schema.ts +45 -3
  22. package/src/lib/source.ts +79 -13
  23. package/src/lib/use-api-operation.ts +15 -0
  24. package/src/pages/ApiLayout.module.css +1 -0
  25. package/src/pages/ApiPage.tsx +7 -38
  26. package/src/pages/DocsPage.tsx +40 -1
  27. package/src/server/api/apis-proxy.ts +8 -1
  28. package/src/server/api/ready.ts +15 -0
  29. package/src/server/api/search.ts +159 -85
  30. package/src/server/entry-server.tsx +1 -1
  31. package/src/server/routes/[...slug].md.ts +1 -0
  32. package/src/server/routes/apis/[...slug].md.ts +181 -0
  33. package/src/server/vite-config.ts +11 -0
  34. package/src/themes/default/Layout.module.css +53 -0
  35. package/src/themes/default/Layout.tsx +162 -11
  36. package/src/themes/default/Page.module.css +4 -0
  37. package/src/themes/default/Page.tsx +6 -1
  38. package/src/types/config.ts +2 -1
  39. package/src/components/api/code-snippets.module.css +0 -7
  40. package/src/components/api/code-snippets.tsx +0 -76
  41. package/src/components/api/endpoint-page.module.css +0 -58
  42. package/src/components/api/endpoint-page.tsx +0 -283
  43. package/src/components/api/field-row.module.css +0 -126
  44. package/src/components/api/field-row.tsx +0 -204
  45. package/src/components/api/field-section.module.css +0 -24
  46. package/src/components/api/field-section.tsx +0 -100
  47. package/src/components/api/key-value-editor.module.css +0 -13
  48. package/src/components/api/key-value-editor.tsx +0 -62
  49. package/src/components/api/response-panel.module.css +0 -8
  50. package/src/components/api/response-panel.tsx +0 -44
@@ -1,79 +1,129 @@
1
- import MiniSearch from 'minisearch';
2
1
  import { defineHandler, HTTPError } from 'nitro';
2
+ import { useDatabase } from 'nitro/database';
3
3
  import type { OpenAPIV3 } from 'openapi-types';
4
4
  import { getSpecSlug } from '@/lib/api-routes';
5
5
  import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
6
6
  import { loadApiSpecs } from '@/lib/openapi';
7
- import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
7
+ import { extractFrontmatter, getPageSearchContent, getPagesForVersion } from '@/lib/source';
8
8
  import { LATEST_CONTEXT, type VersionContext } from '@/lib/version-source';
9
9
 
10
10
  interface SearchDocument {
11
11
  id: string;
12
12
  url: string;
13
13
  title: string;
14
- content: string;
14
+ headings: string;
15
+ body: string;
15
16
  type: 'page' | 'api';
16
17
  }
17
18
 
18
- const indexCache = new Map<string, MiniSearch<SearchDocument>>();
19
- const docsCache = new Map<string, SearchDocument[]>();
19
+ import fs from 'node:fs/promises';
20
+ import { existsSync } from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
20
23
 
21
- function keyFor(ctx: VersionContext): string {
24
+ const LOCK_FILE = path.join(os.tmpdir(), 'chronicle-search-ready');
25
+
26
+ export const indexedVersions = new Set<string>();
27
+ let indexPromise: Promise<void> | null = null;
28
+
29
+ function versionKey(ctx: VersionContext): string {
22
30
  return ctx.dir ?? '__latest__';
23
31
  }
24
32
 
25
- function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
26
- const index = new MiniSearch<SearchDocument>({
27
- fields: ['title', 'content'],
28
- storeFields: ['url', 'title', 'type'],
29
- searchOptions: {
30
- boost: { title: 2 },
31
- fuzzy: 0.2,
32
- prefix: true
33
- }
34
- });
35
- index.addAll(docs);
36
- return index;
33
+ // biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op catch
34
+ fs.unlink(LOCK_FILE).catch(() => {});
35
+
36
+ export function isSearchReady(): boolean {
37
+ return existsSync(LOCK_FILE);
38
+ }
39
+
40
+ export async function ensureIndex(ctx: VersionContext) {
41
+ const key = versionKey(ctx);
42
+ if (indexedVersions.has(key)) return;
43
+ if (indexPromise) return indexPromise;
44
+ indexPromise = buildIndex(ctx, key);
45
+ await indexPromise;
46
+ indexPromise = null;
47
+ await fs.writeFile(LOCK_FILE, new Date().toISOString());
48
+ }
49
+
50
+ async function buildIndex(ctx: VersionContext, key: string) {
51
+ // biome-ignore lint/correctness/useHookAtTopLevel: useDatabase is a Nitro DI accessor, not a React hook
52
+ const db = useDatabase();
53
+
54
+ await db.exec('DROP TABLE IF EXISTS search_fts');
55
+ await db.exec('DROP TABLE IF EXISTS search_docs');
56
+
57
+ await db.exec(`CREATE TABLE IF NOT EXISTS search_docs (
58
+ id TEXT PRIMARY KEY,
59
+ url TEXT NOT NULL,
60
+ title TEXT NOT NULL,
61
+ headings TEXT NOT NULL,
62
+ body TEXT NOT NULL,
63
+ type TEXT NOT NULL,
64
+ version TEXT NOT NULL
65
+ )`);
66
+
67
+ await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
68
+ title,
69
+ headings,
70
+ body,
71
+ content=search_docs,
72
+ content_rowid=rowid
73
+ )`);
74
+
75
+ const docs = await buildDocs(ctx);
76
+ for (const doc of docs) {
77
+ await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version)
78
+ VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key})`;
79
+ }
80
+
81
+ await db.sql`INSERT INTO search_fts (rowid, title, headings, body)
82
+ SELECT rowid, title, headings, body FROM search_docs WHERE version = ${key}`;
83
+
84
+ indexedVersions.add(key);
37
85
  }
38
86
 
39
- async function scanContent(ctx: VersionContext): Promise<SearchDocument[]> {
87
+ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
88
+ const docs: SearchDocument[] = [];
89
+
40
90
  const pages = await getPagesForVersion(ctx);
41
- return pages.map(p => {
91
+ for (const p of pages) {
42
92
  const fm = extractFrontmatter(p);
43
- return {
93
+ const { headings, body } = await getPageSearchContent(p);
94
+ docs.push({
44
95
  id: p.url,
45
96
  url: p.url,
46
97
  title: fm.title,
47
- content: fm.description ?? '',
48
- type: 'page' as const
49
- };
50
- });
51
- }
98
+ headings,
99
+ body: [fm.description ?? '', body].join(' '),
100
+ type: 'page',
101
+ });
102
+ }
52
103
 
53
- async function buildApiDocs(ctx: VersionContext): Promise<SearchDocument[]> {
54
104
  const config = loadConfig();
55
105
  const apiConfigs = getApiConfigsForVersion(config, ctx.dir);
56
- if (!apiConfigs.length) return [];
57
-
58
- const docs: SearchDocument[] = [];
59
- const specs = await loadApiSpecs(apiConfigs);
60
-
61
- for (const spec of specs) {
62
- const specSlug = getSpecSlug(spec);
63
- const paths = spec.document.paths ?? {};
64
- for (const [, pathItem] of Object.entries(paths)) {
65
- if (!pathItem) continue;
66
- for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
67
- const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
68
- if (!op?.operationId) continue;
69
- const url = `${ctx.urlPrefix}/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
70
- docs.push({
71
- id: url,
72
- url,
73
- title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
74
- content: op.description ?? '',
75
- type: 'api'
76
- });
106
+ if (apiConfigs.length) {
107
+ const specs = await loadApiSpecs(apiConfigs);
108
+ for (const spec of specs) {
109
+ const specSlug = getSpecSlug(spec);
110
+ const paths = spec.document.paths ?? {};
111
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
112
+ if (!pathItem) continue;
113
+ for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
114
+ const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
115
+ if (!op) continue;
116
+ const opId = op.operationId ?? `${method}_${pathStr.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`;
117
+ const url = `${ctx.urlPrefix}/apis/${specSlug}/${encodeURIComponent(opId)}`;
118
+ docs.push({
119
+ id: url,
120
+ url,
121
+ title: `${method.toUpperCase()} ${op.summary ?? opId}`,
122
+ headings: op.summary ?? opId,
123
+ body: [op.description ?? '', pathStr, method.toUpperCase()].join(' '),
124
+ type: 'api',
125
+ });
126
+ }
77
127
  }
78
128
  }
79
129
  }
@@ -81,27 +131,32 @@ async function buildApiDocs(ctx: VersionContext): Promise<SearchDocument[]> {
81
131
  return docs;
82
132
  }
83
133
 
84
- async function getDocs(ctx: VersionContext): Promise<SearchDocument[]> {
85
- const key = keyFor(ctx);
86
- const cached = docsCache.get(key);
87
- if (cached) return cached;
88
- const [contentDocs, apiDocs] = await Promise.all([
89
- scanContent(ctx),
90
- buildApiDocs(ctx)
91
- ]);
92
- const docs = [...contentDocs, ...apiDocs];
93
- docsCache.set(key, docs);
94
- return docs;
95
- }
134
+ function findMatch(
135
+ query: string,
136
+ title: string,
137
+ headings: string,
138
+ body: string,
139
+ ): { match: 'title' | 'heading' | 'body'; snippet: string } {
140
+ if (title.toLowerCase().includes(query)) {
141
+ return { match: 'title', snippet: title };
142
+ }
96
143
 
97
- async function getIndex(ctx: VersionContext): Promise<MiniSearch<SearchDocument>> {
98
- const key = keyFor(ctx);
99
- const cached = indexCache.get(key);
100
- if (cached) return cached;
101
- const docs = await getDocs(ctx);
102
- const index = createIndex(docs);
103
- indexCache.set(key, index);
104
- return index;
144
+ const headingList = headings.split('\n').filter(Boolean);
145
+ for (const h of headingList) {
146
+ if (h.toLowerCase().includes(query)) {
147
+ return { match: 'heading', snippet: h };
148
+ }
149
+ }
150
+
151
+ const idx = body.toLowerCase().indexOf(query);
152
+ if (idx >= 0) {
153
+ const start = Math.max(0, idx - 40);
154
+ const end = Math.min(body.length, idx + query.length + 80);
155
+ const snippet = (start > 0 ? '...' : '') + body.slice(start, end).trim() + (end < body.length ? '...' : '');
156
+ return { match: 'body', snippet };
157
+ }
158
+
159
+ return { match: 'title', snippet: title };
105
160
  }
106
161
 
107
162
  function resolveCtx(tag: string | null): VersionContext {
@@ -121,25 +176,44 @@ export default defineHandler(async event => {
121
176
  const query = event.url.searchParams.get('query') ?? '';
122
177
  const tag = event.url.searchParams.get('tag');
123
178
  const ctx = resolveCtx(tag);
124
- const index = await getIndex(ctx);
179
+
180
+ await ensureIndex(ctx);
181
+ // biome-ignore lint/correctness/useHookAtTopLevel: useDatabase is a Nitro DI accessor, not a React hook
182
+ const db = useDatabase();
183
+ const key = versionKey(ctx);
125
184
 
126
185
  if (!query) {
127
- const docs = await getDocs(ctx);
128
- return Response.json(docs
129
- .filter(d => d.type === 'page')
130
- .slice(0, 8)
131
- .map(d => ({
132
- id: d.id,
133
- url: d.url,
134
- type: d.type,
135
- content: d.title
136
- })));
186
+ const result = await db.sql`SELECT id, url, title, type FROM search_docs
187
+ WHERE version = ${key} AND type = 'page'
188
+ LIMIT 8`;
189
+ return Response.json((result.rows ?? []).map(r => ({
190
+ id: r.id,
191
+ url: r.url,
192
+ type: r.type,
193
+ content: r.title,
194
+ })));
137
195
  }
138
196
 
139
- return Response.json(index.search(query).map(r => ({
140
- id: r.id,
141
- url: r.url,
142
- type: r.type,
143
- content: r.title
144
- })));
197
+ const searchTerm = query.split(/\s+/).map(t => `"${t}"*`).join(' ');
198
+ const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type,
199
+ bm25(search_fts, 10.0, 5.0, 1.0) AS score
200
+ FROM search_fts f
201
+ JOIN search_docs s ON s.rowid = f.rowid
202
+ WHERE search_fts MATCH ${searchTerm}
203
+ AND s.version = ${key}
204
+ ORDER BY score
205
+ LIMIT 20`;
206
+
207
+ const queryLower = query.toLowerCase();
208
+ return Response.json((result.rows ?? []).map(r => {
209
+ const { match, snippet } = findMatch(queryLower, r.title as string, r.headings as string, r.body as string);
210
+ return {
211
+ id: r.id,
212
+ url: r.url,
213
+ type: r.type,
214
+ content: r.title,
215
+ match,
216
+ snippet,
217
+ };
218
+ }));
145
219
  });
@@ -19,7 +19,7 @@ import serverAssets from './entry-server?assets=ssr';
19
19
  export default {
20
20
  async fetch(req: Request) {
21
21
  const url = new URL(req.url);
22
- const pathname = url.pathname;
22
+ const pathname = decodeURIComponent(url.pathname);
23
23
 
24
24
  const config = loadConfig();
25
25
  const route = resolveRoute(pathname, config);
@@ -7,6 +7,7 @@ import { safePath } from '@/server/utils/safe-path';
7
7
  export default defineHandler(async event => {
8
8
  const pathname = event.path || event.req.url?.split('?')[0] || '';
9
9
  if (!pathname.endsWith('.md')) return;
10
+ if (pathname.startsWith('/apis/')) return;
10
11
 
11
12
  const stripped = pathname.replace(/\.md$/, '');
12
13
  const parts = stripped === '/index' || stripped === '/'
@@ -0,0 +1,181 @@
1
+ import type { OpenAPIV3 } from 'openapi-types'
2
+ import { defineHandler, HTTPError } from 'nitro'
3
+ import { loadConfig } from '@/lib/config'
4
+ import { loadApiSpecs } from '@/lib/openapi'
5
+ import { findApiOperation } from '@/lib/api-routes'
6
+ import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
7
+ import { generateCurl } from '@/lib/snippet-generators'
8
+
9
+ export default defineHandler(async event => {
10
+ const pathname = event.path || event.req.url?.split('?')[0] || ''
11
+ if (!pathname.endsWith('.md')) return
12
+
13
+ const stripped = pathname.replace(/\.md$/, '').replace(/^\/apis\//, '')
14
+ const slug = stripped.split('/').filter(Boolean)
15
+ if (slug.length < 2) {
16
+ throw new HTTPError({ status: 404, message: 'Not Found' })
17
+ }
18
+
19
+ const config = loadConfig()
20
+ const specs = await loadApiSpecs(config.api ?? [])
21
+ const match = findApiOperation(specs, slug)
22
+
23
+ if (!match) {
24
+ throw new HTTPError({ status: 404, message: 'Not Found' })
25
+ }
26
+
27
+ const md = generateApiMarkdown(match.method, match.path, match.operation, match.spec.server.url, match.spec.auth)
28
+ return new Response(md, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } })
29
+ })
30
+
31
+ function generateApiMarkdown(
32
+ method: string,
33
+ path: string,
34
+ operation: OpenAPIV3.OperationObject,
35
+ serverUrl: string,
36
+ auth?: { type: string; header: string; placeholder?: string },
37
+ ): string {
38
+ const lines: string[] = []
39
+ const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
40
+
41
+ lines.push(`# ${operation.summary ?? `${method} ${path}`}`)
42
+ lines.push('')
43
+ if (operation.description) {
44
+ lines.push(operation.description)
45
+ lines.push('')
46
+ }
47
+ lines.push(`\`${method}\` \`${path}\``)
48
+ lines.push('')
49
+
50
+ const headerParams = params.filter(p => p.in === 'header')
51
+ const pathParams = params.filter(p => p.in === 'path')
52
+ const queryParams = params.filter(p => p.in === 'query')
53
+
54
+ if (auth || headerParams.length > 0) {
55
+ lines.push('## Authorization')
56
+ lines.push('')
57
+ lines.push('| Header | Type | Required | Description |')
58
+ lines.push('| --- | --- | --- | --- |')
59
+ if (auth) {
60
+ lines.push(`| \`${auth.header}\` | string | Yes | ${auth.placeholder ?? 'API key'} |`)
61
+ }
62
+ for (const p of headerParams) {
63
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
64
+ lines.push(`| \`${p.name}\` | ${schema.type ?? 'string'} | ${p.required ? 'Yes' : 'No'} | ${p.description ?? ''} |`)
65
+ }
66
+ lines.push('')
67
+ }
68
+
69
+ if (pathParams.length > 0) {
70
+ lines.push('## Path Parameters')
71
+ lines.push('')
72
+ lines.push('| Parameter | Type | Required | Description |')
73
+ lines.push('| --- | --- | --- | --- |')
74
+ for (const p of pathParams) {
75
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
76
+ lines.push(`| \`${p.name}\` | ${schema.type ?? 'string'} | ${p.required ? 'Yes' : 'No'} | ${p.description ?? ''} |`)
77
+ }
78
+ lines.push('')
79
+ }
80
+
81
+ if (queryParams.length > 0) {
82
+ lines.push('## Query Parameters')
83
+ lines.push('')
84
+ lines.push('| Parameter | Type | Required | Description |')
85
+ lines.push('| --- | --- | --- | --- |')
86
+ for (const p of queryParams) {
87
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
88
+ lines.push(`| \`${p.name}\` | ${schema.type ?? 'string'} | ${p.required ? 'Yes' : 'No'} | ${p.description ?? ''} |`)
89
+ }
90
+ lines.push('')
91
+ }
92
+
93
+ const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject | undefined
94
+ if (requestBody?.content) {
95
+ const contentType = Object.keys(requestBody.content)[0]
96
+ const schema = contentType ? requestBody.content[contentType]?.schema as OpenAPIV3.SchemaObject : undefined
97
+ if (schema) {
98
+ lines.push('## Request Body')
99
+ lines.push('')
100
+ lines.push(`Content-Type: \`${contentType}\``)
101
+ lines.push('')
102
+ const fields = flattenSchema(schema)
103
+ if (fields.length > 0) {
104
+ lines.push('| Field | Type | Required | Description |')
105
+ lines.push('| --- | --- | --- | --- |')
106
+ renderFieldTable(fields, lines, 0)
107
+ lines.push('')
108
+ }
109
+ const example = generateExampleJson(schema)
110
+ lines.push('**Example:**')
111
+ lines.push('')
112
+ lines.push('```json')
113
+ lines.push(JSON.stringify(example, null, 2))
114
+ lines.push('```')
115
+ lines.push('')
116
+ }
117
+ }
118
+
119
+ const responses = operation.responses as Record<string, OpenAPIV3.ResponseObject> | undefined
120
+ if (responses) {
121
+ lines.push('## Responses')
122
+ lines.push('')
123
+ for (const [status, resp] of Object.entries(responses)) {
124
+ lines.push(`### ${status}${resp.description ? ` — ${resp.description}` : ''}`)
125
+ lines.push('')
126
+ const content = resp.content ?? {}
127
+ const contentType = Object.keys(content)[0]
128
+ const schema = contentType ? content[contentType]?.schema as OpenAPIV3.SchemaObject : undefined
129
+ if (schema) {
130
+ const fields = flattenSchema(schema)
131
+ if (fields.length > 0) {
132
+ lines.push('| Field | Type | Description |')
133
+ lines.push('| --- | --- | --- |')
134
+ renderResponseFieldTable(fields, lines, 0)
135
+ lines.push('')
136
+ }
137
+ const example = generateExampleJson(schema)
138
+ lines.push('```json')
139
+ lines.push(JSON.stringify(example, null, 2))
140
+ lines.push('```')
141
+ lines.push('')
142
+ }
143
+ }
144
+ }
145
+
146
+ const headers: Record<string, string> = {}
147
+ if (auth) headers[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
148
+ if (requestBody?.content) {
149
+ const ct = Object.keys(requestBody.content)[0]
150
+ if (ct) headers['Content-Type'] = ct
151
+ }
152
+
153
+ const bodySchema = requestBody?.content
154
+ ? (Object.values(requestBody.content)[0]?.schema as OpenAPIV3.SchemaObject | undefined)
155
+ : undefined
156
+ const bodyStr = bodySchema ? JSON.stringify(generateExampleJson(bodySchema), null, 2) : undefined
157
+
158
+ lines.push('## cURL')
159
+ lines.push('')
160
+ lines.push('```bash')
161
+ lines.push(generateCurl({ method, url: serverUrl + path, headers, body: bodyStr }))
162
+ lines.push('```')
163
+
164
+ return lines.join('\n')
165
+ }
166
+
167
+ function renderFieldTable(fields: SchemaField[], lines: string[], depth: number) {
168
+ const indent = ' '.repeat(depth)
169
+ for (const f of fields) {
170
+ lines.push(`| ${indent}\`${f.name}\` | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description ?? ''} |`)
171
+ if (f.children) renderFieldTable(f.children, lines, depth + 1)
172
+ }
173
+ }
174
+
175
+ function renderResponseFieldTable(fields: SchemaField[], lines: string[], depth: number) {
176
+ const indent = ' '.repeat(depth)
177
+ for (const f of fields) {
178
+ lines.push(`| ${indent}\`${f.name}\` | ${f.type} | ${f.description ?? ''} |`)
179
+ if (f.children) renderResponseFieldTable(f.children, lines, depth + 1)
180
+ }
181
+ }
@@ -48,6 +48,7 @@ export async function createViteConfig(
48
48
 
49
49
  return {
50
50
  root: packageRoot,
51
+ publicDir: path.resolve(projectRoot, 'public'),
51
52
  configFile: false,
52
53
  plugins: [
53
54
  nitro({
@@ -131,9 +132,19 @@ export async function createViteConfig(
131
132
  },
132
133
  nitro: {
133
134
  logLevel: 2,
135
+ publicAssets: [{ dir: path.resolve(projectRoot, 'public') }],
134
136
  output: {
135
137
  dir: resolveOutputDir(projectRoot, preset),
136
138
  },
139
+ experimental: {
140
+ database: true,
141
+ },
142
+ database: {
143
+ default: {
144
+ connector: 'sqlite',
145
+ options: { name: 'chronicle-search' },
146
+ },
147
+ },
137
148
  },
138
149
  };
139
150
  }
@@ -226,3 +226,56 @@
226
226
  .page {
227
227
  padding: var(--rs-space-2) 0;
228
228
  }
229
+
230
+ .apiGroup {
231
+ margin-top: var(--rs-space-8);
232
+ width: 100%;
233
+ }
234
+
235
+ .apiGroup:first-child {
236
+ margin-top: 0;
237
+ }
238
+
239
+ .apiGroupLabel {
240
+ font-size: var(--rs-font-size-small);
241
+ font-weight: var(--rs-font-weight-medium);
242
+ line-height: var(--rs-line-height-small);
243
+ letter-spacing: var(--rs-letter-spacing-small);
244
+ color: var(--rs-color-foreground-base-secondary);
245
+ padding: 0 var(--rs-space-3);
246
+ }
247
+
248
+ .apiItem {
249
+ padding: var(--rs-space-3);
250
+ border-radius: var(--rs-radius-2);
251
+ text-decoration: none;
252
+ cursor: pointer;
253
+ white-space: nowrap;
254
+ }
255
+
256
+ .apiItem:hover {
257
+ background: var(--rs-color-background-neutral-secondary);
258
+ }
259
+
260
+ .apiItemActive {
261
+ background: var(--rs-color-background-neutral-secondary);
262
+ }
263
+
264
+ .apiItemName {
265
+ flex: 1;
266
+ min-width: 0;
267
+ overflow: hidden;
268
+ text-overflow: ellipsis;
269
+ font-size: var(--rs-font-size-small);
270
+ font-weight: var(--rs-font-weight-medium);
271
+ line-height: var(--rs-line-height-small);
272
+ letter-spacing: var(--rs-letter-spacing-small);
273
+ color: var(--rs-color-foreground-base-primary);
274
+ }
275
+
276
+ .apiMethodText {
277
+ font-family: var(--rs-font-mono);
278
+ font-size: var(--rs-font-size-mono-mini);
279
+ line-height: var(--rs-line-height-mini);
280
+ flex-shrink: 0;
281
+ }