@karaoke-cms/module-docs 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@karaoke-cms/module-docs",
3
3
  "type": "module",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "description": "Docs module for karaoke-cms — documentation pages with sidebar navigation",
6
6
  "main": "./src/index.ts",
7
7
  "exports": {
@@ -23,13 +23,13 @@
23
23
  ],
24
24
  "peerDependencies": {
25
25
  "astro": ">=6.0.0",
26
- "@karaoke-cms/contracts": "^0.15.0"
26
+ "@karaoke-cms/contracts": "^0.16.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "astro": "^6.0.8",
30
30
  "vitest": "^4.1.1",
31
- "@karaoke-cms/astro": "0.15.0",
32
- "@karaoke-cms/contracts": "0.15.0"
31
+ "@karaoke-cms/astro": "0.16.0",
32
+ "@karaoke-cms/contracts": "0.16.0"
33
33
  },
34
34
  "scripts": {
35
35
  "test": "vitest run test/docs-factory.test.js"
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { ModuleInstance } from '@karaoke-cms/contracts';
1
+ import type { DocsSection, ModuleInstance } from '@karaoke-cms/contracts';
2
2
  import { cssContract } from './css-contract.js';
3
3
 
4
4
  export { cssContract } from './css-contract.js';
5
+ export type { DocsSection } from '@karaoke-cms/contracts';
5
6
  // docsSchema is exported from '@karaoke-cms/module-docs/schema' to avoid
6
7
  // pulling zod into the config-loading context.
7
8
 
@@ -24,43 +25,26 @@ export interface DocsConfig {
24
25
  }
25
26
 
26
27
  /**
27
- * Docs module documentation pages with sidebar navigation.
28
- *
29
- * Returns a ModuleInstance that karaoke() uses to code-generate per-instance
30
- * .astro page files and inject them as routes. Multiple instances may be
31
- * configured to serve separate docs sections from different vault folders.
32
- *
33
- * Publishes three routes per instance:
34
- * - `{mount}` — home page listing all doc titles
35
- * - `{mount}/list` — standalone full docs list
36
- * - `{mount}/[slug]` — individual doc with left-sidebar nav
37
- *
38
- * @example
39
- * // karaoke.config.ts — default section
40
- * import { docs } from '@karaoke-cms/module-docs';
41
- * export default defineConfig({
42
- * theme: themeDefault({ implements: [docs({ mount: '/docs' })] }),
43
- * });
44
- *
45
- * @example
46
- * // karaoke.config.ts — two independent sections
47
- * export default defineConfig({
48
- * theme: themeDefault({ implements: [docs({ mount: '/docs' })] }),
49
- * modules: [docs({ mount: '/api-docs', folder: 'api-reference', label: 'API Reference' })],
50
- * });
28
+ * Shared helper: derive mount, folder, id, and label from config fields.
29
+ * Used by both docsFromSingle and docsFromSection to keep derivation in sync.
51
30
  */
52
- export function docs(config: DocsConfig = {}): ModuleInstance {
53
- const mount = (config.mount ?? '/docs').replace(/\/$/, '');
31
+ function computeDocsMeta(cfg: { mount?: string; folder?: string; id?: string; label?: string }) {
32
+ const mount = (cfg.mount ?? '/docs').replace(/\/$/, '');
54
33
  const rawFolder = mount.replace(/^\//, '');
55
- if (config.folder === undefined && rawFolder === '') {
34
+ if (cfg.folder === undefined && rawFolder === '') {
56
35
  console.warn(
57
36
  '[karaoke-cms] docs({ mount: \'/\' }): folder defaults to "docs". ' +
58
37
  'Pass folder explicitly (e.g. docs({ mount: \'/\', folder: \'docs\' })) to suppress this warning.',
59
38
  );
60
39
  }
61
- const folder = config.folder ?? (rawFolder || 'docs');
62
- const id = config.id ?? (folder.replace(/\//g, '-') || 'docs');
63
- const label = config.label ?? (id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '));
40
+ const folder = cfg.folder ?? (rawFolder || 'docs');
41
+ const id = cfg.id ?? (folder.replace(/\//g, '-') || 'docs');
42
+ const label = cfg.label ?? id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
43
+ return { mount, folder, id, label };
44
+ }
45
+
46
+ function docsFromSingle(config: DocsConfig = {}): ModuleInstance {
47
+ const { mount, folder, id, label } = computeDocsMeta(config);
64
48
  return {
65
49
  _type: 'module-instance',
66
50
  id,
@@ -82,3 +66,68 @@ export function docs(config: DocsConfig = {}): ModuleInstance {
82
66
  scaffoldPages: undefined,
83
67
  };
84
68
  }
69
+
70
+ function docsFromSection(s: DocsSection): ModuleInstance {
71
+ const { mount, folder, id, label } = computeDocsMeta(s);
72
+ return {
73
+ _type: 'module-instance',
74
+ id,
75
+ mount,
76
+ enabled: true,
77
+ collection: {
78
+ name: id,
79
+ folder,
80
+ label,
81
+ layout: s.layout ?? 'default',
82
+ sidebarStyle: s.sidebarStyle ?? 'tree',
83
+ },
84
+ // routes is empty — karaoke() generates per-instance pages via codegen
85
+ routes: [],
86
+ menuEntries: [{
87
+ id,
88
+ name: label,
89
+ path: mount,
90
+ section: 'main',
91
+ weight: s.weight ?? 20,
92
+ ...(s.parent !== undefined ? { parent: s.parent } : {}),
93
+ }],
94
+ cssContract,
95
+ hasDefaultCss: false,
96
+ defaultCssPath: undefined,
97
+ scaffoldPages: undefined,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Docs module — documentation pages with sidebar navigation.
103
+ *
104
+ * Single-form: returns one ModuleInstance.
105
+ * Array-form: accepts DocsSection[] and returns ModuleInstance[] (one per active section).
106
+ * Sections with `enabled: false` are excluded from the returned array.
107
+ *
108
+ * Publishes three routes per instance:
109
+ * - `{mount}` — home page listing all doc titles
110
+ * - `{mount}/list` — standalone full docs list
111
+ * - `{mount}/[slug]` — individual doc with left-sidebar nav
112
+ *
113
+ * @example
114
+ * // karaoke.config.ts — single section
115
+ * modules: [docs({ mount: '/docs' })]
116
+ *
117
+ * @example
118
+ * // karaoke.config.ts — multiple sections with menu ordering
119
+ * modules: [
120
+ * docs([
121
+ * { id: 'docs', mount: '/docs', label: 'Docs', weight: 20 },
122
+ * { id: 'api-docs', mount: '/api-docs', folder: 'api-reference', label: 'API Reference', weight: 25, parent: 'docs' },
123
+ * ]),
124
+ * ]
125
+ */
126
+ export function docs(config: DocsSection[]): ModuleInstance[];
127
+ export function docs(config?: DocsConfig): ModuleInstance;
128
+ export function docs(config?: DocsConfig | DocsSection[]): ModuleInstance | ModuleInstance[] {
129
+ if (Array.isArray(config)) {
130
+ return config.filter(s => s.enabled !== false).map(s => docsFromSection(s));
131
+ }
132
+ return docsFromSingle(config);
133
+ }
@@ -1,9 +1,18 @@
1
1
  ---
2
- // TODO: use mount path from virtual module once mount-aware routing is implemented.
3
- // For now, mount is assumed to be /docs.
2
+ // NOTE: this is a reference implementation only it is NOT used as an injected route.
3
+ // The actual route entrypoint is generated by karaoke() into
4
+ // .astro/generated/karaoke-cms/{id}/doc.astro
5
+ // Keep this file in sync with generateDocPage() in:
6
+ // packages/astro/src/codegen/generate-docs-instance.ts
7
+ //
8
+ // Differences from the generated version:
9
+ // - mount is hardcoded to /docs (default instance only)
10
+ // - collection is hardcoded to 'docs'
11
+ // - multi-section switcher (docsSections) is omitted
4
12
  import { getCollection, render } from 'astro:content';
5
13
  import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
6
14
  import ModuleLoader from '@karaoke-cms/astro/components/ModuleLoader.astro';
15
+ import DocsTree from '@karaoke-cms/astro/components/DocsTree.astro';
7
16
  import { resolveWikiImage } from '@karaoke-cms/astro';
8
17
 
9
18
  export async function getStaticPaths() {
@@ -17,23 +26,28 @@ export async function getStaticPaths() {
17
26
  const { entry } = Astro.props;
18
27
  const { Content } = await render(entry);
19
28
 
20
- // All docs for the sidebar, sorted alphabetically by title
21
29
  const allDocs = (await getCollection('docs', ({ data }) => data.publish === true))
22
30
  .sort((a, b) => a.data.title.localeCompare(b.data.title));
31
+
32
+ // Show the right sidebar only when the current entry is a directory (has child entries).
33
+ const isDirectory = allDocs.some(d => d.id.startsWith(entry.id + '/'));
23
34
  ---
24
35
 
25
36
  <DefaultPage title={entry.data.title} description={entry.data.description} type="article">
26
37
  <nav class="docs-sidebar" slot="left">
27
- <ul class="docs-sidebar-list">
28
- {allDocs.map(doc => (
29
- <li class="docs-sidebar-item">
30
- <a href={`/docs/${doc.id}`} aria-current={doc.id === entry.id ? 'page' : undefined}>
31
- {doc.data.title}
32
- </a>
33
- </li>
34
- ))}
35
- </ul>
38
+ <details class="docs-sidebar-wrap" open>
39
+ <summary class="docs-sidebar-toggle">Docs</summary>
40
+ <DocsTree entries={allDocs} currentId={entry.id} mount="/docs" />
41
+ </details>
36
42
  </nav>
43
+ {isDirectory && (
44
+ <aside class="docs-page-nav" slot="right">
45
+ <details class="docs-sidebar-wrap" open>
46
+ <summary class="docs-sidebar-toggle">In this section</summary>
47
+ <DocsTree entries={allDocs} currentId={entry.id} mount="/docs" rootPath={entry.id} maxDepth={1} />
48
+ </details>
49
+ </aside>
50
+ )}
37
51
  <article class="docs-article">
38
52
  {entry.data.featured_image && (
39
53
  <img src={resolveWikiImage(entry.data.featured_image)} alt="" class="docs-article-image" />