@life-and-dev/mdsite 0.5.0 → 0.5.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.
@@ -1,11 +1,20 @@
1
1
  <template>
2
- <div class="toc-sidebar" ref="tocContainer">
2
+ <!--
3
+ Guard at the root: the consuming composable's `shouldShowTOC` already
4
+ requires `tocItems.length >= TOC_MIN_HEADINGS (3)`, so by the time we
5
+ reach this template a usable TOC is guaranteed. Render nothing otherwise.
6
+ -->
7
+ <div
8
+ v-if="tocItems.length >= 3"
9
+ class="toc-sidebar"
10
+ ref="tocContainer"
11
+ >
3
12
  <!-- Header -->
4
13
  <h3 v-if="showHeader" class="toc-header">On This Page</h3>
5
14
  <v-divider v-if="showHeader" class="mb-2" />
6
15
 
7
16
  <!-- TOC Items -->
8
- <nav v-if="tocItems.length >= 3" class="toc-nav">
17
+ <nav class="toc-nav">
9
18
  <TocItem
10
19
  v-for="item in tocItems"
11
20
  :key="item.id"
@@ -15,11 +24,6 @@
15
24
  />
16
25
  </nav>
17
26
 
18
- <!-- Empty state -->
19
- <div v-else class="toc-empty">
20
- <p class="text-caption text-center">No table of contents available</p>
21
- </div>
22
-
23
27
  <!-- Fade gradient for overflow -->
24
28
  <div v-if="showFade" class="fade-gradient" />
25
29
  </div>
