@meistrari/tela-build 1.65.0 → 1.65.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,10 +1,10 @@
1
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { dirname, join, resolve } from 'pathe'
5
5
  import matter from 'gray-matter'
6
6
  import { afterEach, describe, expect, it } from 'vitest'
7
- import { canonicalizeTypeString, compareCanonical, generateDocsToDirectory, resolveSiblingMdx } from '../doc-generator'
7
+ import { canonicalizeTypeString, compareCanonical, generateDocsToDirectory, generateGroupSlug, generateTagName, parseMdxFile, resolveSiblingMdx } from '../doc-generator'
8
8
  import type { ComponentDoc } from '../doc-generator'
9
9
  import { TypeResolver } from '../type-resolver'
10
10
 
@@ -82,6 +82,73 @@ describe('generateDocsToDirectory', () => {
82
82
 
83
83
  expect(packageJson.files).toContain('docs')
84
84
  })
85
+
86
+ it('groups nested components by their source family', () => {
87
+ const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
88
+ tempDirs.push(outDir)
89
+
90
+ const makeComponent = (tagName: string, directory: string, path: string): ComponentDoc => ({
91
+ name: tagName,
92
+ tagName,
93
+ path,
94
+ directory,
95
+ description: '',
96
+ props: [],
97
+ events: [],
98
+ slots: [],
99
+ })
100
+ const components = [
101
+ makeComponent('TelaComplexTable', 'tela/complex-table', 'components/tela/complex-table/complex-table.vue'),
102
+ makeComponent('TelaComplexTableHeader', 'tela/complex-table/header', 'components/tela/complex-table/header/header.vue'),
103
+ makeComponent('TelaComplexBadge', 'tela/badge', 'components/tela/badge/complex-badge.vue'),
104
+ ]
105
+
106
+ generateDocsToDirectory(components.reverse(), new TypeResolver(outDir), outDir)
107
+
108
+ const skillMd = readFileSync(join(outDir, 'tela-build', 'SKILL.md'), 'utf-8')
109
+ const componentPage = readFileSync(join(outDir, 'tela-build', 'components', 'tela-complex-table.md'), 'utf-8')
110
+ const badgePage = readFileSync(join(outDir, 'tela-build', 'components', 'tela-badge.md'), 'utf-8')
111
+
112
+ expect(skillMd).toContain('[TelaComplexTable](components/tela-complex-table.md)')
113
+ expect(componentPage).toContain('## TelaComplexTable')
114
+ expect(componentPage).toContain('## TelaComplexTableHeader')
115
+ expect(badgePage).toContain('## TelaComplexBadge')
116
+ expect(existsSync(join(outDir, 'tela-build', 'components', 'tela-complex.md'))).toBe(false)
117
+ })
118
+ })
119
+
120
+ describe('nuxt-compatible component names', () => {
121
+ it.each([
122
+ ['metrics-card', 'tela/home', 'TelaHomeMetricsCard'],
123
+ ['hero-metrics-card', 'tela/details/hero', 'TelaDetailsHeroMetricsCard'],
124
+ ['index', 'tela/chat', 'TelaChat'],
125
+ ['index', 'tela/chat/command/mention', 'TelaChatCommandMention'],
126
+ ['sheet.client', 'tela/sheet', 'TelaSheet'],
127
+ ['sheet.server', 'tela/sheet', 'TelaSheet'],
128
+ ['avatar.global', 'tela/avatar', 'TelaAvatar'],
129
+ ['index.client', 'tela/chat', 'TelaChat'],
130
+ ['Item', 'tela/item/item', 'TelaItem'],
131
+ ['DropdownMenuSeparator', 'tela/dropdown-menu', 'TelaDropdownMenuSeparator'],
132
+ ['button', '', 'Button'],
133
+ ])('resolves %s in %s as %s', (fileName, directory, expected) => {
134
+ expect(generateTagName(fileName, directory)).toBe(expected)
135
+ })
136
+
137
+ it.each([
138
+ ['tela/DropdownMenu', 'tela-dropdown-menu'],
139
+ ['tela\\DropdownMenu', 'tela-dropdown-menu'],
140
+ ])('normalizes source family %s as %s', (directory, expected) => {
141
+ expect(generateGroupSlug({
142
+ name: 'TelaDropdownMenu',
143
+ tagName: 'TelaDropdownMenu',
144
+ path: 'components/tela/dropdown-menu/dropdown-menu.vue',
145
+ directory,
146
+ description: '',
147
+ props: [],
148
+ events: [],
149
+ slots: [],
150
+ })).toBe(expected)
151
+ })
85
152
  })
86
153
 
87
154
  // The extractors emit members in type-resolution order, which differs between
@@ -114,6 +181,35 @@ describe('resolveSiblingMdx', () => {
114
181
  expect(resolveSiblingMdx(join(dir, 'Collapsible.vue'))).toBe(join(dir, 'collapsible.mdx'))
115
182
  })
116
183
 
184
+ it('resolves an index component to its family MDX', () => {
185
+ const root = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
186
+ const dir = join(root, 'chat')
187
+ tempDirs.push(root)
188
+ mkdirSync(dir)
189
+ writeFileSync(join(dir, 'index.vue'), '')
190
+ writeFileSync(join(dir, 'chat.mdx'), '')
191
+
192
+ expect(resolveSiblingMdx(join(dir, 'index.vue'))).toBe(join(dir, 'chat.mdx'))
193
+ })
194
+
195
+ it('resolves PascalCase component files to kebab-case MDX', () => {
196
+ const dir = makeComponentDir(['DropdownMenu.vue', 'dropdown-menu.mdx'])
197
+
198
+ expect(resolveSiblingMdx(join(dir, 'DropdownMenu.vue'))).toBe(join(dir, 'dropdown-menu.mdx'))
199
+ })
200
+
201
+ it('resolves Nuxt mode-suffixed components to unsuffixed MDX', () => {
202
+ const dir = makeComponentDir(['sheet.client.vue', 'sheet.mdx'])
203
+
204
+ expect(resolveSiblingMdx(join(dir, 'sheet.client.vue'))).toBe(join(dir, 'sheet.mdx'))
205
+ })
206
+
207
+ it('prefers an exact component MDX over normalized candidates', () => {
208
+ const dir = makeComponentDir(['DropdownMenu.vue', 'DropdownMenu.mdx', 'dropdown-menu.mdx'])
209
+
210
+ expect(resolveSiblingMdx(join(dir, 'DropdownMenu.vue'))).toBe(join(dir, 'DropdownMenu.mdx'))
211
+ })
212
+
117
213
  it('returns undefined when the component has no mdx', () => {
118
214
  const dir = makeComponentDir(['CollapsibleTrigger.vue', 'collapsible.mdx'])
119
215
 
@@ -125,6 +221,238 @@ describe('resolveSiblingMdx', () => {
125
221
  })
126
222
  })
