@karaoke-cms/theme-default 0.9.0 → 0.9.2

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/theme-default",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.9.2",
5
5
  "description": "Default theme for karaoke-cms — two-column knowledge base with blog and docs",
6
6
  "main": "./src/index.ts",
7
7
  "exports": {
@@ -16,13 +16,16 @@
16
16
  "theme",
17
17
  "karaoke-cms"
18
18
  ],
19
+ "dependencies": {
20
+ "@karaoke-cms/module-blog": "0.9.2"
21
+ },
19
22
  "peerDependencies": {
20
23
  "astro": ">=6.0.0",
21
- "@karaoke-cms/astro": "^0.9.0"
24
+ "@karaoke-cms/astro": "^0.9.2"
22
25
  },
23
26
  "devDependencies": {
24
27
  "astro": "^6.0.8",
25
- "@karaoke-cms/astro": "0.9.0"
28
+ "@karaoke-cms/astro": "0.9.2"
26
29
  },
27
30
  "scripts": {
28
31
  "test": "echo \"Stub — no tests\""
package/src/index.ts CHANGED
@@ -1,17 +1,26 @@
1
1
  import type { AstroIntegration } from 'astro';
2
- import type { KaraokeConfig } from '@karaoke-cms/astro';
3
2
  import { fileURLToPath } from 'url';
4
3
 
5
4
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
6
5
 
7
- export default function themeDefault(_config: KaraokeConfig): AstroIntegration {
6
+ // Constructs the Astro integration for theme-default.
7
+ // Exported separately so it can be called by both the new factory (themeDefault)
8
+ // and the legacy default export without triggering a @karaoke-cms/astro import.
9
+ function buildIntegration(): AstroIntegration {
8
10
  return {
9
11
  name: '@karaoke-cms/theme-default',
10
12
  hooks: {
11
13
  'astro:config:setup': ({ injectRoute, updateConfig }) => {
14
+ // Blog routes are owned by @karaoke-cms/module-blog.
15
+ // theme-default injects them here so that the old string-based theme
16
+ // config (theme: '@karaoke-cms/theme-default') continues to work.
17
+ // When the new modules[] array config is fully adopted, theme-default
18
+ // will stop injecting these and let the core integration do it.
19
+ injectRoute({ pattern: '/blog', entrypoint: '@karaoke-cms/module-blog/pages/list' });
20
+ injectRoute({ pattern: '/blog/[slug]', entrypoint: '@karaoke-cms/module-blog/pages/post' });
21
+
22
+ // Docs, tags, and homepage remain owned by theme-default
12
23
  injectRoute({ pattern: '/', entrypoint: `${__dirname}pages/index.astro` });
13
- injectRoute({ pattern: '/blog', entrypoint: `${__dirname}pages/blog/index.astro` });
14
- injectRoute({ pattern: '/blog/[slug]', entrypoint: `${__dirname}pages/blog/[slug].astro` });
15
24
  injectRoute({ pattern: '/docs', entrypoint: `${__dirname}pages/docs/index.astro` });
16
25
  injectRoute({ pattern: '/docs/[slug]', entrypoint: `${__dirname}pages/docs/[slug].astro` });
17
26
  injectRoute({ pattern: '/tags', entrypoint: `${__dirname}pages/tags/index.astro` });
@@ -29,3 +38,36 @@ export default function themeDefault(_config: KaraokeConfig): AstroIntegration {
29
38
  },
30
39
  };
31
40
  }
41
+
42
+ /**
43
+ * New API: returns a ThemeInstance for use with defineConfig().
44
+ *
45
+ * Note: ThemeInstance is constructed manually here (without importing defineTheme
46
+ * from @karaoke-cms/astro) so that this file remains loadable via Node's native
47
+ * ESM when used through the legacy string-based theme config path.
48
+ *
49
+ * @example
50
+ * // karaoke.config.ts
51
+ * import { themeDefault } from '@karaoke-cms/theme-default';
52
+ * export default defineConfig({
53
+ * theme: themeDefault({ implements: [blog({ mount: '/blog' })] }),
54
+ * });
55
+ */
56
+ export function themeDefault(config: { implements?: Array<{ id: string }> } = {}) {
57
+ return {
58
+ _type: 'theme-instance' as const,
59
+ id: 'theme-default',
60
+ implementedModuleIds: (config.implements ?? []).map(m => m.id),
61
+ toAstroIntegration: () => buildIntegration(),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Legacy default export — supports the old string-based theme config:
67
+ * theme: '@karaoke-cms/theme-default'
68
+ * The core integration calls themeModule.default(config) when loading
69
+ * themes by package name string.
70
+ */
71
+ export default function themeDefaultLegacy(_config: unknown): AstroIntegration {
72
+ return buildIntegration();
73
+ }
@@ -0,0 +1,9 @@
1
+ /* theme-default implementation of the blog module CSS contract.
2
+ *
3
+ * These rules target the class names defined in @karaoke-cms/module-blog cssContract.
4
+ * Currently the blog-specific rules live in ../styles.css (shared with docs).
5
+ * A future pass will extract them fully here and remove the duplication.
6
+ *
7
+ * TODO: move .post-list, .post-header, .post-meta, .post-footer, .post-tags,
8
+ * .tag, .tag-list, .related-posts here and out of styles.css.
9
+ */
package/src/styles.css CHANGED
@@ -201,17 +201,47 @@ footer {
201
201
  .footer-inner {
202
202
  max-width: var(--width-site);
203
203
  margin: 0 auto;
204
- padding: var(--spacing-lg) var(--spacing-md);
204
+ padding: 0 var(--spacing-md);
205
+ }
206
+
207
+ .footer-grid {
208
+ display: grid;
209
+ grid-template-columns: repeat(4, 1fr);
210
+ gap: var(--spacing-lg);
211
+ padding: var(--spacing-xl) 0 var(--spacing-lg);
212
+ }
213
+
214
+ .footer-col {
215
+ min-width: 0;
216
+ }
217
+
218
+ .footer-brand {
219
+ font-weight: 700;
220
+ font-size: var(--font-size-sm);
221
+ color: var(--color-text);
222
+ margin: 0 0 var(--spacing-sm);
223
+ }
224
+
225
+ .footer-tagline {
226
+ font-size: var(--font-size-sm);
227
+ color: var(--color-muted);
228
+ margin: 0;
229
+ line-height: var(--line-height-body);
230
+ }
231
+
232
+ .footer-below {
233
+ border-top: 1px solid var(--color-border);
234
+ padding: var(--spacing-md) 0;
205
235
  display: flex;
206
236
  align-items: center;
207
237
  justify-content: space-between;
208
238
  gap: var(--spacing-md);
209
- color: var(--color-muted);
210
239
  font-size: var(--font-size-sm);
240
+ color: var(--color-muted);
211
241
  }
212
242
 
213
243
  .footer-attr {
214
- margin-left: auto;
244
+ flex-shrink: 0;
215
245
  }
216
246
 
217
247
  footer a {
@@ -223,6 +253,26 @@ footer a:hover {
223
253
  color: var(--color-link);
224
254
  }
225
255
 
256
+ /* Footer vertical menu — top-level entry as column heading */
257
+ .footer-col nav.karaoke-menu > ul > li > a {
258
+ font-size: var(--font-size-sm);
259
+ font-weight: 600;
260
+ color: var(--color-text);
261
+ display: block;
262
+ padding-bottom: var(--spacing-xs);
263
+ }
264
+
265
+ .footer-col nav.karaoke-menu > ul > li > ul a {
266
+ font-size: var(--font-size-sm);
267
+ color: var(--color-muted);
268
+ display: block;
269
+ padding: 2px 0;
270
+ }
271
+
272
+ .footer-col nav.karaoke-menu > ul > li > ul a:hover {
273
+ color: var(--color-link);
274
+ }
275
+
226
276
  /* --- Typography --- */
227
277
 
228
278
  h1, h2, h3, h4 {
@@ -378,14 +428,16 @@ pre code {
378
428
  width: 100%;
379
429
  }
380
430
 
381
- .footer-inner {
382
- flex-direction: column;
383
- align-items: flex-start;
384
- gap: var(--spacing-sm);
431
+ .footer-grid {
432
+ grid-template-columns: 1fr 1fr;
433
+ gap: var(--spacing-md);
434
+ padding: var(--spacing-lg) 0 var(--spacing-md);
385
435
  }
386
436
 
387
- .footer-attr {
388
- margin-left: 0;
437
+ .footer-below {
438
+ flex-direction: column;
439
+ align-items: flex-start;
440
+ gap: var(--spacing-xs);
389
441
  }
390
442
  }
391
443
 
@@ -1,66 +0,0 @@
1
- ---
2
- import { getCollection, render } from 'astro:content';
3
- import Base from '@karaoke-cms/astro/layouts/Base.astro';
4
- import ModuleLoader from '@karaoke-cms/astro/components/ModuleLoader.astro';
5
- import { siteTitle } from 'virtual:karaoke-cms/config';
6
-
7
- export async function getStaticPaths() {
8
- const posts = await getCollection('blog', ({ data }) => data.publish === true);
9
- return posts.map(entry => ({
10
- params: { slug: entry.id },
11
- props: { entry },
12
- }));
13
- }
14
-
15
- const { entry } = Astro.props;
16
- const { Content } = await render(entry);
17
-
18
- // Resolve related entries by ID across both collections
19
- const relatedIds = entry.data.related ?? [];
20
- const related = relatedIds.length > 0
21
- ? (await Promise.all([
22
- getCollection('blog', ({ data }) => data.publish === true),
23
- getCollection('docs', ({ data }) => data.publish === true),
24
- ]))
25
- .flat()
26
- .filter(e => relatedIds.includes(e.id))
27
- : [];
28
- ---
29
-
30
- <Base title={`${entry.data.title} — ${siteTitle}`} description={entry.data.description} type="article">
31
- <article>
32
- <div class="post-header">
33
- <h1>{entry.data.title}</h1>
34
- <div class="post-meta">
35
- {entry.data.date && <span>{entry.data.date.toISOString().slice(0, 10)}</span>}
36
- {entry.data.author && entry.data.date && <span> · </span>}
37
- {entry.data.author && (
38
- <span>{Array.isArray(entry.data.author) ? entry.data.author.join(' · ') : entry.data.author}</span>
39
- )}
40
- {entry.data.reading_time && <span> · {entry.data.reading_time} min read</span>}
41
- </div>
42
- </div>
43
- <div class="prose">
44
- <Content />
45
- </div>
46
- <div class="post-footer">
47
- {entry.data.tags && entry.data.tags.length > 0 && (
48
- <div class="post-tags">
49
- {entry.data.tags.map(tag => <a href={`/tags/${tag}`} class="tag">#{tag}</a>)}
50
- </div>
51
- )}
52
- {related.length > 0 && (
53
- <div class="related-posts">
54
- <p class="related-label">Related</p>
55
- <ul>
56
- {related.map(r => (
57
- <li><a href={`/${r.collection}/${r.id}`}>{r.data.title}</a></li>
58
- ))}
59
- </ul>
60
- </div>
61
- )}
62
- <a href="/blog">← Blog</a>
63
- </div>
64
- </article>
65
- <ModuleLoader comments={entry.data.comments} />
66
- </Base>
@@ -1,31 +0,0 @@
1
- ---
2
- import { getCollection } from 'astro:content';
3
- import Base from '@karaoke-cms/astro/layouts/Base.astro';
4
- import { siteTitle } from 'virtual:karaoke-cms/config';
5
-
6
- const posts = (await getCollection('blog', ({ data }) => data.publish === true))
7
- .sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0));
8
- ---
9
-
10
- <Base title={`Blog — ${siteTitle}`}>
11
- <div class="listing-header">
12
- <h1>Blog</h1>
13
- </div>
14
- {posts.length > 0 ? (
15
- <ul class="post-list">
16
- {posts.map(post => (
17
- <li>
18
- {post.data.date && (
19
- <span class="post-date">{post.data.date.toISOString().slice(0, 10)}</span>
20
- )}
21
- <a href={`/blog/${post.id}`}>{post.data.title}</a>
22
- </li>
23
- ))}
24
- </ul>
25
- ) : (
26
- <div class="empty-state">
27
- <p>No posts published yet.</p>
28
- <p>Create a Markdown file in your vault's <code>blog/</code> folder and set <code>publish: true</code> in the frontmatter to make it appear here.</p>
29
- </div>
30
- )}
31
- </Base>