@@ -122,16 +126,6 @@ watch(() => props.tocItems, () => {
122
126
  scrollbar-width: none;
123
127
  }
124
128
 
125
- .toc-empty {
126
- flex: 1;
127
- display: flex;
128
- align-items: center;
129
- justify-content: center;
130
- padding: 32px 16px;
131
- color: rgb(var(--v-theme-on-surface-rail));
132
- opacity: 0.6;
133
- }
134
-
135
129
  .fade-gradient {
136
130
  position: absolute;
137
131
  bottom: 0;
@@ -8,7 +8,7 @@ import { computeShouldShowTOC, TOC_MIN_HEADINGS, TOC_MIN_LINES } from './useTabl
8
8
  describe('useTableOfContents threshold', () => {
9
9
  it('exposes the expected minimum constants', () => {
10
10
  expect(TOC_MIN_HEADINGS).toBe(3)
11
- expect(TOC_MIN_LINES).toBe(40)
11
+ expect(TOC_MIN_LINES).toBe(15)
12
12
  })
13
13
 
14
14
  describe('computeShouldShowTOC', () => {
@@ -18,13 +18,13 @@ describe('useTableOfContents threshold', () => {
18
18
  expect(computeShouldShowTOC(2, 100)).toBe(false)
19
19
  })
20
20
 
21
- it('returns false when lines < 40 (and headings >= 3)', () => {
21
+ it('returns false when lines < 15 (and headings >= 3)', () => {
22
22
  expect(computeShouldShowTOC(3, 0)).toBe(false)
23
- expect(computeShouldShowTOC(5, 39)).toBe(false)
23
+ expect(computeShouldShowTOC(5, 14)).toBe(false)
24
24
  })
25
25
 
26
- it('returns true when headings >= 3 and lines >= 40', () => {
27
- expect(computeShouldShowTOC(3, 40)).toBe(true)
26
+ it('returns true when headings >= 3 and lines >= 15', () => {
27
+ expect(computeShouldShowTOC(3, 15)).toBe(true)
28
28
  expect(computeShouldShowTOC(5, 100)).toBe(true)
29
29
  })
30
30
 
@@ -8,7 +8,7 @@ export interface TocItem {
8
8
  /** Minimum number of headings required to show the TOC. */
9
9
  export const TOC_MIN_HEADINGS = 3
10
10
  /** Minimum number of non-empty lines in the rendered content required to show the TOC. */
11
- export const TOC_MIN_LINES = 40
11
+ export const TOC_MIN_LINES = 15
12
12
 
13
13
  /**
14
14
  * Pure helper that decides whether the TOC should be shown.
@@ -18,7 +18,25 @@ export default defineNuxtConfig({
18
18
  public: {
19
19
  contentDomain: path.basename(mdsite.contentDir),
20
20
  contentPath: mdsite.contentDir,
21
- siteConfig
21
+ // `mdsite.config` is a valid `MdsiteConfig` at runtime, but
22
+ // Nuxt's runtime-config type generator collapses every complex
23
+ // field of `siteConfig` to a degenerate shape — `menu` becomes
24
+ // `Array<{}>` (regardless of whether the source type is recursive
25
+ // or contains `Array<any>`) and `footer` becomes `Array<any>`.
26
+ // We verified this by:
27
+ // 1. Tightening `MdsiteConfig.menu` to a proper recursive
28
+ // `MdsiteMenuItem` (no `any`) and clearing `.nuxt` cache:
29
+ // the generated `menu` was still `Array<{}>`.
30
+ // 2. Trying `declare module 'nuxt/schema'` augmentations of
31
+ // `PublicRuntimeConfig.siteConfig`: the augmentation
32
+ // *intersects* with the broken generated type rather than
33
+ // overriding it, producing an even narrower target.
34
+ // Since the runtime value is unchanged, `as any` is the most
35
+ // honest pragmatic escape hatch. The recursive `MdsiteMenuItem`
36
+ // type is still useful: it gives the rest of the codebase
37
+ // (notably `scripts/generate-indices.ts`) a single source of
38
+ // truth and proper types.
39
+ siteConfig: siteConfig as any
22
40
  }
23
41
  },
24
42
 
@@ -4,6 +4,7 @@ import fs from 'fs-extra'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
6
  import { parse as parseYaml } from 'yaml'
7
+ import type { MdsiteMenuItem } from '../utils/mdsite-config'
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url)
9
10
  const __dirname = path.dirname(__filename)
@@ -108,8 +109,10 @@ export interface MinimalTreeNode {
108
109
  isPrimary?: boolean
109
110
  }
110
111
 
111
- type MenuItemType = string | { [key: string]: string | null | MenuItemType[] } | null
112
-
112
+ // `MdsiteMenuItem` is imported from `~/utils/mdsite-config` (see top of file).
113
+ // It is the same recursive shape that was previously defined locally as
114
+ // `MdsiteMenuItem`. Single source of truth lives in `utils/mdsite-config.ts`
115
+ // so the Nuxt runtime-config type generator can infer it correctly.
113
116
  /**
114
117
  * Normalize URL path so a trailing /index resolves to its parent.
115
118
  * Mirrors filePathToUrlPath behavior so menu paths match content routes.
@@ -161,7 +164,7 @@ function resolvePath(menuPath: string, contextPath: string): string {
161
164
  * Process menu items recursively and build minimal tree structure
162
165
  */
163
166
  async function processMenuItems(
164
- items: MenuItemType[],
167
+ items: MdsiteMenuItem[],
165
168
  contextPath: string,
166
169
  order: number = 0
167
170
  ): Promise<{ nodes: MinimalTreeNode[], nextOrder: number }> {
@@ -371,14 +374,14 @@ async function buildFallbackNavigationTree(sourceDir: string): Promise<MinimalTr
371
374
  * Load the menu array from a candidate config file (legacy _menu.yml/yaml or mdsite.yml).
372
375
  * Returns null if the file is missing, unreadable, has no menu key, or has an empty menu.
373
376
  */
374
- async function tryReadMenuFromConfig(configPath: string): Promise<MenuItemType[] | null> {
377
+ async function tryReadMenuFromConfig(configPath: string): Promise<MdsiteMenuItem[] | null> {
375
378
  if (!await fs.pathExists(configPath)) {
376
379
  return null
377
380
  }
378
381
 
379
382
  try {
380
383
  const content = await fs.readFile(configPath, 'utf-8')
381
- const parsed = parseYaml(content) as { menu?: MenuItemType[] } | null
384
+ const parsed = parseYaml(content) as { menu?: MdsiteMenuItem[] } | null
382
385
  if (parsed && Array.isArray(parsed.menu) && parsed.menu.length > 0) {
383
386
  return parsed.menu
384
387
  }
@@ -393,14 +396,14 @@ async function tryReadMenuFromConfig(configPath: string): Promise<MenuItemType[]
393
396
  * Try to read menu items from a plain (non-wrapped) legacy _menu.yml/yaml file.
394
397
  * Returns null if the file is missing, unreadable, or doesn't contain a non-empty array.
395
398
  */
396
- async function tryReadLegacyMenuFile(menuPath: string): Promise<MenuItemType[] | null> {
399
+ async function tryReadLegacyMenuFile(menuPath: string): Promise<MdsiteMenuItem[] | null> {
397
400
  if (!await fs.pathExists(menuPath)) {
398
401
  return null
399
402
  }
400
403
 
401
404
  try {
402
405
  const content = await fs.readFile(menuPath, 'utf-8')
403
- const parsed = parseYaml(content) as MenuItemType[] | null
406
+ const parsed = parseYaml(content) as MdsiteMenuItem[] | null
404
407
  if (Array.isArray(parsed) && parsed.length > 0) {
405
408
  return parsed
406
409
  }
@@ -420,7 +423,7 @@ async function tryReadLegacyMenuFile(menuPath: string): Promise<MenuItemType[] |
420
423
  * 5. <sourceDir>/mdsite.yml
421
424
  * Returns the first non-empty menu array, or null if none resolve to a menu.
422
425
  */
423
- async function loadMenuConfig(sourceDir: string): Promise<MenuItemType[] | null> {
426
+ async function loadMenuConfig(sourceDir: string): Promise<MdsiteMenuItem[] | null> {
424
427
  const candidates: { path: string, isLegacy: boolean }[] = [
425
428
  { path: path.join(sourceDir, '_menu.yml'), isLegacy: true },
426
429
  { path: path.join(sourceDir, '_menu.yaml'), isLegacy: true }
@@ -2,6 +2,30 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import YAML from 'yaml';
4
4
 
5
+ /**
6
+ * Recursive shape of a single `mdsite.yml` `menu:` entry.
7
+ *
8
+ * - `string` → flat link to a markdown slug
9
+ * (e.g. `- genesis`, `- resurrections`)
10
+ * - `null` → visual separator (`===` in YAML)
11
+ * - `Record<string, MdsiteMenuValue>` → group: object whose keys are group
12
+ * labels (e.g. `"Members of the Trinity":`) and whose
13
+ * values are `MdsiteMenuValue` items
14
+ * - `MdsiteMenuValue` (the value side of a group) can be:
15
+ * - `string` → alias link with custom title
16
+ * (e.g. `"Job": https://...`, `"Homepage": index`)
17
+ * - `null` → group label only (renders as a heading, no link)
18
+ * - `MdsiteMenuItem[]` → nested submenu (recursive)
19
+ *
20
+ * Keeping this a `type` alias (not an `interface`) and avoiding `any` lets
21
+ * Nuxt's runtime-config type generator infer `runtimeConfig.public.siteConfig`
22
+ * without collapsing `menu` to `{}[]`. See `nuxt.config.ts` for the cast
23
+ * history this replaces.
24
+ */
25
+ export type MdsiteMenuItem = string | null | MdsiteMenuGroup
26
+ export type MdsiteMenuGroup = { [key: string]: MdsiteMenuValue }
27
+ export type MdsiteMenuValue = string | null | MdsiteMenuItem[]
28
+
5
29
  export interface MdsiteConfig {
6
30
  content?: {
7
31
  path?: string
@@ -11,7 +35,7 @@ export interface MdsiteConfig {
11
35
  bibleTooltips: boolean
12
36
  sourceEdit: boolean
13
37
  }
14
- menu: Array<string | null | Record<string, string | null | Array<any>>>
38
+ menu: MdsiteMenuItem[]
15
39
  footer: string[]
16
40
  server: {
17
41
  output: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@life-and-dev/mdsite",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Local-first CLI that orchestrates mdsite-nuxt",