@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.
Files changed (295) hide show
  1. package/README.md +75 -0
  2. package/app.config.ts +73 -0
  3. package/components/tela/animated/animated-calculating-number.vue +16 -0
  4. package/components/tela/animated/animated-number.mdx +248 -0
  5. package/components/tela/animated/animated-number.stories.ts +52 -0
  6. package/components/tela/animated/animated-number.vue +23 -0
  7. package/components/tela/animated/animated-text.vue +124 -0
  8. package/components/tela/animated/animated-value.vue +68 -0
  9. package/components/tela/avatar/avatar.mdx +117 -0
  10. package/components/tela/avatar/avatar.stories.ts +62 -0
  11. package/components/tela/avatar/avatar.vue +71 -0
  12. package/components/tela/avatar/group/avatar-group.stories.ts +78 -0
  13. package/components/tela/avatar/group/avatar-group.vue +46 -0
  14. package/components/tela/badge/badge.mdx +154 -0
  15. package/components/tela/badge/badge.stories.ts +82 -0
  16. package/components/tela/badge/badge.vue +41 -0
  17. package/components/tela/button/button.mdx +155 -0
  18. package/components/tela/button/button.stories.ts +202 -0
  19. package/components/tela/button/button.vue +107 -0
  20. package/components/tela/card.vue +30 -0
  21. package/components/tela/chart/chart-bar.vue +58 -0
  22. package/components/tela/chat/chat.mdx +268 -0
  23. package/components/tela/chat/chat.stories.ts +253 -0
  24. package/components/tela/chat/command/index.vue +41 -0
  25. package/components/tela/chat/command/mention/index.vue +138 -0
  26. package/components/tela/chat/index.vue +112 -0
  27. package/components/tela/chat/pure-text-input/chat-text-input.vue +190 -0
  28. package/components/tela/chat/text-input/chat-text-input.stories.ts +128 -0
  29. package/components/tela/chat/text-input/index.vue +217 -0
  30. package/components/tela/chat/text-message/chat-text-message.stories.ts +138 -0
  31. package/components/tela/chat/text-message/index.vue +355 -0
  32. package/components/tela/chat/types.ts +19 -0
  33. package/components/tela/checkbox/checkbox-card.vue +30 -0
  34. package/components/tela/checkbox/checkbox.mdx +164 -0
  35. package/components/tela/checkbox/checkbox.stories.ts +104 -0
  36. package/components/tela/checkbox/checkbox.vue +43 -0
  37. package/components/tela/collapsible/Collapsible.vue +15 -0
  38. package/components/tela/collapsible/CollapsibleContent.vue +59 -0
  39. package/components/tela/collapsible/CollapsibleTrigger.vue +12 -0
  40. package/components/tela/collapsible/collapsible.mdx +157 -0
  41. package/components/tela/collapsible-section/collapsible-section.mdx +180 -0
  42. package/components/tela/collapsible-section/collapsible-section.stories.ts +53 -0
  43. package/components/tela/collapsible-section/collapsible-section.vue +51 -0
  44. package/components/tela/collapsible-section-with-actions.vue +98 -0
  45. package/components/tela/combobox/combobox-anchor.vue +24 -0
  46. package/components/tela/combobox/combobox-empty.vue +19 -0
  47. package/components/tela/combobox/combobox-group.vue +24 -0
  48. package/components/tela/combobox/combobox-indicator.vue +22 -0
  49. package/components/tela/combobox/combobox-input.vue +31 -0
  50. package/components/tela/combobox/combobox-item.vue +28 -0
  51. package/components/tela/combobox/combobox-label.vue +24 -0
  52. package/components/tela/combobox/combobox-list.vue +90 -0
  53. package/components/tela/combobox/combobox-module-selector.vue +366 -0
  54. package/components/tela/combobox/combobox-root.vue +15 -0
  55. package/components/tela/combobox/combobox-trigger.vue +12 -0
  56. package/components/tela/combobox/combobox.mdx +285 -0
  57. package/components/tela/combobox/combobox.stories.ts +232 -0
  58. package/components/tela/combobox/combobox.vue +497 -0
  59. package/components/tela/command/command-dialog.vue +22 -0
  60. package/components/tela/command/command-empty.vue +25 -0
  61. package/components/tela/command/command-group.vue +46 -0
  62. package/components/tela/command/command-input.vue +38 -0
  63. package/components/tela/command/command-item.vue +78 -0
  64. package/components/tela/command/command-list.vue +78 -0
  65. package/components/tela/command/command-separator.vue +23 -0
  66. package/components/tela/command/command-shortcut.vue +13 -0
  67. package/components/tela/command/command.vue +88 -0
  68. package/components/tela/command/dialog-base.vue +15 -0
  69. package/components/tela/command/dialog-content.vue +50 -0
  70. package/components/tela/command/utils.ts +15 -0
  71. package/components/tela/complex-table/complex-table-cell.stories.ts +145 -0
  72. package/components/tela/complex-table/complex-table-cell.vue +45 -0
  73. package/components/tela/complex-table/complex-table-header-cell.stories.ts +103 -0
  74. package/components/tela/complex-table/complex-table-header-cell.vue +48 -0
  75. package/components/tela/complex-table/complex-table-header.stories.ts +89 -0
  76. package/components/tela/complex-table/complex-table-header.vue +70 -0
  77. package/components/tela/complex-table/complex-table-row.vue +199 -0
  78. package/components/tela/complex-table/complex-table-virtualized.vue +326 -0
  79. package/components/tela/complex-table/complex-table.stories.ts +358 -0
  80. package/components/tela/complex-table/complex-table.vue +237 -0
  81. package/components/tela/complex-table/composables/table-common.ts +93 -0
  82. package/components/tela/complex-table/composables/table-selection.ts +87 -0
  83. package/components/tela/complex-table/composables/virtual-scroll.ts +252 -0
  84. package/components/tela/complex-table/styles/table-shared.css +170 -0
  85. package/components/tela/complex-table/types.ts +63 -0
  86. package/components/tela/complex-table/utils.ts +35 -0
  87. package/components/tela/confirm-button/confirm-button.vue +137 -0
  88. package/components/tela/confirmation-modal/confirmation-modal.vue +72 -0
  89. package/components/tela/copy-button.vue +86 -0
  90. package/components/tela/date-range-picker.vue +221 -0
  91. package/components/tela/dialog/dialog.mdx +170 -0
  92. package/components/tela/dialog/dialog.vue +182 -0
  93. package/components/tela/disabled-area.vue +16 -0
  94. package/components/tela/disclaimer/disclaimer.mdx +238 -0
  95. package/components/tela/disclaimer/disclaimer.stories.ts +196 -0
  96. package/components/tela/disclaimer/disclaimer.vue +125 -0
  97. package/components/tela/dropdown-menu/DropdownMenu.vue +121 -0
  98. package/components/tela/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
  99. package/components/tela/dropdown-menu/DropdownMenuContent.vue +75 -0
  100. package/components/tela/dropdown-menu/DropdownMenuGroup.vue +12 -0
  101. package/components/tela/dropdown-menu/DropdownMenuItem.vue +137 -0
  102. package/components/tela/dropdown-menu/DropdownMenuLabel.vue +26 -0
  103. package/components/tela/dropdown-menu/DropdownMenuRadioGroup.vue +18 -0
  104. package/components/tela/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  105. package/components/tela/dropdown-menu/DropdownMenuRoot.vue +15 -0
  106. package/components/tela/dropdown-menu/DropdownMenuSeparator.vue +21 -0
  107. package/components/tela/dropdown-menu/DropdownMenuShortcut.vue +14 -0
  108. package/components/tela/dropdown-menu/DropdownMenuSub.vue +18 -0
  109. package/components/tela/dropdown-menu/DropdownMenuSubContent.vue +30 -0
  110. package/components/tela/dropdown-menu/DropdownMenuSubTrigger.vue +35 -0
  111. package/components/tela/dropdown-menu/DropdownMenuTrigger.vue +14 -0
  112. package/components/tela/dropdown-menu/dropdown-menu.mdx +265 -0
  113. package/components/tela/dropdown-menu/dropdown-menu.stories.ts +156 -0
  114. package/components/tela/expandable-input.vue +96 -0
  115. package/components/tela/file-drop.vue +37 -0
  116. package/components/tela/file-upload/file-upload.mdx +189 -0
  117. package/components/tela/file-upload/file-upload.stories.ts +48 -0
  118. package/components/tela/file-upload/file-upload.vue +205 -0
  119. package/components/tela/filters/checkbox-filter.stories.ts +218 -0
  120. package/components/tela/filters/checkbox-filter.vue +165 -0
  121. package/components/tela/filters/date-filter.stories.ts +258 -0
  122. package/components/tela/filters/date-filter.vue +200 -0
  123. package/components/tela/filters/user-filter.stories.ts +344 -0
  124. package/components/tela/filters/user-filter.vue +271 -0
  125. package/components/tela/hover-card/hover-card.mdx +221 -0
  126. package/components/tela/hover-card/hover-card.stories.ts +87 -0
  127. package/components/tela/hover-card/hover-card.vue +61 -0
  128. package/components/tela/icon/custom.vue +319 -0
  129. package/components/tela/icon/spinner.vue +12 -0
  130. package/components/tela/icon-button/icon-button.vue +114 -0
  131. package/components/tela/icon.vue +37 -0
  132. package/components/tela/initials.vue +28 -0
  133. package/components/tela/inline-input.vue +77 -0
  134. package/components/tela/input/input.mdx +182 -0
  135. package/components/tela/input/input.stories.ts +153 -0
  136. package/components/tela/input/tela-input.vue +240 -0
  137. package/components/tela/kbd/kbd-return.vue +6 -0
  138. package/components/tela/kbd/kbd.mdx +238 -0
  139. package/components/tela/kbd/kbd.vue +18 -0
  140. package/components/tela/label/label.mdx +121 -0
  141. package/components/tela/label/label.stories.ts +37 -0
  142. package/components/tela/label/label.vue +25 -0
  143. package/components/tela/link-decoration/link-decoration.vue +19 -0
  144. package/components/tela/live-label.vue +32 -0
  145. package/components/tela/long-press-button.vue +98 -0
  146. package/components/tela/menubar/menubar-content.vue +77 -0
  147. package/components/tela/menubar/menubar-item.vue +32 -0
  148. package/components/tela/menubar/menubar-label.vue +14 -0
  149. package/components/tela/menubar/menubar-menu.vue +12 -0
  150. package/components/tela/menubar/menubar-root.vue +30 -0
  151. package/components/tela/menubar/menubar-separator.vue +17 -0
  152. package/components/tela/menubar/menubar-shortcut.vue +14 -0
  153. package/components/tela/menubar/menubar-sub-content.vue +36 -0
  154. package/components/tela/menubar/menubar-sub-trigger.vue +28 -0
  155. package/components/tela/menubar/menubar-sub.vue +20 -0
  156. package/components/tela/menubar/menubar-trigger.vue +27 -0
  157. package/components/tela/menubar/menubar.vue +298 -0
  158. package/components/tela/modal/modal.mdx +145 -0
  159. package/components/tela/modal/modal.vue +242 -0
  160. package/components/tela/multiple-select/multiple-select.mdx +274 -0
  161. package/components/tela/multiple-select/multiple-select.stories.ts +325 -0
  162. package/components/tela/multiple-select/multiple-select.vue +666 -0
  163. package/components/tela/pane.vue +110 -0
  164. package/components/tela/popover/popover-content.vue +48 -0
  165. package/components/tela/popover/popover-trigger.vue +12 -0
  166. package/components/tela/popover/popover.mdx +239 -0
  167. package/components/tela/popover/popover.stories.ts +150 -0
  168. package/components/tela/popover/popover.vue +15 -0
  169. package/components/tela/popover-list/popover-list-nested.vue +104 -0
  170. package/components/tela/popover-list/popover-list.stories.ts +330 -0
  171. package/components/tela/popover-list/popover-list.vue +191 -0
  172. package/components/tela/radio-button.vue +66 -0
  173. package/components/tela/radio-group/radio-group-item.vue +40 -0
  174. package/components/tela/radio-group/radio-group-root.vue +26 -0
  175. package/components/tela/radio-group/radio-group.mdx +78 -0
  176. package/components/tela/radio-group/radio-group.stories.ts +106 -0
  177. package/components/tela/radio-group/radio-group.vue +23 -0
  178. package/components/tela/range-calendar.stories.ts +110 -0
  179. package/components/tela/range-calendar.vue +109 -0
  180. package/components/tela/scroll-area/scroll-area.mdx +183 -0
  181. package/components/tela/scroll-area/scroll-area.vue +30 -0
  182. package/components/tela/scroll-area/scroll-bar.vue +31 -0
  183. package/components/tela/segment-toggle.stories.ts +114 -0
  184. package/components/tela/segment-toggle.vue +66 -0
  185. package/components/tela/select-menu/select-menu-content.vue +106 -0
  186. package/components/tela/select-menu/select-menu-down-button.vue +20 -0
  187. package/components/tela/select-menu/select-menu-group.vue +16 -0
  188. package/components/tela/select-menu/select-menu-item.vue +40 -0
  189. package/components/tela/select-menu/select-menu-root.vue +15 -0
  190. package/components/tela/select-menu/select-menu-trigger.vue +34 -0
  191. package/components/tela/select-menu/select-menu-up-button.vue +20 -0
  192. package/components/tela/select-menu/select-menu-value.vue +12 -0
  193. package/components/tela/select-menu/select-menu.mdx +221 -0
  194. package/components/tela/select-menu/select-menu.stories.ts +91 -0
  195. package/components/tela/select-menu/select-menu.vue +165 -0
  196. package/components/tela/selector/selector.vue +47 -0
  197. package/components/tela/sheet/sheet-close.vue +12 -0
  198. package/components/tela/sheet/sheet-content.vue +57 -0
  199. package/components/tela/sheet/sheet-description.vue +23 -0
  200. package/components/tela/sheet/sheet-footer.vue +18 -0
  201. package/components/tela/sheet/sheet-header.vue +15 -0
  202. package/components/tela/sheet/sheet-root.vue +18 -0
  203. package/components/tela/sheet/sheet-title.vue +23 -0
  204. package/components/tela/sheet/sheet-trigger.vue +12 -0
  205. package/components/tela/sheet/sheet.client.vue +150 -0
  206. package/components/tela/sheet/sheet.mdx +176 -0
  207. package/components/tela/sheet/sheet.stories.ts +201 -0
  208. package/components/tela/sheet/variants.ts +22 -0
  209. package/components/tela/side-sheet/side-sheet.mdx +131 -0
  210. package/components/tela/side-sheet/side-sheet.stories.ts +134 -0
  211. package/components/tela/side-sheet/side-sheet.vue +106 -0
  212. package/components/tela/skeleton/skeleton.mdx +165 -0
  213. package/components/tela/skeleton/skeleton.stories.ts +35 -0
  214. package/components/tela/skeleton/skeleton.vue +45 -0
  215. package/components/tela/skeleton-icon.vue +24 -0
  216. package/components/tela/span.vue +24 -0
  217. package/components/tela/star-button.vue +70 -0
  218. package/components/tela/status/status-lean.vue +30 -0
  219. package/components/tela/status/status.mdx +187 -0
  220. package/components/tela/status/status.stories.ts +160 -0
  221. package/components/tela/status/status.vue +420 -0
  222. package/components/tela/status-bar/status-bar.mdx +178 -0
  223. package/components/tela/status-bar/status-bar.stories.ts +64 -0
  224. package/components/tela/status-bar/status-bar.vue +56 -0
  225. package/components/tela/status-bar/types.ts +5 -0
  226. package/components/tela/switch/switch.mdx +118 -0
  227. package/components/tela/switch/switch.stories.ts +80 -0
  228. package/components/tela/switch/switch.vue +56 -0
  229. package/components/tela/table/table-body.vue +13 -0
  230. package/components/tela/table/table-caption.vue +13 -0
  231. package/components/tela/table/table-cell.vue +20 -0
  232. package/components/tela/table/table-empty.vue +37 -0
  233. package/components/tela/table/table-footer.vue +13 -0
  234. package/components/tela/table/table-head.vue +13 -0
  235. package/components/tela/table/table-header.vue +13 -0
  236. package/components/tela/table/table-row.vue +13 -0
  237. package/components/tela/table/table.mdx +230 -0
  238. package/components/tela/table/table.stories.ts +384 -0
  239. package/components/tela/table/table.vue +15 -0
  240. package/components/tela/tabs/tabs-content.vue +20 -0
  241. package/components/tela/tabs/tabs-indicator.vue +22 -0
  242. package/components/tela/tabs/tabs-list.vue +23 -0
  243. package/components/tela/tabs/tabs-root.vue +15 -0
  244. package/components/tela/tabs/tabs-trigger.vue +27 -0
  245. package/components/tela/tabs/tabs.mdx +138 -0
  246. package/components/tela/tabs/tabs.stories.ts +72 -0
  247. package/components/tela/tabs/tabs.vue +61 -0
  248. package/components/tela/tags/tags-select.mdx +318 -0
  249. package/components/tela/tags/tags-select.stories.ts +47 -0
  250. package/components/tela/tags/tags-select.vue +637 -0
  251. package/components/tela/tags/tags.mdx +151 -0
  252. package/components/tela/tags/tags.stories.ts +118 -0
  253. package/components/tela/tags/tags.vue +112 -0
  254. package/components/tela/textarea/textarea.mdx +102 -0
  255. package/components/tela/textarea/textarea.stories.ts +50 -0
  256. package/components/tela/textarea/textarea.vue +34 -0
  257. package/components/tela/toggle-group.vue +91 -0
  258. package/components/tela/tooltip/tooltip-content.vue +45 -0
  259. package/components/tela/tooltip/tooltip-provider.vue +12 -0
  260. package/components/tela/tooltip/tooltip-root.vue +15 -0
  261. package/components/tela/tooltip/tooltip-trigger.vue +12 -0
  262. package/components/tela/tooltip/tooltip.mdx +196 -0
  263. package/components/tela/tooltip/tooltip.stories.ts +200 -0
  264. package/components/tela/tooltip/tooltip.vue +91 -0
  265. package/components/tela/tooltip-group/tooltip-group-trigger.vue +92 -0
  266. package/components/tela/tooltip-group/tooltip-group.mdx +236 -0
  267. package/components/tela/tooltip-group/tooltip-group.stories.ts +465 -0
  268. package/components/tela/tooltip-group/tooltip-group.vue +35 -0
  269. package/components/tela/transparent-input.vue +151 -0
  270. package/components/tela/variable-icon.vue +28 -0
  271. package/components/tela/variable-input.vue +77 -0
  272. package/components/tela/wide-button/wide-button.vue +40 -0
  273. package/components.json +18 -0
  274. package/composables/status-toast.ts +67 -0
  275. package/css/reset.css +386 -0
  276. package/css/text.css +22 -0
  277. package/lib/doc-generator.ts +903 -0
  278. package/lib/extractors/volar-extract.ts +186 -0
  279. package/lib/type-resolver.ts +402 -0
  280. package/lib/utils.ts +6 -0
  281. package/modules/tela-build-docs/index.ts +139 -0
  282. package/nuxt.config.ts +80 -0
  283. package/package.json +84 -0
  284. package/plugins/test-id.ts +7 -0
  285. package/tsconfig.json +7 -0
  286. package/types/custom-icon.ts +1 -0
  287. package/types/index.ts +2 -0
  288. package/types/status.ts +1 -0
  289. package/unocss.config.ts +89 -0
  290. package/utils/component-utils.ts +30 -0
  291. package/utils/design-tokens.ts +431 -0
  292. package/utils/fold.ts +8 -0
  293. package/utils/select-menu.ts +10 -0
  294. package/utils/status.ts +1 -0
  295. 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
+ }