@meistrari/tela-build 1.0.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/README.md +75 -0
- package/app.config.ts +73 -0
- package/components/tela/animated/animated-calculating-number.vue +16 -0
- package/components/tela/animated/animated-number.mdx +248 -0
- package/components/tela/animated/animated-number.stories.ts +52 -0
- package/components/tela/animated/animated-number.vue +23 -0
- package/components/tela/animated/animated-text.vue +124 -0
- package/components/tela/animated/animated-value.vue +68 -0
- package/components/tela/avatar/avatar.mdx +117 -0
- package/components/tela/avatar/avatar.stories.ts +62 -0
- package/components/tela/avatar/avatar.vue +71 -0
- package/components/tela/avatar/group/avatar-group.stories.ts +78 -0
- package/components/tela/avatar/group/avatar-group.vue +46 -0
- package/components/tela/badge/badge.mdx +154 -0
- package/components/tela/badge/badge.stories.ts +82 -0
- package/components/tela/badge/badge.vue +41 -0
- package/components/tela/button/button.mdx +155 -0
- package/components/tela/button/button.stories.ts +202 -0
- package/components/tela/button/button.vue +107 -0
- package/components/tela/card.vue +30 -0
- package/components/tela/chart/chart-bar.vue +58 -0
- package/components/tela/chat/chat.mdx +268 -0
- package/components/tela/chat/chat.stories.ts +253 -0
- package/components/tela/chat/command/index.vue +41 -0
- package/components/tela/chat/command/mention/index.vue +138 -0
- package/components/tela/chat/index.vue +112 -0
- package/components/tela/chat/pure-text-input/chat-text-input.vue +190 -0
- package/components/tela/chat/text-input/chat-text-input.stories.ts +128 -0
- package/components/tela/chat/text-input/index.vue +217 -0
- package/components/tela/chat/text-message/chat-text-message.stories.ts +138 -0
- package/components/tela/chat/text-message/index.vue +355 -0
- package/components/tela/chat/types.ts +19 -0
- package/components/tela/checkbox/checkbox-card.vue +30 -0
- package/components/tela/checkbox/checkbox.mdx +164 -0
- package/components/tela/checkbox/checkbox.stories.ts +104 -0
- package/components/tela/checkbox/checkbox.vue +43 -0
- package/components/tela/collapsible/Collapsible.vue +15 -0
- package/components/tela/collapsible/CollapsibleContent.vue +59 -0
- package/components/tela/collapsible/CollapsibleTrigger.vue +12 -0
- package/components/tela/collapsible/collapsible.mdx +157 -0
- package/components/tela/collapsible-section/collapsible-section.mdx +180 -0
- package/components/tela/collapsible-section/collapsible-section.stories.ts +53 -0
- package/components/tela/collapsible-section/collapsible-section.vue +51 -0
- package/components/tela/collapsible-section-with-actions.vue +98 -0
- package/components/tela/combobox/combobox-anchor.vue +24 -0
- package/components/tela/combobox/combobox-empty.vue +19 -0
- package/components/tela/combobox/combobox-group.vue +24 -0
- package/components/tela/combobox/combobox-indicator.vue +22 -0
- package/components/tela/combobox/combobox-input.vue +31 -0
- package/components/tela/combobox/combobox-item.vue +28 -0
- package/components/tela/combobox/combobox-label.vue +24 -0
- package/components/tela/combobox/combobox-list.vue +90 -0
- package/components/tela/combobox/combobox-module-selector.vue +366 -0
- package/components/tela/combobox/combobox-root.vue +15 -0
- package/components/tela/combobox/combobox-trigger.vue +12 -0
- package/components/tela/combobox/combobox.mdx +285 -0
- package/components/tela/combobox/combobox.stories.ts +232 -0
- package/components/tela/combobox/combobox.vue +497 -0
- package/components/tela/command/command-dialog.vue +22 -0
- package/components/tela/command/command-empty.vue +25 -0
- package/components/tela/command/command-group.vue +46 -0
- package/components/tela/command/command-input.vue +38 -0
- package/components/tela/command/command-item.vue +78 -0
- package/components/tela/command/command-list.vue +78 -0
- package/components/tela/command/command-separator.vue +23 -0
- package/components/tela/command/command-shortcut.vue +13 -0
- package/components/tela/command/command.vue +88 -0
- package/components/tela/command/dialog-base.vue +15 -0
- package/components/tela/command/dialog-content.vue +50 -0
- package/components/tela/command/utils.ts +15 -0
- package/components/tela/complex-table/complex-table-cell.stories.ts +145 -0
- package/components/tela/complex-table/complex-table-cell.vue +45 -0
- package/components/tela/complex-table/complex-table-header-cell.stories.ts +103 -0
- package/components/tela/complex-table/complex-table-header-cell.vue +48 -0
- package/components/tela/complex-table/complex-table-header.stories.ts +89 -0
- package/components/tela/complex-table/complex-table-header.vue +70 -0
- package/components/tela/complex-table/complex-table-row.vue +199 -0
- package/components/tela/complex-table/complex-table-virtualized.vue +326 -0
- package/components/tela/complex-table/complex-table.stories.ts +358 -0
- package/components/tela/complex-table/complex-table.vue +237 -0
- package/components/tela/complex-table/composables/table-common.ts +93 -0
- package/components/tela/complex-table/composables/table-selection.ts +87 -0
- package/components/tela/complex-table/composables/virtual-scroll.ts +252 -0
- package/components/tela/complex-table/styles/table-shared.css +170 -0
- package/components/tela/complex-table/types.ts +63 -0
- package/components/tela/complex-table/utils.ts +35 -0
- package/components/tela/confirm-button/confirm-button.vue +137 -0
- package/components/tela/confirmation-modal/confirmation-modal.vue +72 -0
- package/components/tela/copy-button.vue +86 -0
- package/components/tela/date-range-picker.vue +221 -0
- package/components/tela/dialog/dialog.mdx +170 -0
- package/components/tela/dialog/dialog.vue +182 -0
- package/components/tela/disabled-area.vue +16 -0
- package/components/tela/disclaimer/disclaimer.mdx +238 -0
- package/components/tela/disclaimer/disclaimer.stories.ts +196 -0
- package/components/tela/disclaimer/disclaimer.vue +125 -0
- package/components/tela/dropdown-menu/DropdownMenu.vue +121 -0
- package/components/tela/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
- package/components/tela/dropdown-menu/DropdownMenuContent.vue +75 -0
- package/components/tela/dropdown-menu/DropdownMenuGroup.vue +12 -0
- package/components/tela/dropdown-menu/DropdownMenuItem.vue +137 -0
- package/components/tela/dropdown-menu/DropdownMenuLabel.vue +26 -0
- package/components/tela/dropdown-menu/DropdownMenuRadioGroup.vue +18 -0
- package/components/tela/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
- package/components/tela/dropdown-menu/DropdownMenuRoot.vue +15 -0
- package/components/tela/dropdown-menu/DropdownMenuSeparator.vue +21 -0
- package/components/tela/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- package/components/tela/dropdown-menu/DropdownMenuSub.vue +18 -0
- package/components/tela/dropdown-menu/DropdownMenuSubContent.vue +30 -0
- package/components/tela/dropdown-menu/DropdownMenuSubTrigger.vue +35 -0
- package/components/tela/dropdown-menu/DropdownMenuTrigger.vue +14 -0
- package/components/tela/dropdown-menu/dropdown-menu.mdx +265 -0
- package/components/tela/dropdown-menu/dropdown-menu.stories.ts +156 -0
- package/components/tela/expandable-input.vue +96 -0
- package/components/tela/file-drop.vue +37 -0
- package/components/tela/file-upload/file-upload.mdx +189 -0
- package/components/tela/file-upload/file-upload.stories.ts +48 -0
- package/components/tela/file-upload/file-upload.vue +205 -0
- package/components/tela/filters/checkbox-filter.stories.ts +218 -0
- package/components/tela/filters/checkbox-filter.vue +165 -0
- package/components/tela/filters/date-filter.stories.ts +258 -0
- package/components/tela/filters/date-filter.vue +200 -0
- package/components/tela/filters/user-filter.stories.ts +344 -0
- package/components/tela/filters/user-filter.vue +271 -0
- package/components/tela/hover-card/hover-card.mdx +221 -0
- package/components/tela/hover-card/hover-card.stories.ts +87 -0
- package/components/tela/hover-card/hover-card.vue +61 -0
- package/components/tela/icon/custom.vue +319 -0
- package/components/tela/icon/spinner.vue +12 -0
- package/components/tela/icon-button/icon-button.vue +114 -0
- package/components/tela/icon.vue +37 -0
- package/components/tela/initials.vue +28 -0
- package/components/tela/inline-input.vue +77 -0
- package/components/tela/input/input.mdx +182 -0
- package/components/tela/input/input.stories.ts +153 -0
- package/components/tela/input/tela-input.vue +240 -0
- package/components/tela/kbd/kbd-return.vue +6 -0
- package/components/tela/kbd/kbd.mdx +238 -0
- package/components/tela/kbd/kbd.vue +18 -0
- package/components/tela/label/label.mdx +121 -0
- package/components/tela/label/label.stories.ts +37 -0
- package/components/tela/label/label.vue +25 -0
- package/components/tela/link-decoration/link-decoration.vue +19 -0
- package/components/tela/live-label.vue +32 -0
- package/components/tela/long-press-button.vue +98 -0
- package/components/tela/menubar/menubar-content.vue +77 -0
- package/components/tela/menubar/menubar-item.vue +32 -0
- package/components/tela/menubar/menubar-label.vue +14 -0
- package/components/tela/menubar/menubar-menu.vue +12 -0
- package/components/tela/menubar/menubar-root.vue +30 -0
- package/components/tela/menubar/menubar-separator.vue +17 -0
- package/components/tela/menubar/menubar-shortcut.vue +14 -0
- package/components/tela/menubar/menubar-sub-content.vue +36 -0
- package/components/tela/menubar/menubar-sub-trigger.vue +28 -0
- package/components/tela/menubar/menubar-sub.vue +20 -0
- package/components/tela/menubar/menubar-trigger.vue +27 -0
- package/components/tela/menubar/menubar.vue +298 -0
- package/components/tela/modal/modal.mdx +145 -0
- package/components/tela/modal/modal.vue +242 -0
- package/components/tela/multiple-select/multiple-select.mdx +274 -0
- package/components/tela/multiple-select/multiple-select.stories.ts +325 -0
- package/components/tela/multiple-select/multiple-select.vue +666 -0
- package/components/tela/pane.vue +110 -0
- package/components/tela/popover/popover-content.vue +48 -0
- package/components/tela/popover/popover-trigger.vue +12 -0
- package/components/tela/popover/popover.mdx +239 -0
- package/components/tela/popover/popover.stories.ts +150 -0
- package/components/tela/popover/popover.vue +15 -0
- package/components/tela/popover-list/popover-list-nested.vue +104 -0
- package/components/tela/popover-list/popover-list.stories.ts +330 -0
- package/components/tela/popover-list/popover-list.vue +191 -0
- package/components/tela/radio-button.vue +66 -0
- package/components/tela/radio-group/radio-group-item.vue +40 -0
- package/components/tela/radio-group/radio-group-root.vue +26 -0
- package/components/tela/radio-group/radio-group.mdx +78 -0
- package/components/tela/radio-group/radio-group.stories.ts +106 -0
- package/components/tela/radio-group/radio-group.vue +23 -0
- package/components/tela/range-calendar.stories.ts +110 -0
- package/components/tela/range-calendar.vue +109 -0
- package/components/tela/scroll-area/scroll-area.mdx +183 -0
- package/components/tela/scroll-area/scroll-area.vue +30 -0
- package/components/tela/scroll-area/scroll-bar.vue +31 -0
- package/components/tela/segment-toggle.stories.ts +114 -0
- package/components/tela/segment-toggle.vue +66 -0
- package/components/tela/select-menu/select-menu-content.vue +106 -0
- package/components/tela/select-menu/select-menu-down-button.vue +20 -0
- package/components/tela/select-menu/select-menu-group.vue +16 -0
- package/components/tela/select-menu/select-menu-item.vue +40 -0
- package/components/tela/select-menu/select-menu-root.vue +15 -0
- package/components/tela/select-menu/select-menu-trigger.vue +34 -0
- package/components/tela/select-menu/select-menu-up-button.vue +20 -0
- package/components/tela/select-menu/select-menu-value.vue +12 -0
- package/components/tela/select-menu/select-menu.mdx +221 -0
- package/components/tela/select-menu/select-menu.stories.ts +91 -0
- package/components/tela/select-menu/select-menu.vue +165 -0
- package/components/tela/selector/selector.vue +47 -0
- package/components/tela/sheet/sheet-close.vue +12 -0
- package/components/tela/sheet/sheet-content.vue +57 -0
- package/components/tela/sheet/sheet-description.vue +23 -0
- package/components/tela/sheet/sheet-footer.vue +18 -0
- package/components/tela/sheet/sheet-header.vue +15 -0
- package/components/tela/sheet/sheet-root.vue +18 -0
- package/components/tela/sheet/sheet-title.vue +23 -0
- package/components/tela/sheet/sheet-trigger.vue +12 -0
- package/components/tela/sheet/sheet.client.vue +150 -0
- package/components/tela/sheet/sheet.mdx +176 -0
- package/components/tela/sheet/sheet.stories.ts +201 -0
- package/components/tela/sheet/variants.ts +22 -0
- package/components/tela/side-sheet/side-sheet.mdx +131 -0
- package/components/tela/side-sheet/side-sheet.stories.ts +134 -0
- package/components/tela/side-sheet/side-sheet.vue +106 -0
- package/components/tela/skeleton/skeleton.mdx +165 -0
- package/components/tela/skeleton/skeleton.stories.ts +35 -0
- package/components/tela/skeleton/skeleton.vue +45 -0
- package/components/tela/skeleton-icon.vue +24 -0
- package/components/tela/span.vue +24 -0
- package/components/tela/star-button.vue +70 -0
- package/components/tela/status/status-lean.vue +30 -0
- package/components/tela/status/status.mdx +187 -0
- package/components/tela/status/status.stories.ts +160 -0
- package/components/tela/status/status.vue +420 -0
- package/components/tela/status-bar/status-bar.mdx +178 -0
- package/components/tela/status-bar/status-bar.stories.ts +64 -0
- package/components/tela/status-bar/status-bar.vue +56 -0
- package/components/tela/status-bar/types.ts +5 -0
- package/components/tela/switch/switch.mdx +118 -0
- package/components/tela/switch/switch.stories.ts +80 -0
- package/components/tela/switch/switch.vue +56 -0
- package/components/tela/table/table-body.vue +13 -0
- package/components/tela/table/table-caption.vue +13 -0
- package/components/tela/table/table-cell.vue +20 -0
- package/components/tela/table/table-empty.vue +37 -0
- package/components/tela/table/table-footer.vue +13 -0
- package/components/tela/table/table-head.vue +13 -0
- package/components/tela/table/table-header.vue +13 -0
- package/components/tela/table/table-row.vue +13 -0
- package/components/tela/table/table.mdx +230 -0
- package/components/tela/table/table.stories.ts +384 -0
- package/components/tela/table/table.vue +15 -0
- package/components/tela/tabs/tabs-content.vue +20 -0
- package/components/tela/tabs/tabs-indicator.vue +22 -0
- package/components/tela/tabs/tabs-list.vue +23 -0
- package/components/tela/tabs/tabs-root.vue +15 -0
- package/components/tela/tabs/tabs-trigger.vue +27 -0
- package/components/tela/tabs/tabs.mdx +138 -0
- package/components/tela/tabs/tabs.stories.ts +72 -0
- package/components/tela/tabs/tabs.vue +61 -0
- package/components/tela/tags/tags-select.mdx +318 -0
- package/components/tela/tags/tags-select.stories.ts +47 -0
- package/components/tela/tags/tags-select.vue +637 -0
- package/components/tela/tags/tags.mdx +151 -0
- package/components/tela/tags/tags.stories.ts +118 -0
- package/components/tela/tags/tags.vue +112 -0
- package/components/tela/textarea/textarea.mdx +102 -0
- package/components/tela/textarea/textarea.stories.ts +50 -0
- package/components/tela/textarea/textarea.vue +34 -0
- package/components/tela/toggle-group.vue +91 -0
- package/components/tela/tooltip/tooltip-content.vue +45 -0
- package/components/tela/tooltip/tooltip-provider.vue +12 -0
- package/components/tela/tooltip/tooltip-root.vue +15 -0
- package/components/tela/tooltip/tooltip-trigger.vue +12 -0
- package/components/tela/tooltip/tooltip.mdx +196 -0
- package/components/tela/tooltip/tooltip.stories.ts +200 -0
- package/components/tela/tooltip/tooltip.vue +91 -0
- package/components/tela/tooltip-group/tooltip-group-trigger.vue +92 -0
- package/components/tela/tooltip-group/tooltip-group.mdx +236 -0
- package/components/tela/tooltip-group/tooltip-group.stories.ts +465 -0
- package/components/tela/tooltip-group/tooltip-group.vue +35 -0
- package/components/tela/transparent-input.vue +151 -0
- package/components/tela/variable-icon.vue +28 -0
- package/components/tela/variable-input.vue +77 -0
- package/components/tela/wide-button/wide-button.vue +40 -0
- package/components.json +18 -0
- package/composables/status-toast.ts +67 -0
- package/css/reset.css +386 -0
- package/css/text.css +22 -0
- package/lib/doc-generator.ts +903 -0
- package/lib/extractors/volar-extract.ts +186 -0
- package/lib/type-resolver.ts +402 -0
- package/lib/utils.ts +6 -0
- package/modules/tela-build-docs/index.ts +139 -0
- package/nuxt.config.ts +80 -0
- package/package.json +84 -0
- package/plugins/test-id.ts +7 -0
- package/tsconfig.json +7 -0
- package/types/custom-icon.ts +1 -0
- package/types/index.ts +2 -0
- package/types/status.ts +1 -0
- package/unocss.config.ts +89 -0
- package/utils/component-utils.ts +30 -0
- package/utils/design-tokens.ts +431 -0
- package/utils/fold.ts +8 -0
- package/utils/select-menu.ts +10 -0
- package/utils/status.ts +1 -0
- package/utils/without-keys.ts +34 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { join, resolve, relative, dirname, basename } from 'pathe'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
4
|
+
import { parse as parseVue } from 'vue-docgen-api'
|
|
5
|
+
// Load glob dynamically for compatibility across versions (v8 vs v11)
|
|
6
|
+
import { getTypeResolver, type TypeResolver } from './type-resolver'
|
|
7
|
+
import { createVolarExtractor, type VolarExtractor } from './extractors/volar-extract'
|
|
8
|
+
|
|
9
|
+
const colors = {
|
|
10
|
+
gray: '\x1B[90m',
|
|
11
|
+
orange: '\x1B[38;5;208m',
|
|
12
|
+
reset: '\x1B[0m',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ComponentDoc {
|
|
16
|
+
name: string
|
|
17
|
+
tagName: string
|
|
18
|
+
path: string
|
|
19
|
+
directory: string
|
|
20
|
+
description: string
|
|
21
|
+
props: any[]
|
|
22
|
+
events: any[]
|
|
23
|
+
slots: any[]
|
|
24
|
+
stories?: StoryDoc
|
|
25
|
+
parseError?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StoryDoc {
|
|
29
|
+
title: string
|
|
30
|
+
examples: any[]
|
|
31
|
+
argTypes: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function collectComponentDocs(layerPath: string, appRootDir?: string): Promise<{ docs: ComponentDoc[], typeResolver: TypeResolver }> {
|
|
35
|
+
const componentPath = join(layerPath, 'components')
|
|
36
|
+
const componentPattern = join(componentPath, '**/*.vue')
|
|
37
|
+
|
|
38
|
+
const debug = process.env.TELA_BUILD_DEBUG === 'true'
|
|
39
|
+
const preferVolar = process.env.TELA_BUILD_EXTRACTOR !== 'docgen' // 'auto' (default) or 'volar'
|
|
40
|
+
|
|
41
|
+
// Debug: Check what paths exist
|
|
42
|
+
const layerNodeModules = resolve(layerPath, 'node_modules')
|
|
43
|
+
const appNodeModules = appRootDir ? resolve(appRootDir, 'node_modules') : null
|
|
44
|
+
const overlayRoot = appRootDir ?? layerPath
|
|
45
|
+
|
|
46
|
+
if (debug) {
|
|
47
|
+
console.log(`${colors.gray}[tela/build] Layer path: ${layerPath}${colors.reset}`)
|
|
48
|
+
console.log(`${colors.gray}[tela/build] Layer node_modules exists: ${existsSync(layerNodeModules)} (${layerNodeModules})${colors.reset}`)
|
|
49
|
+
if (appNodeModules) {
|
|
50
|
+
console.log(`${colors.gray}[tela/build] App node_modules exists: ${existsSync(appNodeModules)} (${appNodeModules})${colors.reset}`)
|
|
51
|
+
}
|
|
52
|
+
console.log(`${colors.gray}[tela/build] Extractor preference: ${preferVolar ? 'volar-first' : 'docgen-only'} (root=${overlayRoot})${colors.reset}`)
|
|
53
|
+
|
|
54
|
+
// Check if specific packages exist
|
|
55
|
+
const radixPath = resolve(layerNodeModules, 'radix-vue')
|
|
56
|
+
const rekaPath = resolve(layerNodeModules, 'reka-ui')
|
|
57
|
+
console.log(`${colors.gray}[tela/build] radix-vue exists: ${existsSync(radixPath)} (${radixPath})${colors.reset}`)
|
|
58
|
+
console.log(`${colors.gray}[tela/build] reka-ui exists: ${existsSync(rekaPath)} (${rekaPath})${colors.reset}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Initialize type resolver for the project (used to format types in markdown)
|
|
62
|
+
const typeResolver = await getTypeResolver(layerPath, appRootDir)
|
|
63
|
+
|
|
64
|
+
// Prepare Volar extractor once (optional)
|
|
65
|
+
let volar: VolarExtractor | null = null
|
|
66
|
+
if (preferVolar) {
|
|
67
|
+
try {
|
|
68
|
+
volar = createVolarExtractor(overlayRoot, layerPath)
|
|
69
|
+
if (debug) {
|
|
70
|
+
console.log(`${colors.gray}[tela/build] Using Volar extractor with tsconfig overlay at ${overlayRoot}/.docs/tsconfig.tela-docs.json${colors.reset}`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e: any) {
|
|
74
|
+
if (debug) {
|
|
75
|
+
console.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} Failed to init Volar extractor: ${e.message}${colors.reset}`)
|
|
76
|
+
}
|
|
77
|
+
volar = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const files = await globAsync(componentPattern, { ignore: ['**/*.story.vue', '**/*.test.vue', '**/*.spec.vue'] })
|
|
82
|
+
const docs: ComponentDoc[] = []
|
|
83
|
+
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
let info
|
|
86
|
+
let parseError: string | undefined
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const absoluteFile = resolve(file)
|
|
90
|
+
|
|
91
|
+
if (volar) {
|
|
92
|
+
try {
|
|
93
|
+
const volarInfo = volar.extract(absoluteFile)
|
|
94
|
+
info = {
|
|
95
|
+
displayName: volarInfo.name,
|
|
96
|
+
exportName: volarInfo.name,
|
|
97
|
+
description: volarInfo.description,
|
|
98
|
+
props: volarInfo.props.map(p => ({
|
|
99
|
+
name: p.name,
|
|
100
|
+
required: p.required,
|
|
101
|
+
type: p.type,
|
|
102
|
+
defaultValue: p.defaultValue,
|
|
103
|
+
description: p.description,
|
|
104
|
+
})),
|
|
105
|
+
events: volarInfo.events.map(e => ({
|
|
106
|
+
name: e.name,
|
|
107
|
+
type: e.type,
|
|
108
|
+
description: e.description,
|
|
109
|
+
})),
|
|
110
|
+
slots: volarInfo.slots.map(s => ({
|
|
111
|
+
name: s.name,
|
|
112
|
+
description: s.description,
|
|
113
|
+
scoped: Array.isArray(s.bindings) && s.bindings.length > 0,
|
|
114
|
+
bindings: (s.bindings || []).map(b => ({ name: b.name, type: b.type })),
|
|
115
|
+
})),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (volarError: any) {
|
|
119
|
+
if (debug) {
|
|
120
|
+
console.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} Volar extractor failed for ${relative(layerPath, file)}: ${volarError.message}. Falling back to vue-docgen-api.${colors.reset}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!info) {
|
|
126
|
+
// Configure module resolution - this is the key to fixing import resolution
|
|
127
|
+
// The 'modules' option mirrors webpack's resolve.modules option
|
|
128
|
+
const parseOptions = {
|
|
129
|
+
modules: [
|
|
130
|
+
layerNodeModules, // Layer's node_modules (where packages actually exist)
|
|
131
|
+
appNodeModules, // App's node_modules as fallback
|
|
132
|
+
].filter(Boolean),
|
|
133
|
+
jsx: false, // Disable JSX parsing to avoid babel issues with TypeScript
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (debug) {
|
|
137
|
+
console.log(`${colors.gray}[tela/build] Using vue-docgen-api with modules:${colors.reset}`, parseOptions.modules)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
info = await parseVue(file, parseOptions as any)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error: any) {
|
|
144
|
+
// Capture the parse error but still create documentation
|
|
145
|
+
parseError = error.message
|
|
146
|
+
console.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} Failed to parse ${relative(layerPath, file)}: ${error.message}${colors.reset}`)
|
|
147
|
+
|
|
148
|
+
// Create basic component info from filename
|
|
149
|
+
const fileName = basename(file, '.vue')
|
|
150
|
+
info = {
|
|
151
|
+
displayName: fileName,
|
|
152
|
+
exportName: fileName,
|
|
153
|
+
description: '',
|
|
154
|
+
props: [],
|
|
155
|
+
events: [],
|
|
156
|
+
slots: [],
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fileName = basename(file, '.vue')
|
|
161
|
+
const directory = relative(componentPath, dirname(file))
|
|
162
|
+
|
|
163
|
+
// Generate component tag name
|
|
164
|
+
const tagName = generateTagName(fileName)
|
|
165
|
+
|
|
166
|
+
const componentDoc: ComponentDoc = {
|
|
167
|
+
name: info.displayName || info.exportName || fileName,
|
|
168
|
+
tagName,
|
|
169
|
+
path: relative(layerPath, file),
|
|
170
|
+
directory,
|
|
171
|
+
description: info.description || '',
|
|
172
|
+
// Normalize props/events/slots shape to your generator consumption
|
|
173
|
+
props: (info.props || []).map((p: any) => ({
|
|
174
|
+
name: p.name,
|
|
175
|
+
required: !!p.required,
|
|
176
|
+
type: typeof p.type === 'string' ? p.type : (p.type?.name ?? p.type ?? 'any'),
|
|
177
|
+
defaultValue: p.defaultValue,
|
|
178
|
+
description: p.description,
|
|
179
|
+
})),
|
|
180
|
+
events: (info.events || []).map((e: any) => ({
|
|
181
|
+
name: e.name,
|
|
182
|
+
type: typeof e.type === 'string' ? e.type : (e.type?.name ?? e.type ?? 'any'),
|
|
183
|
+
description: e.description,
|
|
184
|
+
})),
|
|
185
|
+
slots: (info.slots || []).map((s: any) => ({
|
|
186
|
+
name: s.name,
|
|
187
|
+
description: s.description,
|
|
188
|
+
scoped: s.scoped || (Array.isArray(s.bindings) && s.bindings.length > 0),
|
|
189
|
+
bindings: s.bindings,
|
|
190
|
+
})),
|
|
191
|
+
parseError,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Look for associated MDX documentation file
|
|
195
|
+
const mdxPath = file.replace('.vue', '.mdx')
|
|
196
|
+
if (existsSync(mdxPath)) {
|
|
197
|
+
componentDoc.stories = await parseMdxFile(mdxPath)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
docs.push(componentDoc)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const sortedDocs = docs.sort((a, b) => {
|
|
204
|
+
// Sort by directory first, then by name
|
|
205
|
+
const dirCompare = a.directory.localeCompare(b.directory)
|
|
206
|
+
return dirCompare !== 0 ? dirCompare : a.name.localeCompare(b.name)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return { docs: sortedDocs, typeResolver }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function globAsync(pattern: string, options: any): Promise<string[]> {
|
|
213
|
+
const mod: any = await import('glob')
|
|
214
|
+
const fn = mod.glob ?? mod.default ?? mod
|
|
215
|
+
// Prefer sync API for widest compatibility (glob v8+)
|
|
216
|
+
if (fn && typeof fn.sync === 'function') {
|
|
217
|
+
return fn.sync(pattern, options)
|
|
218
|
+
}
|
|
219
|
+
const res = await fn(pattern, options)
|
|
220
|
+
if (Array.isArray(res))
|
|
221
|
+
return res
|
|
222
|
+
try {
|
|
223
|
+
// Some versions may return an iterable or Glob instance
|
|
224
|
+
if (res && typeof res[Symbol.iterator] === 'function') {
|
|
225
|
+
return Array.from(res)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {}
|
|
229
|
+
return []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function generateTagName(fileName: string): string {
|
|
233
|
+
// Convert file name to PascalCase tag name
|
|
234
|
+
// Components in tela/ directory should preserve the Tela prefix
|
|
235
|
+
// e.g., tela-input -> TelaInput, button -> TelaButton
|
|
236
|
+
|
|
237
|
+
let tagName: string
|
|
238
|
+
|
|
239
|
+
// Check if the file already starts with 'tela-'
|
|
240
|
+
if (fileName.startsWith('tela-')) {
|
|
241
|
+
// Already has tela prefix, just convert to PascalCase
|
|
242
|
+
const parts = fileName.split('-')
|
|
243
|
+
tagName = parts.map(part =>
|
|
244
|
+
part.charAt(0).toUpperCase() + part.slice(1),
|
|
245
|
+
).join('')
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Add Tela prefix for components without it
|
|
249
|
+
const parts = fileName.split('-')
|
|
250
|
+
const componentName = parts.map(part =>
|
|
251
|
+
part.charAt(0).toUpperCase() + part.slice(1),
|
|
252
|
+
).join('')
|
|
253
|
+
tagName = `Tela${componentName}`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return tagName
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function parseMdxFile(mdxPath: string): Promise<StoryDoc | undefined> {
|
|
260
|
+
try {
|
|
261
|
+
const content = readFileSync(mdxPath, 'utf-8')
|
|
262
|
+
|
|
263
|
+
// Extract title from the main heading
|
|
264
|
+
const titleMatch = content.match(/^#\s+(.+)$/m)
|
|
265
|
+
const title = titleMatch ? titleMatch[1] : ''
|
|
266
|
+
|
|
267
|
+
// Extract code examples from ```vue blocks with better heading detection
|
|
268
|
+
const examples: any[] = []
|
|
269
|
+
const codeBlockRegex = /```vue\n([\s\S]*?)\n```/g
|
|
270
|
+
const matches = Array.from(content.matchAll(codeBlockRegex))
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < matches.length; i++) {
|
|
273
|
+
const match = matches[i]
|
|
274
|
+
const codeContent = match[1].trim()
|
|
275
|
+
const codeBlockStart = match.index
|
|
276
|
+
|
|
277
|
+
// Find the closest heading before this code block
|
|
278
|
+
const beforeCode = content.substring(0, codeBlockStart)
|
|
279
|
+
const exampleName = findClosestHeading(beforeCode, i + 1)
|
|
280
|
+
|
|
281
|
+
examples.push({
|
|
282
|
+
name: exampleName,
|
|
283
|
+
code: codeContent,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
title,
|
|
289
|
+
examples,
|
|
290
|
+
argTypes: '',
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error: any) {
|
|
294
|
+
console.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} Failed to parse MDX ${basename(mdxPath)}: ${error.message}${colors.reset}`)
|
|
295
|
+
return undefined
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function findClosestHeading(textBeforeCode: string, exampleIndex: number): string {
|
|
300
|
+
// Split into lines and work backwards to find the closest heading
|
|
301
|
+
const lines = textBeforeCode.split('\n')
|
|
302
|
+
|
|
303
|
+
// Look for headings starting from the end (closest to code block)
|
|
304
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
305
|
+
const line = lines[i].trim()
|
|
306
|
+
|
|
307
|
+
// Check for markdown headings (### or ####)
|
|
308
|
+
const headingMatch = line.match(/^###?\s+(.+)/)
|
|
309
|
+
if (headingMatch) {
|
|
310
|
+
return headingMatch[1].trim()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check for bold text that might be a heading (**text**)
|
|
314
|
+
const boldMatch = line.match(/^\*\*([^*]+)\*\*\s*$/)
|
|
315
|
+
if (boldMatch) {
|
|
316
|
+
return boldMatch[1].trim()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If we hit a higher level heading (## or #), stop looking
|
|
320
|
+
// This prevents us from going too far up the document
|
|
321
|
+
if (line.match(/^##?\s+/) && !line.match(/^###/)) {
|
|
322
|
+
break
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Fallback to generic name
|
|
327
|
+
return `Example ${exampleIndex}`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function generateMarkdown(componentDocs: ComponentDoc[], typeResolver: TypeResolver): string {
|
|
331
|
+
const lines: string[] = []
|
|
332
|
+
|
|
333
|
+
// Header
|
|
334
|
+
lines.push('# tela/build — Components Documentation')
|
|
335
|
+
lines.push('')
|
|
336
|
+
lines.push('> Auto-generated documentation for tela/build components')
|
|
337
|
+
lines.push(`> Generated on ${new Date().toISOString()}`)
|
|
338
|
+
lines.push('')
|
|
339
|
+
|
|
340
|
+
// Group components by directory
|
|
341
|
+
const componentsByDir = groupByDirectory(componentDocs)
|
|
342
|
+
|
|
343
|
+
// Table of Contents
|
|
344
|
+
lines.push('## Table of Contents')
|
|
345
|
+
lines.push('')
|
|
346
|
+
|
|
347
|
+
for (const [dir, components] of Object.entries(componentsByDir)) {
|
|
348
|
+
if (dir && dir !== '.') {
|
|
349
|
+
lines.push(`- **${dir}**`)
|
|
350
|
+
components.forEach((comp) => {
|
|
351
|
+
lines.push(` - [${comp.tagName}](#${comp.tagName.toLowerCase()})`)
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
components.forEach((comp) => {
|
|
356
|
+
lines.push(`- [${comp.tagName}](#${comp.tagName.toLowerCase()})`)
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
lines.push('')
|
|
361
|
+
|
|
362
|
+
lines.push('---')
|
|
363
|
+
lines.push('')
|
|
364
|
+
|
|
365
|
+
// Component sections grouped by directory
|
|
366
|
+
for (const [dir, components] of Object.entries(componentsByDir)) {
|
|
367
|
+
if (dir && dir !== '.') {
|
|
368
|
+
lines.push(`## ${dir}`)
|
|
369
|
+
lines.push('')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const comp of components) {
|
|
373
|
+
lines.push(`### ${comp.tagName}`)
|
|
374
|
+
lines.push('')
|
|
375
|
+
|
|
376
|
+
// Add parse error disclaimer if component failed to parse
|
|
377
|
+
if (comp.parseError) {
|
|
378
|
+
lines.push('> ⚠️ **Documentation incomplete**: This component could not be fully analyzed due to import resolution issues.')
|
|
379
|
+
lines.push('> Props, events, and slots information may be missing or incomplete.')
|
|
380
|
+
lines.push('')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (comp.description) {
|
|
384
|
+
lines.push(comp.description)
|
|
385
|
+
lines.push('')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Props
|
|
389
|
+
if (comp.props && comp.props.length > 0) {
|
|
390
|
+
lines.push('#### Props')
|
|
391
|
+
lines.push('')
|
|
392
|
+
lines.push('```typescript')
|
|
393
|
+
lines.push('interface Props {')
|
|
394
|
+
|
|
395
|
+
comp.props.forEach((prop: any) => {
|
|
396
|
+
const type = formatTypeForInterface(prop.type, typeResolver)
|
|
397
|
+
const required = prop.required ? '' : '?'
|
|
398
|
+
|
|
399
|
+
const commentParts: string[] = []
|
|
400
|
+
const cleanedDescription = sanitizeInlineComment(prop.description)
|
|
401
|
+
if (cleanedDescription) {
|
|
402
|
+
commentParts.push(cleanedDescription)
|
|
403
|
+
}
|
|
404
|
+
if (prop.defaultValue?.value) {
|
|
405
|
+
commentParts.push(`default: ${String(prop.defaultValue.value)}`)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const comment = commentParts.length > 0 ? ` // ${commentParts.join(', ')}` : ''
|
|
409
|
+
|
|
410
|
+
lines.push(` ${prop.name}${required}: ${type}${comment}`)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
lines.push('}')
|
|
414
|
+
lines.push('```')
|
|
415
|
+
lines.push('')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Events
|
|
419
|
+
if (comp.events && comp.events.length > 0) {
|
|
420
|
+
lines.push('#### Events')
|
|
421
|
+
lines.push('')
|
|
422
|
+
lines.push('```typescript')
|
|
423
|
+
lines.push('interface Events {')
|
|
424
|
+
|
|
425
|
+
comp.events.forEach((event: any) => {
|
|
426
|
+
const payload = event.type ? formatTypeForInterface(event.type, typeResolver) : 'any'
|
|
427
|
+
const cleanedDescription = sanitizeInlineComment(event.description)
|
|
428
|
+
const description = cleanedDescription ? ` // ${cleanedDescription}` : ''
|
|
429
|
+
|
|
430
|
+
lines.push(` '${event.name}': (payload: ${payload}) => void${description}`)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
lines.push('}')
|
|
434
|
+
lines.push('```')
|
|
435
|
+
lines.push('')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Slots
|
|
439
|
+
if (comp.slots && comp.slots.length > 0) {
|
|
440
|
+
const hasDefaultSlot = comp.slots.some((slot: any) => slot.name === 'default')
|
|
441
|
+
const namedSlots = comp.slots.filter((slot: any) => slot.name !== 'default')
|
|
442
|
+
|
|
443
|
+
lines.push('#### Slots')
|
|
444
|
+
lines.push('')
|
|
445
|
+
|
|
446
|
+
if (hasDefaultSlot && namedSlots.length === 0) {
|
|
447
|
+
// Only has default slot
|
|
448
|
+
lines.push('Supports slots')
|
|
449
|
+
}
|
|
450
|
+
else if (hasDefaultSlot && namedSlots.length > 0) {
|
|
451
|
+
// Has default slot plus named slots
|
|
452
|
+
lines.push('Supports slots with additional named slots:')
|
|
453
|
+
lines.push('')
|
|
454
|
+
namedSlots.forEach((slot: any) => {
|
|
455
|
+
const description = slot.description ? ` - ${slot.description}` : ''
|
|
456
|
+
const scopedProps = slot.scoped && slot.bindings ? ` (scoped: ${formatScopedPropsInline(slot.bindings, typeResolver)})` : ''
|
|
457
|
+
lines.push(`- \`${slot.name}\`${description}${scopedProps}`)
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
else if (namedSlots.length > 0) {
|
|
461
|
+
// Only named slots, no default
|
|
462
|
+
lines.push('Named slots:')
|
|
463
|
+
lines.push('')
|
|
464
|
+
namedSlots.forEach((slot: any) => {
|
|
465
|
+
const description = slot.description ? ` - ${slot.description}` : ''
|
|
466
|
+
const scopedProps = slot.scoped && slot.bindings ? ` (scoped: ${formatScopedPropsInline(slot.bindings, typeResolver)})` : ''
|
|
467
|
+
lines.push(`- \`${slot.name}\`${description}${scopedProps}`)
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
lines.push('')
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// MDX examples
|
|
475
|
+
if (comp.stories && comp.stories.examples.length > 0) {
|
|
476
|
+
lines.push('#### Examples')
|
|
477
|
+
lines.push('')
|
|
478
|
+
|
|
479
|
+
comp.stories.examples.forEach((example) => {
|
|
480
|
+
lines.push(`**${example.name}**`)
|
|
481
|
+
lines.push('')
|
|
482
|
+
lines.push('```vue')
|
|
483
|
+
lines.push(example.code)
|
|
484
|
+
lines.push('```')
|
|
485
|
+
lines.push('')
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
lines.push('---')
|
|
490
|
+
lines.push('')
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return lines.join('\n')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Generate a directory-based documentation set suitable for Claude Skills-style usage:
|
|
499
|
+
* - Root index at `${outDir}/index.md`
|
|
500
|
+
* - Per-component pages under `${outDir}/components/<original-dir?>/<TagName>.md`
|
|
501
|
+
* - Design tokens documentation embedded in SKILL.md
|
|
502
|
+
*/
|
|
503
|
+
export function generateDocsToDirectory(componentDocs: ComponentDoc[], typeResolver: TypeResolver, outDir: string, layerPath?: string): void {
|
|
504
|
+
// Ensure root directory exists
|
|
505
|
+
if (!existsSync(outDir)) {
|
|
506
|
+
mkdirSync(outDir, { recursive: true })
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Single Skill: tela-build
|
|
510
|
+
const skillDir = join(outDir, 'tela-build')
|
|
511
|
+
const supportingDir = join(skillDir, 'components')
|
|
512
|
+
if (!existsSync(supportingDir)) {
|
|
513
|
+
mkdirSync(supportingDir, { recursive: true })
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Group components by family (e.g., tela-dropdown-*) → tela-dropdown.md
|
|
517
|
+
const groups = new Map<string, ComponentDoc[]>()
|
|
518
|
+
for (const comp of componentDocs) {
|
|
519
|
+
const kebab = toKebabFromTag(comp.tagName)
|
|
520
|
+
const group = toGroupSlugFromKebab(kebab)
|
|
521
|
+
if (!groups.has(group))
|
|
522
|
+
groups.set(group, [])
|
|
523
|
+
groups.get(group)!.push(comp)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Sort groups by name and components by tagName
|
|
527
|
+
const sortedGroups = Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]))
|
|
528
|
+
sortedGroups.forEach(([_, list]) => list.sort((a, b) => a.tagName.localeCompare(b.tagName)))
|
|
529
|
+
|
|
530
|
+
const componentLinks: string[] = []
|
|
531
|
+
|
|
532
|
+
for (const [groupSlug, comps] of sortedGroups) {
|
|
533
|
+
const targetFile = join(supportingDir, `${groupSlug}.md`)
|
|
534
|
+
const content = generateGroupMarkdown(groupSlug, comps, typeResolver)
|
|
535
|
+
writeFileSync(targetFile, content, 'utf-8')
|
|
536
|
+
const title = toTitleFromGroupSlug(groupSlug)
|
|
537
|
+
componentLinks.push(`- [${title}](components/${groupSlug}.md)`)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Read design-tokens.md content if it exists
|
|
541
|
+
let designTokensContent = ''
|
|
542
|
+
if (layerPath) {
|
|
543
|
+
const designTokensSourcePath = join(layerPath, 'docs', 'design-tokens.md')
|
|
544
|
+
if (existsSync(designTokensSourcePath)) {
|
|
545
|
+
designTokensContent = readFileSync(designTokensSourcePath, 'utf-8')
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Create SKILL.md describing Tela Build with links to supporting md
|
|
550
|
+
const skillDescription = buildTelaBuildSkillDescription()
|
|
551
|
+
const designTokensSection = designTokensContent
|
|
552
|
+
? `\n\n---\n\n${designTokensContent}`
|
|
553
|
+
: ''
|
|
554
|
+
const body = dedent`
|
|
555
|
+
# Tela Build
|
|
556
|
+
|
|
557
|
+
This Skill provides structured documentation for the Tela Build component library (Vue 3 + Nuxt).
|
|
558
|
+
Use it when building, refactoring, or using Tela components — props, events, slots, and examples are included where available.
|
|
559
|
+
|
|
560
|
+
## Instructions
|
|
561
|
+
|
|
562
|
+
- IMPORTANT: Before making ANY UI change or introducing NEW UI, ALWAYS consult this Tela Build components index first.
|
|
563
|
+
- Prefer reusing existing Tela components and patterns. Only create new components when a clear gap exists.
|
|
564
|
+
- When a new component is needed, align with existing naming, props, events, and patterns documented here.
|
|
565
|
+
- Keep this documentation current when components are added or changed (update supporting files under \
|
|
566
|
+
\`components/\`).
|
|
567
|
+
|
|
568
|
+
## Components Index
|
|
569
|
+
|
|
570
|
+
${componentLinks.join('\n')}${designTokensSection}
|
|
571
|
+
`
|
|
572
|
+
|
|
573
|
+
const skillMd = wrapWithSkillFrontmatter({ name: 'tela-build', description: skillDescription }, body)
|
|
574
|
+
writeFileSync(join(skillDir, 'SKILL.md'), skillMd, 'utf-8')
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeResolver, opts?: { includeTitle?: boolean }): string {
|
|
578
|
+
const lines: string[] = []
|
|
579
|
+
if (opts?.includeTitle !== false) {
|
|
580
|
+
lines.push(`# ${comp.tagName}`)
|
|
581
|
+
lines.push('')
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (comp.parseError) {
|
|
585
|
+
lines.push('> ⚠️ **Documentation incomplete**: This component could not be fully analyzed due to import resolution issues.')
|
|
586
|
+
lines.push('> Props, events, and slots information may be missing or incomplete.')
|
|
587
|
+
lines.push('')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (comp.description) {
|
|
591
|
+
lines.push(comp.description)
|
|
592
|
+
lines.push('')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Props
|
|
596
|
+
if (comp.props && comp.props.length > 0) {
|
|
597
|
+
lines.push('## Props')
|
|
598
|
+
lines.push('')
|
|
599
|
+
lines.push('```typescript')
|
|
600
|
+
lines.push('interface Props {')
|
|
601
|
+
comp.props.forEach((prop: any) => {
|
|
602
|
+
const type = formatTypeForInterface(prop.type, typeResolver)
|
|
603
|
+
const required = prop.required ? '' : '?'
|
|
604
|
+
|
|
605
|
+
const commentParts: string[] = []
|
|
606
|
+
const cleanedDescription = sanitizeInlineComment(prop.description)
|
|
607
|
+
if (cleanedDescription)
|
|
608
|
+
commentParts.push(cleanedDescription)
|
|
609
|
+
if (prop.defaultValue?.value)
|
|
610
|
+
commentParts.push(`default: ${String(prop.defaultValue.value)}`)
|
|
611
|
+
const comment = commentParts.length > 0 ? ` // ${commentParts.join(', ')}` : ''
|
|
612
|
+
lines.push(` ${prop.name}${required}: ${type}${comment}`)
|
|
613
|
+
})
|
|
614
|
+
lines.push('}')
|
|
615
|
+
lines.push('```')
|
|
616
|
+
lines.push('')
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Events
|
|
620
|
+
if (comp.events && comp.events.length > 0) {
|
|
621
|
+
lines.push('## Events')
|
|
622
|
+
lines.push('')
|
|
623
|
+
lines.push('| Event | Type | Description |')
|
|
624
|
+
lines.push('|------:|:-----|:------------|')
|
|
625
|
+
comp.events.forEach((event: any) => {
|
|
626
|
+
const name = escapeTableCell(event.name)
|
|
627
|
+
const type = escapeTableCell(formatType(event.type))
|
|
628
|
+
const description = escapeTableCell(event.description || '')
|
|
629
|
+
lines.push(`| \`${name}\` | ${type} | ${description} |`)
|
|
630
|
+
})
|
|
631
|
+
lines.push('')
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Slots
|
|
635
|
+
if (comp.slots && comp.slots.length > 0) {
|
|
636
|
+
lines.push('## Slots')
|
|
637
|
+
lines.push('')
|
|
638
|
+
lines.push('| Slot | Scoped Props | Description |')
|
|
639
|
+
lines.push('|-----:|:-------------|:------------|')
|
|
640
|
+
comp.slots.forEach((slot: any) => {
|
|
641
|
+
const name = escapeTableCell(slot.name)
|
|
642
|
+
const scoped = escapeTableCell(formatScopedPropsInline(slot.bindings || [], typeResolver))
|
|
643
|
+
const description = escapeTableCell(slot.description || '')
|
|
644
|
+
lines.push(`| \`${name}\` | ${scoped || '—'} | ${description} |`)
|
|
645
|
+
})
|
|
646
|
+
lines.push('')
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Examples (from MDX)
|
|
650
|
+
if (comp.stories && comp.stories.examples && comp.stories.examples.length > 0) {
|
|
651
|
+
lines.push('## Examples')
|
|
652
|
+
lines.push('')
|
|
653
|
+
comp.stories.examples.forEach((ex: any) => {
|
|
654
|
+
lines.push(`### ${ex.name}`)
|
|
655
|
+
lines.push('')
|
|
656
|
+
lines.push('```vue')
|
|
657
|
+
lines.push(ex.code)
|
|
658
|
+
lines.push('```')
|
|
659
|
+
lines.push('')
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return `${lines.join('\n')}\n`
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function toKebabFromTag(tagName: string): string {
|
|
667
|
+
// Convert PascalCase or camelCase to kebab-case
|
|
668
|
+
return tagName
|
|
669
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
670
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
671
|
+
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
672
|
+
.toLowerCase()
|
|
673
|
+
.replace(/--+/g, '-')
|
|
674
|
+
.replace(/^-+|-+$/g, '')
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function wrapWithSkillFrontmatter(meta: { name: string; description: string; allowedTools?: string[] }, body: string): string {
|
|
678
|
+
const { name, description, allowedTools } = meta
|
|
679
|
+
const lines: string[] = []
|
|
680
|
+
lines.push('---')
|
|
681
|
+
lines.push(`name: ${sanitizeSkillName(name)}`)
|
|
682
|
+
lines.push(`description: ${sanitizeDescription(description)}`)
|
|
683
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
684
|
+
lines.push(`allowed-tools: ${allowedTools.join(', ')}`)
|
|
685
|
+
}
|
|
686
|
+
lines.push('---')
|
|
687
|
+
lines.push('')
|
|
688
|
+
lines.push(body.trimStart())
|
|
689
|
+
if (!body.endsWith('\n'))
|
|
690
|
+
lines.push('')
|
|
691
|
+
return lines.join('\n')
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function sanitizeSkillName(name: string): string {
|
|
695
|
+
// Lowercase + hyphens + digits only, max 64 chars
|
|
696
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 64).replace(/--+/g, '-')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function sanitizeDescription(desc: string): string {
|
|
700
|
+
const trimmed = desc.replace(/\s+/g, ' ').trim()
|
|
701
|
+
return trimmed.slice(0, 1024)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function buildTelaBuildSkillDescription(): string {
|
|
705
|
+
const base = 'Documentation and usage references for the Tela Build component library (Vue 3 + Nuxt). '
|
|
706
|
+
+ 'Always consult this Tela Build index BEFORE making any UI changes or creating new UI. '
|
|
707
|
+
+ 'Use when working on UI in this repository: ask for component props, events, slots, or examples. '
|
|
708
|
+
+ 'Supporting files under components/ contain per-component details.'
|
|
709
|
+
return sanitizeDescription(base)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function toGroupSlugFromKebab(kebab: string): string {
|
|
713
|
+
const parts = kebab.split('-').filter(Boolean)
|
|
714
|
+
if (parts.length <= 2)
|
|
715
|
+
return kebab
|
|
716
|
+
// Prefer first two parts (e.g., tela-dropdown), otherwise fallback to entire kebab
|
|
717
|
+
return `${parts[0]}-${parts[1]}`
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function toTitleFromGroupSlug(groupSlug: string): string {
|
|
721
|
+
return groupSlug
|
|
722
|
+
.split('-')
|
|
723
|
+
.map(p => p.charAt(0).toUpperCase() + p.slice(1))
|
|
724
|
+
.join('')
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function generateGroupMarkdown(groupSlug: string, comps: ComponentDoc[], typeResolver: TypeResolver): string {
|
|
728
|
+
const title = toTitleFromGroupSlug(groupSlug)
|
|
729
|
+
const sections = comps.map((comp) => {
|
|
730
|
+
const section = generateSingleComponentMarkdown(comp, typeResolver, { includeTitle: false }).trim()
|
|
731
|
+
return dedent`
|
|
732
|
+
## ${comp.tagName}
|
|
733
|
+
|
|
734
|
+
${section}
|
|
735
|
+
`
|
|
736
|
+
}).join('\n')
|
|
737
|
+
|
|
738
|
+
return dedent`
|
|
739
|
+
# ${title}
|
|
740
|
+
|
|
741
|
+
This page groups related components in the same family.
|
|
742
|
+
|
|
743
|
+
${sections}
|
|
744
|
+
`
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Small utility to write readable multi-line strings in code
|
|
748
|
+
function dedent(strings: TemplateStringsArray, ...values: any[]): string {
|
|
749
|
+
let result = ''
|
|
750
|
+
for (let i = 0; i < strings.length; i++) {
|
|
751
|
+
result += strings[i].replace(/\r/g, '')
|
|
752
|
+
if (i < values.length)
|
|
753
|
+
result += String(values[i])
|
|
754
|
+
}
|
|
755
|
+
result = result.replace(/^\n/, '')
|
|
756
|
+
const lines = result.split('\n')
|
|
757
|
+
// Determine minimum indentation (skip empty lines)
|
|
758
|
+
let minIndent: number | null = null
|
|
759
|
+
for (const line of lines) {
|
|
760
|
+
if (!line.trim())
|
|
761
|
+
continue
|
|
762
|
+
const m = line.match(/^\s+/)
|
|
763
|
+
const indent = m ? m[0].length : 0
|
|
764
|
+
if (minIndent === null || indent < minIndent)
|
|
765
|
+
minIndent = indent
|
|
766
|
+
}
|
|
767
|
+
if (minIndent && minIndent > 0) {
|
|
768
|
+
for (let i = 0; i < lines.length; i++) {
|
|
769
|
+
lines[i] = lines[i].startsWith(' '.repeat(minIndent)) ? lines[i].slice(minIndent) : lines[i]
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return `${lines.join('\n').replace(/[ \t]+$/gm, '').trim()}\n`
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function groupByDirectory(components: ComponentDoc[]): Record<string, ComponentDoc[]> {
|
|
776
|
+
const grouped: Record<string, ComponentDoc[]> = {}
|
|
777
|
+
|
|
778
|
+
components.forEach((comp) => {
|
|
779
|
+
const dir = comp.directory || 'root'
|
|
780
|
+
if (!grouped[dir]) {
|
|
781
|
+
grouped[dir] = []
|
|
782
|
+
}
|
|
783
|
+
grouped[dir].push(comp)
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
// Sort directories
|
|
787
|
+
const sortedGrouped: Record<string, ComponentDoc[]> = {}
|
|
788
|
+
Object.keys(grouped).sort().forEach((key) => {
|
|
789
|
+
sortedGrouped[key] = grouped[key]
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
return sortedGrouped
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function sanitizeInlineComment(value?: string): string {
|
|
796
|
+
if (!value)
|
|
797
|
+
return ''
|
|
798
|
+
|
|
799
|
+
const withoutMarkdown = value.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
|
|
800
|
+
const singleLine = withoutMarkdown.replace(/\s+/g, ' ').trim()
|
|
801
|
+
if (!singleLine)
|
|
802
|
+
return ''
|
|
803
|
+
|
|
804
|
+
const sentenceMatch = singleLine.match(/^(.*?\.)\s/)
|
|
805
|
+
if (sentenceMatch) {
|
|
806
|
+
return sentenceMatch[1].trim()
|
|
807
|
+
}
|
|
808
|
+
return singleLine
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function formatType(type: any): string {
|
|
812
|
+
if (!type)
|
|
813
|
+
return '`any`'
|
|
814
|
+
|
|
815
|
+
if (typeof type === 'string')
|
|
816
|
+
return `\`${type}\``
|
|
817
|
+
|
|
818
|
+
if (type.name) {
|
|
819
|
+
if (type.name === 'union' && type.value) {
|
|
820
|
+
const values = type.value.map((v: any) => {
|
|
821
|
+
if (typeof v === 'string')
|
|
822
|
+
return v
|
|
823
|
+
return v.name || v
|
|
824
|
+
}).join(' \\| ')
|
|
825
|
+
return `\`${values}\``
|
|
826
|
+
}
|
|
827
|
+
return `\`${type.name}\``
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (Array.isArray(type)) {
|
|
831
|
+
const types = type.map((t) => {
|
|
832
|
+
const formatted = formatType(t)
|
|
833
|
+
// Remove backticks if already present to avoid double backticks
|
|
834
|
+
return formatted.replace(/`/g, '')
|
|
835
|
+
}).join(' \\| ')
|
|
836
|
+
return `\`${types}\``
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return '`any`'
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function escapeTableCell(content: string): string {
|
|
843
|
+
// Escape pipes and newlines for markdown table cells
|
|
844
|
+
return content
|
|
845
|
+
.replace(/\|/g, '\\|')
|
|
846
|
+
.replace(/\n/g, ' ')
|
|
847
|
+
.replace(/\r/g, '')
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function formatTypeForInterface(type: any, typeResolver: TypeResolver): string {
|
|
851
|
+
if (!type)
|
|
852
|
+
return 'any'
|
|
853
|
+
|
|
854
|
+
if (typeof type === 'string') {
|
|
855
|
+
// Try to resolve the type using our TypeScript resolver
|
|
856
|
+
const resolved = typeResolver.resolveType(type)
|
|
857
|
+
return resolved !== type ? resolved : type
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (type.name) {
|
|
861
|
+
// First try to resolve using our TypeScript resolver
|
|
862
|
+
const resolved = typeResolver.resolveType(type.name)
|
|
863
|
+
if (resolved !== type.name) {
|
|
864
|
+
return resolved
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (type.name === 'union' && type.value) {
|
|
868
|
+
const values = type.value.map((v: any) => {
|
|
869
|
+
if (typeof v === 'string')
|
|
870
|
+
return `'${v}'`
|
|
871
|
+
if (v.name)
|
|
872
|
+
return `'${v.name}'`
|
|
873
|
+
return v
|
|
874
|
+
}).join(' | ')
|
|
875
|
+
return values
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Handle common Vue prop types
|
|
879
|
+
if (type.name === 'Array')
|
|
880
|
+
return 'Array<any>'
|
|
881
|
+
if (type.name === 'Function')
|
|
882
|
+
return '(...args: any[]) => any'
|
|
883
|
+
if (type.name === 'Object')
|
|
884
|
+
return 'Record<string, any>'
|
|
885
|
+
|
|
886
|
+
// Use TypeScript resolver for unknown types
|
|
887
|
+
return typeResolver.resolveType(type.name)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (Array.isArray(type)) {
|
|
891
|
+
const types = type.map(t => formatTypeForInterface(t, typeResolver)).join(' | ')
|
|
892
|
+
return types
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return 'any'
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function formatScopedPropsInline(bindings: any[], typeResolver: TypeResolver): string {
|
|
899
|
+
if (!bindings || bindings.length === 0)
|
|
900
|
+
return ''
|
|
901
|
+
|
|
902
|
+
return bindings.map(b => `${b.name}: ${formatTypeForInterface(b.type, typeResolver)}`).join(', ')
|
|
903
|
+
}
|