127
223
 
224
+ describe('authored MDX guidance', () => {
225
+ it('uses the MDX structure to preserve guidance and remove Storybook controls and authored API', async () => {
226
+ const mdxDir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
227
+ const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
228
+ tempDirs.push(mdxDir, outDir)
229
+
230
+ const mdxPath = join(mdxDir, 'sample.mdx')
231
+ const mdx = [
232
+ 'import { Meta, Canvas as Preview, ArgTypes, Controls } from \'@storybook/blocks\'',
233
+ 'import * as Blocks from \'@storybook/blocks\'',
234
+ 'import Storybook from \'@storybook/blocks\'',
235
+ 'import Note from \'./note.vue\'',
236
+ 'export const Template = () => <TelaBadge />',
237
+ '',
238
+ '# TelaSample',
239
+ '',
240
+ 'Content after a JSX export remains.',
241
+ '',
242
+ '<Meta title="Sample" />',
243
+ '',
244
+ '## Rules',
245
+ '',
246
+ '- Prefer the canonical component.',
247
+ '',
248
+ '<Note>Keep non-Storybook JSX.</Note>',
249
+ '',
250
+ '### Interactive preview',
251
+ '',
252
+ '<Preview',
253
+ ' parameters={{ format: value => value > 0 ? \'/>\' : \'>\' }}',
254
+ '>',
255
+ '```vue',
256
+ '<TelaStorybookOnly />',
257
+ '```',
258
+ '</Preview>',
259
+ '',
260
+ 'Before <Controls /> after.',
261
+ '<Blocks.Source />',
262
+ '<Storybook.Primary />',
263
+ '<Storybook />',
264
+ '',
265
+ '### Basic usage',
266
+ '',
267
+ '```vue',
268
+ '<TelaSample />',
269
+ '```',
270
+ '',
271
+ '### Literal Storybook syntax',
272
+ '',
273
+ '~~~mdx',
274
+ '<Meta title="literal" />',
275
+ '~~~',
276
+ '',
277
+ '| Purpose | Component |',
278
+ '| --- | --- |',
279
+ '| Sample | TelaSample |',
280
+ '',
281
+ '## Props',
282
+ '',
283
+ 'Stale authored props.',
284
+ '',
285
+ '### Nested prop notes',
286
+ '',
287
+ 'These must be removed with Props.',
288
+ '',
289
+ '## Slots',
290
+ '',
291
+ 'The leading slot receives row context.',
292
+ '',
293
+ '## Events',
294
+ '',
295
+ 'The update event fires after validation.',
296
+ '',
297
+ '## Emits',
298
+ '',
299
+ 'Catch errors from async parent handlers.',
300
+ '',
301
+ '## API',
302
+ '',
303
+ '<ArgTypes of={SampleStories} />',
304
+ '',
305
+ '## Virtualization Props',
306
+ '',
307
+ 'Keep semantic prop guidance.',
308
+ '',
309
+ '## Size Prop',
310
+ '',
311
+ 'Keep focused size guidance.',
312
+ '',
313
+ ].join('\n')
314
+ writeFileSync(mdxPath, mdx, 'utf-8')
315
+
316
+ const stories = await parseMdxFile(mdxPath)
317
+ const component: ComponentDoc = {
318
+ name: 'TelaSample',
319
+ tagName: 'TelaSample',
320
+ path: 'components/tela/sample.vue',
321
+ directory: 'tela',
322
+ description: '',
323
+ props: [{ name: 'variant', required: false, type: 'string' }],
324
+ events: [{ name: 'update', type: 'string' }],
325
+ slots: [{ name: 'default', description: 'Sample content' }],
326
+ stories,
327
+ }
328
+
329
+ generateDocsToDirectory([component], new TypeResolver(outDir), outDir)
330
+ const page = readFileSync(join(outDir, 'tela-build', 'components', 'tela-sample.md'), 'utf-8')
331
+
332
+ expect(stories?.examples).toEqual([{ name: 'Basic usage', code: '<TelaSample />' }])
333
+ expect(page).toContain('### Usage guidance')
334
+ expect(page).toContain('Content after a JSX export remains.')
335
+ expect(page).toContain('#### Rules')
336
+ expect(page).toContain('- Prefer the canonical component.')
337
+ expect(page).toContain('<Note>Keep non-Storybook JSX.</Note>')
338
+ expect(page).toContain('<Meta title="literal" />')
339
+ expect(page).toMatch(/\| Purpose\s+\| Component\s+\|/)
340
+ expect(page).toContain('#### Virtualization Props')
341
+ expect(page).toContain('Keep semantic prop guidance.')
342
+ expect(page).toContain('#### Size Prop')
343
+ expect(page).toContain('Keep focused size guidance.')
344
+ expect(page.match(/<TelaSample \/>/g)).toHaveLength(1)
345
+ expect(page.match(/^### Props$/gm)).toHaveLength(1)
346
+ expect(page.match(/^### Events$/gm)).toHaveLength(1)
347
+ expect(page.match(/^### Event guidance$/gm)).toHaveLength(1)
348
+ expect(page).toContain('The update event fires after validation.')
349
+ expect(page).toContain('Catch errors from async parent handlers.')
350
+ expect(page.match(/^### Slots$/gm)).toHaveLength(1)
351
+ expect(page.match(/^### Slot guidance$/gm)).toHaveLength(1)
352
+ expect(page).toContain('The leading slot receives row context.')
353
+ expect(page).not.toContain('@storybook/blocks')
354
+ expect(page).not.toContain('<TelaStorybookOnly />')
355
+ expect(page).not.toContain('<Preview')
356
+ expect(page).not.toContain('<Controls')
357
+ expect(page).not.toContain('<Blocks.Source')
358
+ expect(page).not.toContain('<Storybook')
359
+ expect(page).toMatch(/Before\s+after\./)
360
+ expect(page).not.toContain('Stale authored props.')
361
+ expect(page).not.toContain('Nested prop notes')
362
+ expect(page).not.toContain('<ArgTypes')
363
+ expect(page.match(/^# TelaSample$/gm)).toHaveLength(1)
364
+ })
365
+
366
+ it('preserves authored event and slot semantics when extracted metadata is incomplete', async () => {
367
+ const mdxDir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
368
+ const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
369
+ tempDirs.push(mdxDir, outDir)
370
+ const mdxPath = join(mdxDir, 'sample.mdx')
371
+ writeFileSync(mdxPath, [
372
+ '# TelaSample',
373
+ '',
374
+ '## Events',
375
+ '',
376
+ '`loadMore` requires errors from async parent handlers to be caught.',
377
+ '',
378
+ '## Slots',
379
+ '',
380
+ '`leading` receives row data and loading state.',
381
+ '',
382
+ '```vue',
383
+ '<TelaSample />',
384
+ '```',
385
+ ].join('\n'))
386
+
387
+ const stories = await parseMdxFile(mdxPath)
388
+ const component: ComponentDoc = {
389
+ name: 'TelaSample',
390
+ tagName: 'TelaSample',
391
+ path: 'components/tela/sample.vue',
392
+ directory: 'tela',
393
+ description: '',
394
+ props: [],
395
+ events: [],
396
+ slots: [{ name: 'leading', description: '' }],
397
+ stories,
398
+ }
399
+ generateDocsToDirectory([component], new TypeResolver(outDir), outDir)
400
+ const page = readFileSync(join(outDir, 'tela-build', 'components', 'tela-sample.md'), 'utf-8')
401
+
402
+ expect(page).not.toContain('### Events\n')
403
+ expect(page).toContain('### Event guidance')
404
+ expect(page).toContain('requires errors from async parent handlers to be caught')
405
+ expect(page).toContain('### Slots')
406
+ expect(page).toContain('| `leading` | — | |')
407
+ expect(page).toContain('### Slot guidance')
408
+ expect(page).toContain('receives row data and loading state')
409
+ expect(page.match(/<TelaSample \/>/g)).toHaveLength(1)
410
+ expect(page).not.toContain('### Examples')
411
+ expect(stories?.examples).toEqual([{ name: 'Slots', code: '<TelaSample />' }])
412
+ })
413
+
414
+ it('does not restore fenced examples removed with a Storybook subtree', async () => {
415
+ const dir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
416
+ tempDirs.push(dir)
417
+ const mdxPath = join(dir, 'sample.mdx')
418
+ writeFileSync(mdxPath, [
419
+ 'import { Canvas } from \'@storybook/blocks\'',
420
+ '',
421
+ '# TelaSample',
422
+ '',
423
+ '## Interactive preview',
424
+ '',
425
+ '<Canvas>',
426
+ '```vue',
427
+ '<TelaStorybookOnly />',
428
+ '```',
429
+ '</Canvas>',
430
+ ].join('\n'))
431
+
432
+ const stories = await parseMdxFile(mdxPath)
433
+
434
+ expect(stories?.content).toBe('')
435
+ expect(stories?.examples).toEqual([])
436
+ })
437
+
438
+ it('preserves similarly named JSX not imported from Storybook', async () => {
439
+ const dir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
440
+ tempDirs.push(dir)
441
+ const mdxPath = join(dir, 'sample.mdx')
442
+ writeFileSync(mdxPath, [
443
+ 'import Canvas from \'./canvas.vue\'',
444
+ '',
445
+ '# TelaSample',
446
+ '',
447
+ '<Canvas>Keep this content.</Canvas>',
448
+ ].join('\n'))
449
+
450
+ const stories = await parseMdxFile(mdxPath)
451
+
452
+ expect(stories?.content).toContain('<Canvas>Keep this content.</Canvas>')
453
+ })
454
+ })
455
+
128
456
  describe('canonical ordering', () => {
129
457
  function makeDoc(overrides: Partial<ComponentDoc> = {}): ComponentDoc {
130
458
  return {
@@ -2,6 +2,15 @@
2
2
  import { join, resolve, relative, dirname, basename } from 'pathe'
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs'
4
4
  import { parse as parseVue } from 'vue-docgen-api'
5
+ import { pascalCase, splitByCase } from 'scule'
6
+ import ts from 'typescript'
7
+ import { unified } from 'unified'
8
+ import remarkParse from 'remark-parse'
9
+ import remarkMdx from 'remark-mdx'
10
+ import remarkGfm from 'remark-gfm'
11
+ import remarkStringify from 'remark-stringify'
12
+ import type { Code, Heading, Root, RootContent } from 'mdast'
13
+ import type { MdxJsxFlowElement, MdxJsxTextElement, MdxjsEsm } from 'mdast-util-mdx'
5
14
  // Load glob dynamically for compatibility across versions (v8 vs v11)
6
15
  import { getTypeResolver } from './type-resolver'
7
16
  import type { TypeResolver } from './type-resolver'
@@ -29,9 +38,17 @@ export interface ComponentDoc {
29
38
  parseError?: string
30
39
  }
31
40
 
41
+ export interface StoryExample {
42
+ name: string
43
+ code: string
44
+ }
45
+
32
46
  export interface StoryDoc {
33
47
  title: string
34
- examples: any[]
48
+ content: string
49
+ eventGuidance: string
50
+ slotGuidance: string
51
+ examples: StoryExample[]
35
52
  argTypes: any
36
53
  }
37
54
 
@@ -285,8 +302,8 @@ export async function collectComponentDocs(layerPath: string, appRootDir?: strin
285
302
  const fileName = basename(file, '.vue')
286
303
  const directory = relative(componentPath, dirname(file))
287
304
 
288
- // Generate component tag name
289
- const tagName = generateTagName(fileName)
305
+ // Match Nuxt's path-aware component auto-import name.
306
+ const tagName = generateTagName(fileName, directory)
290
307
 
291
308
  const componentDoc: ComponentDoc = {
292
309
  name: info.displayName || info.exportName || fileName,
@@ -355,99 +372,120 @@ async function globAsync(pattern: string, options: any): Promise<string[]> {
355
372
  return []
356
373
  }
357
374
 
358
- export function generateTagName(fileName: string): string {
359
- // Convert file name to PascalCase tag name
360
- // Components in tela/ directory should preserve the Tela prefix
361
- // e.g., input -> TelaInput, button -> TelaButton
375
+ const COMPONENT_MODE_REPLACEMENT = /(?:\.(?:client|server))?(?:\.global|\.island)*$/
362
376
 
363
- let tagName: string
377
+ /**
378
+ * Mirror Nuxt's directory-aware component naming. Directory segments prefix the
379
+ * file name, repeated suffixes are collapsed, `index.vue` resolves to its
380
+ * directory, and mode suffixes such as `.client.vue` are not part of the tag.
381
+ */
382
+ export function generateTagName(fileName: string, directory: string): string {
383
+ const prefixParts = splitByCase(directory)
384
+ const normalizedFileName = fileName
385
+ .replace(COMPONENT_MODE_REPLACEMENT, '')
386
+ .replace(/^index$/i, '')
387
+ const fileNameParts = splitByCase(normalizedFileName)
388
+ const fileNamePartsContent = fileNameParts.join('/').toLowerCase()
389
+ const componentNameParts = prefixParts.flatMap(part => splitByCase(part))
390
+
391
+ let index = prefixParts.length - 1
392
+ const matchedSuffix: string[] = []
393
+ while (index >= 0) {
394
+ const prefixPart = prefixParts[index]
395
+ if (!prefixPart) {
396
+ index--
397
+ continue
398
+ }
364
399
 
365
- // Check if the file already starts with 'tela-'
366
- if (fileName.startsWith('tela-')) {
367
- // Already has tela prefix, just convert to PascalCase
368
- const parts = fileName.split('-')
369
- tagName = parts.map(part =>
370
- part.charAt(0).toUpperCase() + part.slice(1),
371
- ).join('')
372
- }
373
- else {
374
- // Add Tela prefix for components without it
375
- const parts = fileName.split('-')
376
- const componentName = parts.map(part =>
377
- part.charAt(0).toUpperCase() + part.slice(1),
378
- ).join('')
379
- tagName = `Tela${componentName}`
400
+ matchedSuffix.unshift(...splitByCase(prefixPart).map(part => part.toLowerCase()))
401
+ const matchedSuffixContent = matchedSuffix.join('/')
402
+ const repeatedDirectory = prefixPart.toLowerCase() === fileNamePartsContent
403
+ && prefixParts[index + 1]
404
+ && prefixParts[index] === prefixParts[index + 1]
405
+
406
+ if (
407
+ fileNamePartsContent === matchedSuffixContent
408
+ || fileNamePartsContent.startsWith(`${matchedSuffixContent}/`)
409
+ || repeatedDirectory
410
+ ) {
411
+ componentNameParts.length = index
412
+ }
413
+ index--
380
414
  }
381
415
 
382
- return tagName
416
+ return pascalCase([...componentNameParts, ...fileNameParts])
383
417
  }
384
418
 
385
419
  /**
386
- * Find the `.mdx` next to a component, matching the way the filesystem does
387
- * rather than the way one particular filesystem does.
388
- *
389
- * `components/tela/collapsible/Collapsible.vue` is documented by
390
- * `collapsible.mdx` — the casings differ. A plain `existsSync()` on the derived
391
- * `Collapsible.mdx` is true on macOS (case-insensitive APFS quietly resolves it)
392
- * and false on Linux, so CI generated that component with its examples missing
393
- * and reported the committed page as drift. Resolve through the directory
394
- * listing instead, so both platforms find the same file — and pick the
395
- * lowest-sorting candidate, because a case-sensitive filesystem is free to hold
396
- * several files that differ only in case.
420
+ * Resolve the authored MDX associated with a component using the same filename
421
+ * normalization as Nuxt component discovery. Family docs commonly use the
422
+ * directory name for `index.vue`, kebab-case for PascalCase component files,
423
+ * and omit `.client` / `.server` mode suffixes.
397
424
  */
398
425
  export function resolveSiblingMdx(vueFile: string): string | undefined {
399
- const mdxPath = vueFile.replace(/\.vue$/, '.mdx')
400
- const directory = dirname(mdxPath)
426
+ const directory = dirname(vueFile)
401
427
  if (!existsSync(directory)) {
402
428
  return undefined
403
429
  }
404
430
 
405
- // Resolved against the listing rather than existsSync() on both branches:
406
- // an exact-path probe reports a hit on macOS for a name that is not on disk,
407
- // which would put the derived casing into the output on one platform and the
408
- // real filename on the other.
409
- const wanted = basename(mdxPath)
431
+ const fileName = basename(vueFile, '.vue')
432
+ const normalizedFileName = fileName.replace(COMPONENT_MODE_REPLACEMENT, '')
433
+ const normalizedCandidates = normalizedFileName.toLowerCase() === 'index'
434
+ ? [toKebabFromTag(basename(directory)), normalizedFileName, fileName]
435
+ : [fileName, normalizedFileName, toKebabFromTag(normalizedFileName)]
436
+ const candidates = [...new Set(normalizedCandidates.map(candidate => `${candidate}.mdx`))]
410
437
  const entries = readdirSync(directory).sort(compareCanonical)
411
- const match = entries.find(entry => entry === wanted)
412
- ?? entries.find(entry => entry.toLowerCase() === wanted.toLowerCase())
438
+
439
+ // Check every normalized candidate exactly before trying case-insensitive
440
+ // matches, so a fuzzy match cannot beat a deterministic Nuxt-style name.
441
+ const exactMatch = candidates
442
+ .map(candidate => entries.find(entry => entry === candidate))
443
+ .find(Boolean)
444
+ const caseInsensitiveMatch = candidates
445
+ .map(candidate => entries.find(entry => entry.toLowerCase() === candidate.toLowerCase()))
446
+ .find(Boolean)
447
+ const match = exactMatch ?? caseInsensitiveMatch
413
448
 
414
449
  return match ? join(directory, match) : undefined
415
450
  }
416
451
 
417
- export async function parseMdxFile(mdxPath: string): Promise<StoryDoc | undefined> {
418
- try {
419
- const content = readFileSync(mdxPath, 'utf-8')
420
-
421
- // Extract title from the main heading
422
- const titleMatch = content.match(/^#\s+(.+)$/m)
423
- const title = titleMatch?.[1] ?? ''
424
-
425
- // Extract code examples from ```vue blocks with better heading detection
426
- const examples: any[] = []
427
- const codeBlockRegex = /```vue\n([\s\S]*?)\n```/g
428
- const matches = Array.from(content.matchAll(codeBlockRegex))
429
-
430
- for (let i = 0; i < matches.length; i++) {
431
- const match = matches[i]
432
- const codeContent = match?.[1]?.trim()
433
- const codeBlockStart = match?.index
434
- if (!codeContent || codeBlockStart === undefined) {
435
- continue
436
- }
452
+ type MdxContent = RootContent | MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm
453
+ type MdxRoot = Omit<Root, 'children'> & { children: MdxContent[] }
454
+ type MdxParent = MdxContent & { children: MdxContent[] }
437
455
 
438
- // Find the closest heading before this code block
439
- const beforeCode = content.substring(0, codeBlockStart)
440
- const exampleName = findClosestHeading(beforeCode, i + 1)
456
+ interface StorybookBindings {
457
+ names: Set<string>
458
+ namespaces: Set<string>
459
+ }
441
460
 
442
- examples.push({
443
- name: exampleName,
444
- code: codeContent,
445
- })
446
- }
461
+ interface SanitizedMdx {
462
+ title: string
463
+ content: string
464
+ eventGuidance: string
465
+ slotGuidance: string
466
+ examples: StoryExample[]
467
+ }
468
+
469
+ const AUTHORED_API_SECTIONS = new Set(['api', 'emits', 'events', 'props', 'slots'])
470
+ const mdxProcessor = unified()
471
+ .use(remarkParse)
472
+ .use(remarkMdx)
473
+ .use(remarkGfm)
474
+ .use(remarkStringify, {
475
+ bullet: '-',
476
+ fences: true,
477
+ })
478
+
479
+ export async function parseMdxFile(mdxPath: string): Promise<StoryDoc | undefined> {
480
+ try {
481
+ const sanitized = sanitizeMdxContent(readFileSync(mdxPath, 'utf-8'))
447
482
 
448
483
  return {
449
- title,
450
- examples,
484
+ title: sanitized.title,
485
+ content: sanitized.content,
486
+ eventGuidance: sanitized.eventGuidance,
487
+ slotGuidance: sanitized.slotGuidance,
488
+ examples: sanitized.examples,
451
489
  argTypes: '',
452
490
  }
453
491
  }
@@ -457,39 +495,264 @@ export async function parseMdxFile(mdxPath: string): Promise<StoryDoc | undefine
457
495
  }
458
496
  }
459
497
 
460
- function findClosestHeading(textBeforeCode: string, exampleIndex: number): string {
461
- // Split into lines and work backwards to find the closest heading
462
- const lines = textBeforeCode.split('\n')
498
+ function isMdxEsm(node: MdxContent): node is MdxjsEsm {
499
+ return node.type === 'mdxjsEsm'
500
+ }
501
+
502
+ function isMdxJsxElement(node: MdxContent): node is MdxJsxFlowElement | MdxJsxTextElement {
503
+ return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement'
504
+ }
505
+
506
+ function isHeading(node: MdxContent): node is Heading {
507
+ return node.type === 'heading'
508
+ }
509
+
510
+ function isCode(node: MdxContent): node is Code {
511
+ return node.type === 'code'
512
+ }
513
+
514
+ function hasChildren(node: MdxContent): node is MdxParent {
515
+ return 'children' in node && Array.isArray(node.children)
516
+ }
517
+
518
+ function readNodeText(node: MdxContent): string {
519
+ if ('value' in node && typeof node.value === 'string')
520
+ return node.value
521
+ if (!hasChildren(node))
522
+ return ''
523
+ return node.children.map(readNodeText).join('')
524
+ }
463
525
 
464
- // Look for headings starting from the end (closest to code block)
465
- for (let i = lines.length - 1; i >= 0; i--) {
466
- const rawLine = lines[i]
467
- if (!rawLine) {
526
+ function collectStorybookBindings(nodes: MdxContent[]): StorybookBindings {
527
+ const bindings: StorybookBindings = {
528
+ names: new Set(),
529
+ namespaces: new Set(),
530
+ }
531
+
532
+ for (const node of nodes) {
533
+ if (!isMdxEsm(node))
468
534
  continue
535
+
536
+ const sourceFile = ts.createSourceFile('documentation.mtsx', node.value, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX)
537
+ for (const statement of sourceFile.statements) {
538
+ if (!ts.isImportDeclaration(statement)
539
+ || !ts.isStringLiteral(statement.moduleSpecifier)
540
+ || statement.moduleSpecifier.text !== '@storybook/blocks'
541
+ || !statement.importClause) {
542
+ continue
543
+ }
544
+
545
+ if (statement.importClause.name) {
546
+ bindings.names.add(statement.importClause.name.text)
547
+ bindings.namespaces.add(statement.importClause.name.text)
548
+ }
549
+
550
+ const namedBindings = statement.importClause.namedBindings
551
+ if (namedBindings && ts.isNamespaceImport(namedBindings)) {
552
+ bindings.namespaces.add(namedBindings.name.text)
553
+ }
554
+ else if (namedBindings && ts.isNamedImports(namedBindings)) {
555
+ for (const element of namedBindings.elements)
556
+ bindings.names.add(element.name.text)
557
+ }
469
558
  }
470
- const line = rawLine.trim()
559
+ }
560
+
561
+ return bindings
562
+ }
563
+
564
+ function isStorybookElement(node: MdxJsxFlowElement | MdxJsxTextElement, bindings: StorybookBindings): boolean {
565
+ if (!node.name)
566
+ return false
567
+ if (bindings.names.has(node.name))
568
+ return true
569
+
570
+ const separator = node.name.indexOf('.')
571
+ return separator > 0 && bindings.namespaces.has(node.name.slice(0, separator))
572
+ }
573
+
574
+ function removeMdxArtifacts(nodes: MdxContent[], storybookBindings: StorybookBindings): MdxContent[] {
575
+ const retained: MdxContent[] = []
471
576
 
472
- // Check for markdown headings (### or ####)
473
- const headingMatch = line.match(/^###?\s+(.+)/)
474
- if (headingMatch) {
475
- return headingMatch?.[1]?.trim() ?? `Example ${exampleIndex}`
577
+ for (const node of nodes) {
578
+ if (isMdxEsm(node) || (isMdxJsxElement(node) && isStorybookElement(node, storybookBindings)))
579
+ continue
580
+
581
+ if (hasChildren(node)) {
582
+ const children = removeMdxArtifacts(node.children, storybookBindings)
583
+ node.children.splice(0, node.children.length, ...children)
584
+ if (node.type === 'paragraph' && children.length === 0)
585
+ continue
476
586
  }
477
587
 
478
- // Check for bold text that might be a heading (**text**)
479
- const boldMatch = line.match(/^\*\*([^*]+)\*\*\s*$/)
480
- if (boldMatch) {
481
- return boldMatch?.[1]?.trim() ?? `Example ${exampleIndex}`
588
+ retained.push(node)
589
+ }
590
+
591
+ return retained
592
+ }
593
+
594
+ function stripFirstTitle(nodes: MdxContent[]): MdxContent[] {
595
+ let removed = false
596
+ return nodes.filter((node) => {
597
+ if (!removed && isHeading(node) && node.depth === 1) {
598
+ removed = true
599
+ return false
482
600
  }
601
+ return true
602
+ })
603
+ }
604
+
605
+ interface AuthoredApiSections {
606
+ content: MdxContent[]
607
+ eventGuidance: MdxContent[]
608
+ slotGuidance: MdxContent[]
609
+ }
483
610
 
484
- // If we hit a higher level heading (## or #), stop looking
485
- // This prevents us from going too far up the document
486
- if (line.match(/^##?\s+/) && !line.match(/^###/)) {
487
- break
611
+ function extractAuthoredApiSections(nodes: MdxContent[]): AuthoredApiSections {
612
+ const sections: AuthoredApiSections = {
613
+ content: [],
614
+ eventGuidance: [],
615
+ slotGuidance: [],
616
+ }
617
+ let index = 0
618
+
619
+ while (index < nodes.length) {
620
+ const node = nodes[index]
621
+ if (!node) {
622
+ index++
623
+ continue
488
624
  }
625
+
626
+ if (!isHeading(node)) {
627
+ sections.content.push(node)
628
+ index++
629
+ continue
630
+ }
631
+
632
+ const sectionName = readNodeText(node).trim().toLowerCase()
633
+ if (!AUTHORED_API_SECTIONS.has(sectionName)) {
634
+ sections.content.push(node)
635
+ index++
636
+ continue
637
+ }
638
+
639
+ const sectionDepth = node.depth
640
+ const sectionContent: MdxContent[] = []
641
+ index++
642
+ while (index < nodes.length) {
643
+ const candidate = nodes[index]
644
+ if (candidate && isHeading(candidate) && candidate.depth <= sectionDepth)
645
+ break
646
+ if (candidate)
647
+ sectionContent.push(candidate)
648
+ index++
649
+ }
650
+
651
+ if (sectionName === 'events' || sectionName === 'emits')
652
+ sections.eventGuidance.push(...sectionContent)
653
+ else if (sectionName === 'slots')
654
+ sections.slotGuidance.push(...sectionContent)
489
655
  }
490
656
 
491
- // Fallback to generic name
492
- return `Example ${exampleIndex}`
657
+ return sections
658
+ }
659
+
660
+ function stripEmptyHeadings(nodes: MdxContent[]): MdxContent[] {
661
+ return nodes.filter((node, index) => {
662
+ if (!isHeading(node))
663
+ return true
664
+
665
+ const boundary = nodes.findIndex((candidate, candidateIndex) =>
666
+ candidateIndex > index && isHeading(candidate) && candidate.depth <= node.depth)
667
+ const sectionEnd = boundary === -1 ? nodes.length : boundary
668
+ return nodes.slice(index + 1, sectionEnd).some(candidate => !isHeading(candidate))
669
+ })
670
+ }
671
+
672
+ function collectVueExamples(nodes: MdxContent[], initialHeading = '', startingIndex = 0): StoryExample[] {
673
+ const examples: StoryExample[] = []
674
+ let currentHeading = initialHeading
675
+
676
+ const visit = (children: MdxContent[]): void => {
677
+ for (const node of children) {
678
+ if (isHeading(node))
679
+ currentHeading = readNodeText(node).trim()
680
+ if (isCode(node) && node.lang?.toLowerCase() === 'vue' && node.value.trim()) {
681
+ examples.push({
682
+ name: currentHeading || `Example ${startingIndex + examples.length + 1}`,
683
+ code: node.value.trim(),
684
+ })
685
+ }
686
+ if (hasChildren(node))
687
+ visit(node.children)
688
+ }
689
+ }
690
+
691
+ visit(nodes)
692
+ return examples
693
+ }
694
+
695
+ function shiftHeadingDepth(depth: Heading['depth'], offset: number): Heading['depth'] {
696
+ const shifted = Math.min(6, depth + offset)
697
+ if (shifted === 1)
698
+ return 1
699
+ if (shifted === 2)
700
+ return 2
701
+ if (shifted === 3)
702
+ return 3
703
+ if (shifted === 4)
704
+ return 4
705
+ if (shifted === 5)
706
+ return 5
707
+ return 6
708
+ }
709
+
710
+ function shiftHeadings(nodes: MdxContent[], offset: number): void {
711
+ for (const node of nodes) {
712
+ if (isHeading(node))
713
+ node.depth = shiftHeadingDepth(node.depth, offset)
714
+ if (hasChildren(node))
715
+ shiftHeadings(node.children, offset)
716
+ }
717
+ }
718
+
719
+ function stringifyGuidance(nodes: MdxContent[]): string {
720
+ const content = stripEmptyHeadings(nodes)
721
+ if (content.length === 0)
722
+ return ''
723
+
724
+ shiftHeadings(content, 1)
725
+ const tree: MdxRoot = { type: 'root', children: content }
726
+ return mdxProcessor.stringify(tree).trim()
727
+ }
728
+
729
+ function sanitizeMdxContent(source: string): SanitizedMdx {
730
+ const tree: MdxRoot = mdxProcessor.parse(source)
731
+ const titleNode = tree.children.find(node => isHeading(node) && node.depth === 1)
732
+ const title = titleNode && isHeading(titleNode) ? readNodeText(titleNode).trim() : ''
733
+ const storybookBindings = collectStorybookBindings(tree.children)
734
+
735
+ tree.children = removeMdxArtifacts(tree.children, storybookBindings)
736
+ tree.children = stripFirstTitle(tree.children)
737
+ const authoredApi = extractAuthoredApiSections(tree.children)
738
+ tree.children = stripEmptyHeadings(authoredApi.content)
739
+
740
+ const contentExamples = collectVueExamples(tree.children)
741
+ const eventExamples = collectVueExamples(authoredApi.eventGuidance, 'Events', contentExamples.length)
742
+ const slotExamples = collectVueExamples(
743
+ authoredApi.slotGuidance,
744
+ 'Slots',
745
+ contentExamples.length + eventExamples.length,
746
+ )
747
+ shiftHeadings(tree.children, 2)
748
+
749
+ return {
750
+ title,
751
+ content: mdxProcessor.stringify(tree).trim(),
752
+ eventGuidance: stringifyGuidance(authoredApi.eventGuidance),
753
+ slotGuidance: stringifyGuidance(authoredApi.slotGuidance),
754
+ examples: [...contentExamples, ...eventExamples, ...slotExamples],
755
+ }
493
756
  }
494
757
 
495
758
  export function generateMarkdown(componentDocs: ComponentDoc[], typeResolver: TypeResolver): string {
@@ -677,11 +940,10 @@ export function generateDocsToDirectory(componentDocs: ComponentDoc[], typeResol
677
940
  mkdirSync(supportingDir, { recursive: true })
678
941
  }
679
942
 
680
- // Group components by family (e.g., tela-dropdown-*) → tela-dropdown.md
943
+ // Group components by their source family (e.g., tela/complex-table/* → tela-complex-table.md).
681
944
  const groups = new Map<string, ComponentDoc[]>()
682
945
  for (const comp of componentDocs.map(canonicalizeComponentDoc)) {
683
- const kebab = toKebabFromTag(comp.tagName)
684
- const group = toGroupSlugFromKebab(kebab)
946
+ const group = generateGroupSlug(comp)
685
947
  if (!groups.has(group))
686
948
  groups.set(group, [])
687
949
  groups.get(group)!.push(comp)
@@ -807,9 +1069,16 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
807
1069
  lines.push('')
808
1070
  }
809
1071
 
1072
+ if (comp.stories?.content) {
1073
+ lines.push('### Usage guidance')
1074
+ lines.push('')
1075
+ lines.push(comp.stories.content)
1076
+ lines.push('')
1077
+ }
1078
+
810
1079
  // Props
811
1080
  if (comp.props && comp.props.length > 0) {
812
- lines.push('## Props')
1081
+ lines.push('### Props')
813
1082
  lines.push('')
814
1083
  lines.push('```typescript')
815
1084
  lines.push('interface Props {')
@@ -833,7 +1102,7 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
833
1102
 
834
1103
  // Events
835
1104
  if (comp.events && comp.events.length > 0) {
836
- lines.push('## Events')
1105
+ lines.push('### Events')
837
1106
  lines.push('')
838
1107
  lines.push('| Event | Type | Description |')
839
1108
  lines.push('|------:|:-----|:------------|')
@@ -846,9 +1115,16 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
846
1115
  lines.push('')
847
1116
  }
848
1117
 
1118
+ if (comp.stories?.eventGuidance) {
1119
+ lines.push('### Event guidance')
1120
+ lines.push('')
1121
+ lines.push(comp.stories.eventGuidance)
1122
+ lines.push('')
1123
+ }
1124
+
849
1125
  // Slots
850
1126
  if (comp.slots && comp.slots.length > 0) {
851
- lines.push('## Slots')
1127
+ lines.push('### Slots')
852
1128
  lines.push('')
853
1129
  lines.push('| Slot | Scoped Props | Description |')
854
1130
  lines.push('|-----:|:-------------|:------------|')
@@ -861,12 +1137,21 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
861
1137
  lines.push('')
862
1138
  }
863
1139
 
864
- // Examples (from MDX)
865
- if (comp.stories && comp.stories.examples && comp.stories.examples.length > 0) {
866
- lines.push('## Examples')
1140
+ if (comp.stories?.slotGuidance) {
1141
+ lines.push('### Slot guidance')
1142
+ lines.push('')
1143
+ lines.push(comp.stories.slotGuidance)
1144
+ lines.push('')
1145
+ }
1146
+
1147
+ if (!comp.stories?.content
1148
+ && !comp.stories?.eventGuidance
1149
+ && !comp.stories?.slotGuidance
1150
+ && comp.stories?.examples.length) {
1151
+ lines.push('### Examples')
867
1152
  lines.push('')
868
1153
  comp.stories.examples.forEach((ex: any) => {
869
- lines.push(`### ${ex.name}`)
1154
+ lines.push(`#### ${ex.name}`)
870
1155
  lines.push('')
871
1156
  lines.push('```vue')
872
1157
  lines.push(ex.code)
@@ -979,12 +1264,11 @@ function renderPantryFrontmatter(pantry: SkillPantryMetadata): string[] {
979
1264
  return lines
980
1265
  }
981
1266
 
982
- function toGroupSlugFromKebab(kebab: string): string {
983
- const parts = kebab.split('-').filter(Boolean)
984
- if (parts.length <= 2)
985
- return kebab
986
- // Prefer first two parts (e.g., tela-dropdown), otherwise fallback to entire kebab
987
- return `${parts[0]}-${parts[1]}`
1267
+ export function generateGroupSlug(comp: ComponentDoc): string {
1268
+ const directoryParts = comp.directory.split(/[\\/]/).filter(Boolean)
1269
+ if (directoryParts.length > 1)
1270
+ return directoryParts.slice(0, 2).map(toKebabFromTag).join('-')
1271
+ return toKebabFromTag(comp.tagName)
988
1272
  }
989
1273
 
990
1274
  function toTitleFromGroupSlug(groupSlug: string): string {
package/package.json CHANGED
@@ -1,85 +1,89 @@
1
1
  {
2
- "name": "@meistrari/tela-build",
3
- "version": "1.65.0",
4
- "type": "module",
5
- "files": [
6
- "app.config.ts",
7
- "components",
8
- "components.json",
9
- "composables",
10
- "css",
11
- "docs",
12
- "lib",
13
- "modules",
14
- "nuxt.config.ts",
15
- "plugins",
16
- "public",
17
- "tsconfig.json",
18
- "types",
19
- "unocss.config.ts",
20
- "utils"
21
- ],
22
- "main": "./nuxt.config.ts",
23
- "scripts": {
24
- "postinstall": "nuxt prepare",
25
- "generate-docs": "bun scripts/generate-docs.ts"
26
- },
27
- "dependencies": {
28
- "@floating-ui/vue": "1.1.5",
29
- "@fontsource-variable/geist-mono": "^5.2.7",
30
- "@fontsource-variable/inter": "^5.2.8",
31
- "@iconify-json/ph": "1.2.2",
32
- "@internationalized/date": "3.6.0",
33
- "@internationalized/number": "3.6.0",
34
- "@number-flow/vue": "^0.3.2",
35
- "@nuxt/icon": "^1.9.0",
36
- "@nuxtjs/color-mode": "^3.5.2",
37
- "@nuxtjs/tailwindcss": "6.12.2",
38
- "@unocss/nuxt": "66.5.12",
39
- "@unocss/transformer-directives": "66.5.12",
40
- "@vueuse/components": "12.8.2",
41
- "@vueuse/core": "12.8.2",
42
- "@vueuse/nuxt": "12.8.2",
43
- "class-variance-authority": "0.7.0",
44
- "clsx": "2.1.1",
45
- "glob": "11.0.0",
46
- "gsap": "3.12.5",
47
- "import-in-the-middle": "1.11.2",
48
- "lucide-vue-next": "0.456.0",
49
- "motion": "10.16.4",
50
- "motion-v": "2.3.0",
51
- "nitropack": "2.9.7",
52
- "number-flow": "0.4.1",
53
- "nuxt-svgo": "4.2.6",
54
- "pathe": "1.1.2",
55
- "pdfjs-dist": "4.10.38",
56
- "radix-vue": "1.9.17",
57
- "reka-ui": "2.3.0",
58
- "resize-observer-polyfill": "1.5.1",
59
- "shiki": "2.5.0",
60
- "tailwind-merge": "2.5.4",
61
- "tailwindcss-animate": "1.0.7",
62
- "ts-morph": "22.0.0",
63
- "typescript": "5.8.2",
64
- "unocss": "66.5.12",
65
- "vue": "3.5.17",
66
- "vue-component-meta": "3.0.8",
67
- "vue-docgen-api": "4.78.0",
68
- "vue-input-otp": "0.3.2",
69
- "vue-router": "4.5.0"
70
- },
71
- "peerDependencies": {
72
- "nuxt": "^3.17.0 || ^4.0.0"
73
- },
74
- "devDependencies": {
75
- "@nuxt/kit": "3.17.7",
76
- "@types/node": "18.19.79",
77
- "fs-extra": "11.2.0",
78
- "gray-matter": "4.0.3",
79
- "remark-gfm": "4.0.0",
80
- "remark-parse": "11.0.0",
81
- "remark-stringify": "11.0.0",
82
- "nuxt": "3.17.7",
83
- "unified": "11.0.0"
84
- }
2
+ "name": "@meistrari/tela-build",
3
+ "version": "1.65.1",
4
+ "type": "module",
5
+ "files": [
6
+ "app.config.ts",
7
+ "components",
8
+ "components.json",
9
+ "composables",
10
+ "css",
11
+ "docs",
12
+ "lib",
13
+ "modules",
14
+ "nuxt.config.ts",
15
+ "plugins",
16
+ "public",
17
+ "tsconfig.json",
18
+ "types",
19
+ "unocss.config.ts",
20
+ "utils"
21
+ ],
22
+ "main": "./nuxt.config.ts",
23
+ "scripts": {
24
+ "postinstall": "nuxt prepare",
25
+ "generate-docs": "bun scripts/generate-docs.ts"
26
+ },
27
+ "dependencies": {
28
+ "@floating-ui/vue": "1.1.5",
29
+ "@fontsource-variable/geist-mono": "^5.2.7",
30
+ "@fontsource-variable/inter": "^5.2.8",
31
+ "@iconify-json/ph": "1.2.2",
32
+ "@internationalized/date": "3.6.0",
33
+ "@internationalized/number": "3.6.0",
34
+ "@number-flow/vue": "^0.3.2",
35
+ "@nuxt/icon": "^1.9.0",
36
+ "@types/mdast": "4.0.4",
37
+ "@nuxtjs/color-mode": "^3.5.2",
38
+ "@nuxtjs/tailwindcss": "6.12.2",
39
+ "@unocss/nuxt": "66.5.12",
40
+ "@unocss/transformer-directives": "66.5.12",
41
+ "@vueuse/components": "12.8.2",
42
+ "@vueuse/core": "12.8.2",
43
+ "@vueuse/nuxt": "12.8.2",
44
+ "class-variance-authority": "0.7.0",
45
+ "clsx": "2.1.1",
46
+ "glob": "11.0.0",
47
+ "gsap": "3.12.5",
48
+ "import-in-the-middle": "1.11.2",
49
+ "lucide-vue-next": "0.456.0",
50
+ "mdast-util-mdx": "3.0.0",
51
+ "motion": "10.16.4",
52
+ "motion-v": "2.3.0",
53
+ "nitropack": "2.9.7",
54
+ "number-flow": "0.4.1",
55
+ "nuxt-svgo": "4.2.6",
56
+ "pathe": "1.1.2",
57
+ "pdfjs-dist": "4.10.38",
58
+ "radix-vue": "1.9.17",
59
+ "reka-ui": "2.3.0",
60
+ "remark-gfm": "4.0.0",
61
+ "remark-mdx": "3.1.1",
62
+ "remark-parse": "11.0.0",
63
+ "remark-stringify": "11.0.0",
64
+ "resize-observer-polyfill": "1.5.1",
65
+ "scule": "1.3.0",
66
+ "shiki": "2.5.0",
67
+ "tailwind-merge": "2.5.4",
68
+ "tailwindcss-animate": "1.0.7",
69
+ "ts-morph": "22.0.0",
70
+ "typescript": "5.8.2",
71
+ "unified": "11.0.5",
72
+ "unocss": "66.5.12",
73
+ "vue": "3.5.17",
74
+ "vue-component-meta": "3.0.8",
75
+ "vue-docgen-api": "4.78.0",
76
+ "vue-input-otp": "0.3.2",
77
+ "vue-router": "4.5.0"
78
+ },
79
+ "peerDependencies": {
80
+ "nuxt": "^3.17.0 || ^4.0.0"
81
+ },
82
+ "devDependencies": {
83
+ "@nuxt/kit": "3.17.7",
84
+ "@types/node": "18.19.79",
85
+ "fs-extra": "11.2.0",
86
+ "gray-matter": "4.0.3",
87
+ "nuxt": "3.17.7"
88
+ }
85
89
  }