bitboss-ui 3.0.0-beta.21 → 3.0.0-beta.23

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 (252) hide show
  1. package/README.md +55 -3
  2. package/bin/bitboss-ui-mcp.mjs +419 -9
  3. package/bin/bitboss-ui.mjs +293 -22
  4. package/dist/ai/BbAccordion.md +21 -23
  5. package/dist/ai/BbAlert.md +6 -12
  6. package/dist/ai/BbAvatar.md +60 -24
  7. package/dist/ai/BbBadge.md +12 -20
  8. package/dist/ai/BbBaseButton.md +14 -16
  9. package/dist/ai/BbBaseCheckboxIcon.md +6 -10
  10. package/dist/ai/BbBaseColorInput.md +1 -0
  11. package/dist/ai/BbBaseDatePickerInput.md +1 -0
  12. package/dist/ai/BbBaseNumberInput.md +1 -0
  13. package/dist/ai/BbBaseRadioIcon.md +6 -10
  14. package/dist/ai/BbBaseSelect.md +1 -0
  15. package/dist/ai/BbBaseSwitchIcon.md +5 -9
  16. package/dist/ai/BbBaseTag.md +1 -0
  17. package/dist/ai/BbBaseTextInput.md +2 -1
  18. package/dist/ai/BbBaseTextarea.md +1 -0
  19. package/dist/ai/BbBaseTimePickerInput.md +1 -0
  20. package/dist/ai/BbBreadcrumbs.md +23 -34
  21. package/dist/ai/BbButton.md +35 -47
  22. package/dist/ai/BbCheckbox.md +21 -37
  23. package/dist/ai/BbCheckboxGroup.md +13 -13
  24. package/dist/ai/BbCollapsible.md +14 -26
  25. package/dist/ai/BbColorInput.md +27 -23
  26. package/dist/ai/BbColorPalette.md +23 -35
  27. package/dist/ai/BbConfirm.md +17 -32
  28. package/dist/ai/BbDatePickerInput.md +29 -21
  29. package/dist/ai/BbDialog.md +159 -62
  30. package/dist/ai/BbDropdown.md +21 -25
  31. package/dist/ai/BbDropdownButton.md +13 -30
  32. package/dist/ai/BbDropzone.md +9 -11
  33. package/dist/ai/BbIcon.md +44 -33
  34. package/dist/ai/BbIndicator.md +2 -2
  35. package/dist/ai/BbNumberInput.md +32 -20
  36. package/dist/ai/BbOffCanvas.md +42 -42
  37. package/dist/ai/BbPagination.md +21 -23
  38. package/dist/ai/BbPopover.md +15 -17
  39. package/dist/ai/BbProgress.md +40 -51
  40. package/dist/ai/BbRadio.md +9 -9
  41. package/dist/ai/BbRadioGroup.md +7 -11
  42. package/dist/ai/BbRating.md +8 -12
  43. package/dist/ai/BbSelect.md +46 -36
  44. package/dist/ai/BbSelectPopover.md +19 -31
  45. package/dist/ai/BbSlider.md +15 -19
  46. package/dist/ai/BbSmoothHeight.md +16 -31
  47. package/dist/ai/BbSpinner.md +118 -74
  48. package/dist/ai/BbSwitch.md +25 -18
  49. package/dist/ai/BbSwitchGroup.md +9 -14
  50. package/dist/ai/BbTable.md +102 -55
  51. package/dist/ai/BbTabs.md +59 -82
  52. package/dist/ai/BbTag.md +26 -23
  53. package/dist/ai/BbTextInput.md +63 -27
  54. package/dist/ai/BbTextarea.md +42 -34
  55. package/dist/ai/BbTimePickerInput.md +14 -0
  56. package/dist/ai/BbToast.md +14 -34
  57. package/dist/ai/BbTooltip.md +22 -20
  58. package/dist/ai/BbTree.md +46 -72
  59. package/dist/ai/CommonInputWrapper.md +1 -0
  60. package/dist/ai/changelog.json +170 -62
  61. package/dist/ai/components.json +1678 -11
  62. package/dist/ai/composables/useConfirm.md +5 -2
  63. package/dist/ai/composables/useToast.md +4 -2
  64. package/dist/ai/guides/agent-contract.md +7 -4
  65. package/dist/ai/guides/ai-router.md +37 -22
  66. package/dist/ai/guides/component-picker.md +23 -18
  67. package/dist/ai/guides/design-language.md +151 -21
  68. package/dist/ai/guides/design-tokens.md +29 -4
  69. package/dist/ai/guides/icons-policy.md +46 -21
  70. package/dist/ai/guides/installation-and-plugin-setup.md +286 -17
  71. package/dist/ai/guides/migration/components/bb-rating.md +26 -1
  72. package/dist/ai/guides/migration/components/bb-tooltip.md +18 -0
  73. package/dist/ai/guides/migration/v2-to-v3.md +50 -23
  74. package/dist/ai/index.md +1 -1
  75. package/dist/ai/recipes/inertia/approvals-inbox.md +3 -3
  76. package/dist/ai/recipes/inertia/inline-edit-workspace.md +2 -2
  77. package/dist/ai/recipes/inertia/onboarding.md +2 -2
  78. package/dist/ai/recipes/inertia/ownership-atlas.md +6 -6
  79. package/dist/ai/recipes/inertia/record-form.md +5 -5
  80. package/dist/ai/recipes/inertia/records-workspace.md +4 -4
  81. package/dist/ai/recipes/inertia/upload-center.md +6 -6
  82. package/dist/ai/recipes/inertia/wizard-form.md +5 -5
  83. package/dist/ai/recipes/nuxt/approvals-inbox.md +3 -3
  84. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +2 -2
  85. package/dist/ai/recipes/nuxt/onboarding.md +2 -2
  86. package/dist/ai/recipes/nuxt/record-form.md +5 -5
  87. package/dist/ai/recipes/nuxt/records-workspace.md +4 -4
  88. package/dist/ai/recipes/nuxt/upload-center.md +6 -6
  89. package/dist/ai/recipes/nuxt/wizard-form.md +5 -5
  90. package/dist/ai/recipes/vue/approvals-inbox.md +3 -3
  91. package/dist/ai/recipes/vue/inline-edit-workspace.md +2 -2
  92. package/dist/ai/recipes/vue/onboarding.md +2 -2
  93. package/dist/ai/recipes/vue/record-form.md +10 -10
  94. package/dist/ai/recipes/vue/records-workspace.md +4 -4
  95. package/dist/ai/recipes/vue/upload-center.md +6 -6
  96. package/dist/ai/recipes/vue/wizard-form.md +5 -5
  97. package/dist/ai/source/BbBadge.md +1 -1
  98. package/dist/ai/source/BbBaseCheckbox.md +1 -1
  99. package/dist/ai/source/BbBaseColorInput.md +7 -0
  100. package/dist/ai/source/BbBaseDatePicker.md +5 -5
  101. package/dist/ai/source/BbBaseDatePickerInput.md +9 -2
  102. package/dist/ai/source/BbBaseNumberInput.md +7 -0
  103. package/dist/ai/source/BbBaseRadio.md +1 -1
  104. package/dist/ai/source/BbBaseRating.md +5 -5
  105. package/dist/ai/source/BbBaseSelect.md +8 -1
  106. package/dist/ai/source/BbBaseSlider.md +5 -5
  107. package/dist/ai/source/BbBaseSwitch.md +1 -1
  108. package/dist/ai/source/BbBaseTag.md +7 -0
  109. package/dist/ai/source/BbBaseTextInput.md +7 -0
  110. package/dist/ai/source/BbBaseTextarea.md +7 -0
  111. package/dist/ai/source/BbBaseTimePickerInput.md +8 -1
  112. package/dist/ai/source/BbBreadcrumbs.md +20 -1
  113. package/dist/ai/source/BbButton.md +2 -2
  114. package/dist/ai/source/BbColorInput.md +14 -0
  115. package/dist/ai/source/BbDatePickerInput.md +14 -0
  116. package/dist/ai/source/BbDialog.md +39 -8
  117. package/dist/ai/source/BbDropdown.md +1 -1
  118. package/dist/ai/source/BbDropzone.md +5 -5
  119. package/dist/ai/source/BbIcon.md +23 -10
  120. package/dist/ai/source/BbNumberInput.md +14 -0
  121. package/dist/ai/source/BbOffCanvas.md +50 -28
  122. package/dist/ai/source/BbPagination.md +2 -2
  123. package/dist/ai/source/BbPopover.md +12 -1
  124. package/dist/ai/source/BbSelect.md +14 -0
  125. package/dist/ai/source/BbSelectPopover.md +45 -7
  126. package/dist/ai/source/BbSpinner.md +2 -2
  127. package/dist/ai/source/BbTable.md +11 -11
  128. package/dist/ai/source/BbTag.md +14 -0
  129. package/dist/ai/source/BbTextInput.md +14 -0
  130. package/dist/ai/source/BbTextarea.md +14 -0
  131. package/dist/ai/source/BbTimePickerInput.md +14 -0
  132. package/dist/ai/source/BbTooltip.md +57 -10
  133. package/dist/ai/source/ChipsBox.md +1 -1
  134. package/dist/ai/source/CommaBox.md +1 -1
  135. package/dist/ai/source/CommonInputWrapper.md +128 -33
  136. package/dist/ai/source/GuidesComponent.md +14 -7
  137. package/dist/ai/source/OptionsContainer.md +1 -1
  138. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +1 -1
  139. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +1 -1
  140. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  141. package/dist/components/BbBaseColorInput/BbBaseColorInput.vue_vue_type_script_setup_true_lang.js +4 -1
  142. package/dist/components/BbBaseColorInput/types.d.ts +5 -0
  143. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +4 -4
  144. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +1 -1
  145. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +5 -2
  146. package/dist/components/BbBaseDatePickerInput/types.d.ts +5 -0
  147. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +1 -1
  148. package/dist/components/BbBaseNumberInput/BbBaseNumberInput.vue_vue_type_script_setup_true_lang.js +4 -1
  149. package/dist/components/BbBaseNumberInput/types.d.ts +5 -0
  150. package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +1 -1
  151. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +3 -3
  152. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +4 -1
  153. package/dist/components/BbBaseSelect/types.d.ts +5 -0
  154. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +3 -3
  155. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue_vue_type_script_setup_true_lang.js +1 -1
  156. package/dist/components/BbBaseTag/BbBaseTag.vue_vue_type_script_setup_true_lang.js +4 -1
  157. package/dist/components/BbBaseTag/types.d.ts +5 -0
  158. package/dist/components/BbBaseTextInput/BbBaseTextInput.vue_vue_type_script_setup_true_lang.js +4 -1
  159. package/dist/components/BbBaseTextInput/types.d.ts +5 -0
  160. package/dist/components/BbBaseTextarea/BbBaseTextarea.vue_vue_type_script_setup_true_lang.js +4 -1
  161. package/dist/components/BbBaseTextarea/types.d.ts +5 -0
  162. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +5 -2
  163. package/dist/components/BbBaseTimePickerInput/types.d.ts +5 -0
  164. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +71 -71
  165. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +2 -2
  166. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +5 -2
  167. package/dist/components/BbColorInput/types.d.ts +12 -0
  168. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +4 -2
  169. package/dist/components/BbDatePickerInput/types.d.ts +12 -0
  170. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +102 -97
  171. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +1 -1
  172. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +58 -58
  173. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +2 -2
  174. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +33 -35
  175. package/dist/components/BbIcon/iconCache.d.ts +30 -0
  176. package/dist/components/BbIcon/iconCache.js +21 -3
  177. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +5 -2
  178. package/dist/components/BbNumberInput/types.d.ts +12 -0
  179. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +57 -56
  180. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +2 -2
  181. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +138 -138
  182. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +4 -2
  183. package/dist/components/BbSelect/types.d.ts +12 -0
  184. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +43 -43
  185. package/dist/components/BbSpinner/BbSpinner.vue_vue_type_script_setup_true_lang.js +1 -1
  186. package/dist/components/BbSpinner/types.d.ts +1 -1
  187. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +8 -8
  188. package/dist/components/BbTable/BbTableDataRow.js +2 -2
  189. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +5 -2
  190. package/dist/components/BbTag/types.d.ts +12 -0
  191. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +5 -2
  192. package/dist/components/BbTextInput/types.d.ts +12 -0
  193. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +5 -2
  194. package/dist/components/BbTextarea/types.d.ts +12 -0
  195. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +4 -2
  196. package/dist/components/BbTimePickerInput/types.d.ts +12 -0
  197. package/dist/components/BbToast/BbToastRegion.vue_vue_type_script_setup_true_lang.js +62 -62
  198. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +81 -78
  199. package/dist/components/BbTooltip/types.d.ts +8 -1
  200. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +1 -1
  201. package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +1 -1
  202. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue.d.ts +1 -0
  203. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue_vue_type_script_setup_true_lang.js +4 -2
  204. package/dist/components/CommonInputWrapper/types.d.ts +10 -0
  205. package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +1 -1
  206. package/dist/composables/useConfirm.d.ts +22 -0
  207. package/dist/composables/useConfirm.js +43 -40
  208. package/dist/composables/useSafeAreaInternal.js +1 -1
  209. package/dist/composables/useToast.d.ts +16 -0
  210. package/dist/composables/useToast.js +20 -16
  211. package/dist/composables/useViewportWidth.d.ts +20 -6
  212. package/dist/composables/useViewportWidth.js +8 -7
  213. package/dist/icon-scan.d.ts +276 -0
  214. package/dist/icon-scan.js +485 -0
  215. package/dist/icons-whitelist.d.ts +5 -4
  216. package/dist/index.d.ts +2 -0
  217. package/dist/input-variants.d.ts +18 -0
  218. package/dist/llms-full.txt +2173 -1537
  219. package/dist/llms-medium.txt +504 -82
  220. package/dist/llms.txt +1 -1
  221. package/dist/nuxt-module.d.ts +1 -0
  222. package/dist/nuxt.js +10 -7
  223. package/dist/plugin.js +1 -0
  224. package/dist/runtime/nuxt-plugin.js +2 -1
  225. package/dist/styles.css +1 -1
  226. package/dist/types/Config.d.ts +18 -0
  227. package/dist/types/InputVariant.d.ts +22 -0
  228. package/dist/utils/cssLayers.d.ts +91 -0
  229. package/dist/utils/cssLayers.js +43 -0
  230. package/dist/utils/injectLibraryStyles.d.ts +3 -1
  231. package/dist/utils/injectLibraryStyles.js +24 -23
  232. package/dist/validated/BbColorInput.vue_vue_type_script_setup_true_lang.js +1 -0
  233. package/dist/validated/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +1 -0
  234. package/dist/validated/BbNumberInput.vue_vue_type_script_setup_true_lang.js +1 -0
  235. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  236. package/dist/validated/BbTag.vue_vue_type_script_setup_true_lang.js +1 -0
  237. package/dist/validated/BbTextInput.vue_vue_type_script_setup_true_lang.js +1 -0
  238. package/dist/validated/BbTextarea.vue_vue_type_script_setup_true_lang.js +1 -0
  239. package/dist/validated/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +1 -0
  240. package/dist/validated/index.d.ts +1 -0
  241. package/dist/vite-plugin.d.ts +89 -12
  242. package/dist/vite.js +331 -281
  243. package/llms.txt +1 -1
  244. package/package.json +7 -5
  245. package/scripts/lib/ensure-manifest.mjs +28 -5
  246. package/scripts/lib/eslint-plugin.d.ts +35 -0
  247. package/scripts/lib/eslint-plugin.mjs +299 -3
  248. package/scripts/lib/hand-roll-hints.mjs +50 -6
  249. package/scripts/lib/html-attributes.mjs +42 -0
  250. package/scripts/lib/mcp-config.mjs +38 -34
  251. package/scripts/lib/public-type-exports.mjs +246 -0
  252. package/scripts/lib/validate-bb-markup.mjs +205 -12
@@ -14,10 +14,22 @@
14
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
15
  import { homedir } from 'node:os';
16
16
  import { dirname, join, resolve } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
18
17
 
19
18
  export const MCP_SERVER_NAME = 'bitboss-ui';
20
- const COMMAND = 'npx';
19
+ const COMMAND = 'node';
20
+
21
+ /**
22
+ * Path to the installed CLI, relative to the project root.
23
+ *
24
+ * Relative on purpose: harnesses spawn a stdio server with the project root as
25
+ * its working directory, and a relative path is what lets a committed
26
+ * `.mcp.json` work on a teammate's machine. An absolute path would pin the
27
+ * config to whoever ran `ai-init`.
28
+ *
29
+ * Forward slashes even on Windows — Node accepts them, and JSON configs are
30
+ * shared across platforms.
31
+ */
32
+ export const MCP_BIN_PATH = 'node_modules/bitboss-ui/bin/bitboss-ui.mjs';
21
33
 
22
34
  /**
23
35
  * The MCP server's own runtime needs, declared as OPTIONAL peers rather than
@@ -58,39 +70,31 @@ export function missingMcpPeers(root) {
58
70
  }
59
71
 
60
72
  /**
61
- * The npx spec MUST be pinned to this package's own version (2026-08-06
62
- * review): MCP hosts spawn the entry with non-TTY stdio, where `npx` installs
63
- * without prompting — an unpinned `npx bitboss-ui mcp` in a project whose
64
- * dependency is absent or not yet installed silently downloads and executes
65
- * whatever `bitboss-ui@latest` resolves to on the public registry (and even
66
- * with a local install, answers from a different version's knowledge base
67
- * than the one the project uses). This file ships as `scripts/lib/*` in the
68
- * published package, so its own `package.json` two levels up — IS the
69
- * installed version.
73
+ * Invoke the INSTALLED binary directly, never `npx` (2026-09-09, superseding
74
+ * the 2026-08-06 pinned-npx ruling see below).
75
+ *
76
+ * The 2026-08-06 review pinned the spec to this package's own version because
77
+ * MCP hosts spawn the entry with non-TTY stdio, where `npx` installs without
78
+ * prompting: an unpinned `npx bitboss-ui mcp` would silently download and
79
+ * execute whatever `bitboss-ui@latest` resolved to, and answer from a
80
+ * different version's knowledge base than the project uses.
81
+ *
82
+ * The pin fixed that and broke the server outright. `npx bitboss-ui@x.y.z mcp`
83
+ * runs the package OUT OF THE NPX CACHE, and the MCP entry resolves its
84
+ * optional peers (`@modelcontextprotocol/sdk`, `zod`) relative to the package
85
+ * it is running from. The cache directory never has them, so the server cannot
86
+ * start no matter what the project has installed — reported from a consumer
87
+ * project with both peers present.
88
+ *
89
+ * A path to the local binary satisfies the original ruling MORE completely
90
+ * than the pin did: it cannot reach the registry at all, so there is nothing
91
+ * to download and no `latest` to drift to. It also retires the pin's own
92
+ * maintenance problem, where a config written at one version kept naming that
93
+ * version after an upgrade, letting an agent query a manifest that disagreed
94
+ * with the code it was writing against. The binary on disk IS the installed
95
+ * version, by construction.
70
96
  */
71
- function resolveNpxSpec() {
72
- try {
73
- const pkg = JSON.parse(
74
- readFileSync(
75
- join(
76
- dirname(fileURLToPath(import.meta.url)),
77
- '..',
78
- '..',
79
- 'package.json'
80
- ),
81
- 'utf-8'
82
- )
83
- );
84
- if (pkg.name === MCP_SERVER_NAME && typeof pkg.version === 'string') {
85
- return `${MCP_SERVER_NAME}@${pkg.version}`;
86
- }
87
- } catch {
88
- /* fall through to the unpinned spec */
89
- }
90
- return MCP_SERVER_NAME;
91
- }
92
-
93
- const ARGS = [resolveNpxSpec(), 'mcp'];
97
+ const ARGS = [MCP_BIN_PATH, 'mcp'];
94
98
 
95
99
  const stdioEntry = () => ({ command: COMMAND, args: [...ARGS] });
96
100
  const vscodeEntry = () => ({
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The library's public TYPE exports, read from `src/index.ts`.
3
+ *
4
+ * Two problems this solves, both reported from a consumer app:
5
+ *
6
+ * 1. `BbTableColumn`, `BbDropdownItem`, `BbTabsItem` and `BbBreadcrumbsItem`
7
+ * are real, documented exports — but they are TYPES, not components. They
8
+ * were absent from every machine-readable surface, so `get_component` could
9
+ * not describe them and `check` treated `<BbDropdownItem>` as an unknown
10
+ * component whose only offered remedy was `allowComponents` — which is
11
+ * exactly wrong: the library owns the name, the markup is the mistake, and
12
+ * allowlisting the tag silences every prop check on it forever.
13
+ *
14
+ * 2. The manifest printed prop types under names a consumer cannot import.
15
+ * `BbDropdown.items` read `Item[]` (its module-local name; the export is
16
+ * `BbDropdownItem`) and `BbTable.columns` read `InternalColumn<T>[]` — a
17
+ * type alias declared inside `BbTable.vue` and exported nowhere at all.
18
+ *
19
+ * Both are fixed at the source: the export list is emitted into
20
+ * `components.json` (so the CLI, the ESLint rules and the MCP server can all
21
+ * answer for a type name), and the same list rewrites extracted type strings
22
+ * to the importable spelling.
23
+ *
24
+ * Parsing is done with the TypeScript AST rather than a regex: the export
25
+ * blocks carry JSDoc between the braces (`@deprecated` markers on the v2
26
+ * aliases), which a brace-matching regex reads as export names.
27
+ */
28
+ import ts from 'typescript';
29
+
30
+ /**
31
+ * @typedef {object} PublicTypeExport
32
+ * @property {string} name the importable name (`BbDropdownItem`)
33
+ * @property {string} local the name inside its module (`Item`)
34
+ * @property {string} module the specifier it is re-exported from
35
+ * @property {string|null} component owning component, when `module` is a
36
+ * component types file (`./components/BbTable/types` → `BbTable`)
37
+ * @property {boolean} deprecated `@deprecated` in the export's own JSDoc
38
+ */
39
+
40
+ /** `./components/BbTable/types` → `BbTable`; anything else → null. */
41
+ function componentFromModule(moduleSpecifier) {
42
+ const match = /^\.\/components\/([A-Za-z0-9_]+)\/types$/.exec(
43
+ moduleSpecifier
44
+ );
45
+ return match ? match[1] : null;
46
+ }
47
+
48
+ /**
49
+ * Every `export type { … } from '…'` name in the given `src/index.ts` source.
50
+ *
51
+ * Only type-only re-exports count. A value export (`export { bitbossUiPlugin }`)
52
+ * is a runtime binding, not a type a consumer would ever write as a tag, and
53
+ * mixing the two would let `useToast` answer a "did you mean a type?" question
54
+ * with a composable.
55
+ *
56
+ * @param {string} source contents of `src/index.ts`
57
+ * @returns {PublicTypeExport[]}
58
+ */
59
+ export function parsePublicTypeExports(source) {
60
+ const file = ts.createSourceFile(
61
+ 'index.ts',
62
+ source,
63
+ ts.ScriptTarget.Latest,
64
+ /* setParentNodes */ true
65
+ );
66
+
67
+ /** @type {PublicTypeExport[]} */
68
+ const exports = [];
69
+
70
+ for (const statement of file.statements) {
71
+ if (!ts.isExportDeclaration(statement)) continue;
72
+ // `export type { … }` — or `export { type X }`, handled per-specifier below.
73
+ const declarationIsTypeOnly = statement.isTypeOnly;
74
+ const clause = statement.exportClause;
75
+ if (!clause || !ts.isNamedExports(clause)) continue;
76
+ const moduleSpecifier =
77
+ statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
78
+ ? statement.moduleSpecifier.text
79
+ : '';
80
+
81
+ for (const specifier of clause.elements) {
82
+ if (!declarationIsTypeOnly && !specifier.isTypeOnly) continue;
83
+ const name = specifier.name.text;
84
+ const local = specifier.propertyName?.text ?? name;
85
+ exports.push({
86
+ name,
87
+ local,
88
+ module: moduleSpecifier,
89
+ component: componentFromModule(moduleSpecifier),
90
+ deprecated: hasDeprecatedTag(specifier, source),
91
+ });
92
+ }
93
+ }
94
+
95
+ return exports;
96
+ }
97
+
98
+ /**
99
+ * Whether the JSDoc immediately before a specifier carries `@deprecated`.
100
+ *
101
+ * The comment sits inside the braces, attached to nothing the AST calls a
102
+ * documented node, so `ts.getJSDocTags` finds nothing — the leading trivia of
103
+ * the specifier is where it actually lives.
104
+ */
105
+ function hasDeprecatedTag(specifier, source) {
106
+ const ranges =
107
+ ts.getLeadingCommentRanges(source, specifier.getFullStart()) ?? [];
108
+ return ranges.some((range) =>
109
+ /@deprecated/.test(source.slice(range.pos, range.end))
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Module-local name → importable name, for one module.
115
+ *
116
+ * Scoped per module on purpose: `Item` means `BbDropdownItem` inside
117
+ * `components/BbDropdown/types` and `BbBreadcrumbsItem` inside
118
+ * `components/BbBreadcrumbs/types`. A global map would have to pick one and
119
+ * would mislabel the other.
120
+ *
121
+ * A deprecated alias never wins: `BbTabsItem` and `BbTabItem` are the same
122
+ * type, and the manifest should print the name we want written today.
123
+ *
124
+ * @param {PublicTypeExport[]} exports
125
+ * @param {string} moduleSpecifier
126
+ * @returns {Map<string, string>}
127
+ */
128
+ export function localToPublicNames(exports, moduleSpecifier) {
129
+ /** @type {Map<string, PublicTypeExport>} */
130
+ const chosen = new Map();
131
+ for (const entry of exports) {
132
+ if (entry.module !== moduleSpecifier) continue;
133
+ const held = chosen.get(entry.local);
134
+ // First one wins, EXCEPT that a live name always displaces a deprecated
135
+ // one: `BbTableColumn` is re-exported as the deprecated `TableColumn`
136
+ // first and under its own name second, and source order alone would
137
+ // print the spelling we are trying to retire.
138
+ if (!held || (held.deprecated && !entry.deprecated)) {
139
+ chosen.set(entry.local, entry);
140
+ }
141
+ }
142
+ return new Map([...chosen].map(([local, entry]) => [local, entry.name]));
143
+ }
144
+
145
+ /**
146
+ * The type-parameter names an SFC declares in `<script setup generic="…">`.
147
+ *
148
+ * These must never be rewritten. `BbSelectPopover` declares `generic="Item =
149
+ * any"` while `BbDropdown` exports a TYPE whose module-local name is also
150
+ * `Item` — rewriting by name alone would one day turn a type VARIABLE into
151
+ * `BbDropdownItem` and quietly publish a contract that says the wrong thing.
152
+ * No component collides today; this is what keeps that true.
153
+ *
154
+ * @param {string} source the .vue file
155
+ * @returns {Set<string>}
156
+ */
157
+ export function sfcGenericParameters(source) {
158
+ const match = /<script[^>]*\bgeneric=("([^"]*)"|'([^']*)')/.exec(source);
159
+ const declaration = match?.[2] ?? match?.[3];
160
+ if (!declaration) return new Set();
161
+
162
+ /** Split on top-level commas only: `T extends Record<string, any>` is one. */
163
+ const parts = [];
164
+ let depth = 0;
165
+ let current = '';
166
+ for (const char of declaration) {
167
+ if (char === '<' || char === '{' || char === '(' || char === '[') depth++;
168
+ else if (char === '>' || char === '}' || char === ')' || char === ']')
169
+ depth--;
170
+ if (char === ',' && depth === 0) {
171
+ parts.push(current);
172
+ current = '';
173
+ continue;
174
+ }
175
+ current += char;
176
+ }
177
+ parts.push(current);
178
+
179
+ return new Set(
180
+ parts
181
+ .map((part) => /^\s*([A-Za-z_$][A-Za-z0-9_$]*)/.exec(part)?.[1])
182
+ .filter((name) => Boolean(name))
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Rewrite bare identifiers in a printed type string through `aliases`.
188
+ *
189
+ * Word-boundary matching only — never a substring: `Item` must not touch
190
+ * `BbTreeItem`, and `Meta` must not touch `Metadata`. Generic arguments,
191
+ * unions and array suffixes all survive because only the identifier tokens are
192
+ * replaced (`Array<InternalColumn<K>>` → `Array<BbTableColumn<K>>`).
193
+ *
194
+ * @param {string} type
195
+ * @param {Map<string, string>} aliases
196
+ * @returns {string}
197
+ */
198
+ export function rewriteTypeNames(type, aliases) {
199
+ if (!type || aliases.size === 0) return type;
200
+ return type.replace(/\b[A-Za-z_$][A-Za-z0-9_$]*\b/g, (identifier) => {
201
+ // Never rewrite a property name (`{ key: Item }` is fine, `foo.Item` is
202
+ // a qualified name whose right side is not ours to rename).
203
+ return aliases.get(identifier) ?? identifier;
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Type names declared inside an SFC's `<script>` blocks.
209
+ *
210
+ * These are the names that must never reach a published prop type. A type in a
211
+ * component's `types.ts` that is not exported is a DECISION — `OffCanvasSize`
212
+ * is `'xs' | 'sm' | …`, an app writes `size="lg"`, and exporting a name to
213
+ * spell a string literal would be a permanent compatibility promise bought for
214
+ * no capability. A type declared in the .vue file is not a decision: it is
215
+ * plumbing that escaped, and `BbTable.columns` published `InternalColumn<T>[]`
216
+ * for months because nothing looked.
217
+ *
218
+ * The script blocks are extracted before parsing rather than handing the whole
219
+ * .vue to TypeScript: the template is not TypeScript, and while the parser
220
+ * tolerates it, what it makes of `<div v-if="x < y">` is not worth relying on.
221
+ *
222
+ * @param {string} source the .vue file
223
+ * @returns {Set<string>}
224
+ */
225
+ export function sfcDeclaredTypeNames(source) {
226
+ /** @type {Set<string>} */
227
+ const names = new Set();
228
+ for (const [, script] of source.matchAll(
229
+ /<script\b[^>]*>([\s\S]*?)<\/script>/g
230
+ )) {
231
+ const file = ts.createSourceFile(
232
+ 'sfc.ts',
233
+ script,
234
+ ts.ScriptTarget.Latest,
235
+ true
236
+ );
237
+ const visit = (node) => {
238
+ if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
239
+ names.add(node.name.text);
240
+ }
241
+ ts.forEachChild(node, visit);
242
+ };
243
+ ts.forEachChild(file, visit);
244
+ }
245
+ return names;
246
+ }
@@ -17,7 +17,11 @@ import { existsSync, readFileSync } from 'node:fs';
17
17
  import { dirname, join } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { parse } from 'vue/compiler-sfc';
20
- import { isFallthroughAttr, NATIVE_DOM_EVENTS } from './html-attributes.mjs';
20
+ import {
21
+ ELEMENT_DOM_EVENTS,
22
+ isFallthroughAttr,
23
+ NATIVE_DOM_EVENTS,
24
+ } from './html-attributes.mjs';
21
25
 
22
26
  const HERE = dirname(fileURLToPath(import.meta.url));
23
27
  const DEFAULT_MANIFEST = join(HERE, '../../dist/ai/components.json');
@@ -156,6 +160,61 @@ export function nearestName(key, candidates) {
156
160
  return best;
157
161
  }
158
162
 
163
+ /**
164
+ * Event names a consumer reaches for, mapped to the names our components
165
+ * actually emit, in preference order.
166
+ *
167
+ * Edit distance is the wrong tool for events. Props are misspelled, so
168
+ * `hideLabl` → `hideLabel` works; events are mis-VOCABULARIZED — someone writes
169
+ * the word their previous library used, or the word the interaction is called
170
+ * in English. `close` to `hide` is three edits and `close` to `hidden` is five,
171
+ * so `nearestName` finds nothing and the hint degrades to dumping every emitted
172
+ * event. That is exactly the case that shipped: `<BbDialog @close>` got
173
+ * "emitted events: hidden, hide, show, shown, update:modelValue" — a menu, not
174
+ * an answer.
175
+ *
176
+ * Only the resolved candidates that a component REALLY emits are offered, so
177
+ * this map can list generously without inventing events on components that
178
+ * don't have them.
179
+ */
180
+ const EVENT_SYNONYMS = new Map([
181
+ // `hidden` before `hide`: the native `close` event these authors are
182
+ // currently getting fires at the END of the close, so `hidden` is the
183
+ // suggestion that preserves the timing they already have. `cancel` keeps
184
+ // `hide` first for the same reason — it fires when close is REQUESTED.
185
+ ['close', ['hidden', 'hide', 'update:modelValue']],
186
+ ['closed', ['hidden', 'hide']],
187
+ ['dismiss', ['hidden', 'hide']],
188
+ ['dismissed', ['hidden', 'hide']],
189
+ ['hidden', ['hide']],
190
+ ['open', ['show', 'shown', 'update:modelValue']],
191
+ ['opened', ['shown', 'show']],
192
+ ['reveal', ['show', 'shown']],
193
+ ['shown', ['show']],
194
+ ['toggle', ['update:modelValue']],
195
+ ['change', ['update:modelValue']],
196
+ ['changed', ['update:modelValue']],
197
+ ['update', ['update:modelValue']],
198
+ ['input', ['update:modelValue']],
199
+ ['cancel', ['hide', 'hidden']],
200
+ ]);
201
+
202
+ /**
203
+ * The event this component emits that the author most likely meant by `name`:
204
+ * a spelling near-miss first (`resizecolumn` → `resize:column`), then the
205
+ * vocabulary map above. Null when neither has an answer.
206
+ * @param {string} name event name exactly as written, modifiers stripped
207
+ * @param {Set<string>} declared events this component emits
208
+ */
209
+ export function nearestEventName(name, declared) {
210
+ const near = nearestName(name, declared);
211
+ if (near) return near;
212
+ for (const candidate of EVENT_SYNONYMS.get(name) ?? []) {
213
+ if (declared.has(candidate)) return candidate;
214
+ }
215
+ return null;
216
+ }
217
+
159
218
  /**
160
219
  * The opposite-polarity spellings of a boolean prop name.
161
220
  *
@@ -286,7 +345,11 @@ const INTENT_ALIASES = {
286
345
  * @typedef {{ name: string, new?: string | null, since?: string, migration?: string, silent?: boolean }} RemovedEntry
287
346
  * @typedef {{ name: string, props: Array<{ name: string }>, models: Array<{ name: string }>, referenceOnly?: boolean, removed?: RemovedEntry[], renamed?: Array<{ from: string, to: string }> }} ComponentSurface
288
347
  * @typedef {Map<string, ComponentSurface>} ManifestMap
289
- * @typedef {{ component: string, attr: string, line?: number, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string }} Finding
348
+ * `fallthrough` marks a finding that describes markup which is not a typo and
349
+ * may well be doing something today — a native listener reaching the root
350
+ * element — but is outside the component's contract. It is the flag a caller
351
+ * uses to report at a lower severity; see `checkEvent`.
352
+ * @typedef {{ component: string, attr: string, line?: number, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string, fallthrough?: boolean }} Finding
290
353
  */
291
354
 
292
355
  /**
@@ -316,6 +379,20 @@ export function manifestFromComponents(input) {
316
379
  const list = Array.isArray(input) ? input : (input.components ?? []);
317
380
  /** @type {ManifestMap} */
318
381
  const map = new Map();
382
+ /*
383
+ * Public TYPE exports, carried as a property on the map rather than as
384
+ * entries in it. `manifest.get(name)` means "this is a component I can
385
+ * check the props of" at nine call sites across the ESLint rules and the
386
+ * doc scanners; a type answering that lookup would make every one of them
387
+ * validate attributes against an empty surface. The property is invisible
388
+ * to `get`, to iteration, and to those call sites.
389
+ */
390
+ map.types = new Map(
391
+ (Array.isArray(input) ? [] : (input.types ?? [])).map((entry) => [
392
+ entry.name,
393
+ entry,
394
+ ])
395
+ );
319
396
  for (const entry of list) {
320
397
  map.set(entry.name, {
321
398
  name: entry.name,
@@ -401,7 +478,7 @@ export function validateVueSnippet(code, manifest, options = {}) {
401
478
  }
402
479
 
403
480
  /**
404
- * @typedef {{ attr: string, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string }} AttrFinding
481
+ * @typedef {{ attr: string, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string, fallthrough?: boolean }} AttrFinding
405
482
  */
406
483
 
407
484
  /**
@@ -673,15 +750,37 @@ export function createComponentChecker(surface, options = {}) {
673
750
  * unknown component events only under `strictTemplates`. A consumer's
674
751
  * migration report found it.
675
752
  *
676
- * Accepted without consulting the manifest: any NATIVE DOM event, since
753
+ * Accepted without consulting the manifest: any UNIVERSAL DOM event, since
677
754
  * Vue passes an undeclared listener through to the root element —
678
755
  * `@click` on BbButton is good markup even though BbButton emits only
679
756
  * `update:modelValue`.
680
757
  *
758
+ * That blanket pass used to cover every DOM event we listed, and it hid
759
+ * a second bug class. An event that fires on only a few element types —
760
+ * `close`, `cancel`, `play` — falls through the same way, but whether it
761
+ * ever fires depends on what the component's root element happens to be,
762
+ * which is an internal detail no consumer should be reasoning about.
763
+ * `<BbDialog @close>` works, because that root is a native `<dialog>`;
764
+ * `<BbAlert @close>` is inert. Both compile clean and neither is a typo.
765
+ * Those names (`ELEMENT_DOM_EVENTS`) get their own `fallthrough` finding
766
+ * so the caller can say "this is not the contract" instead of the
767
+ * flatly-wrong "this event does not exist" — the wording that led one
768
+ * migration report to file a live `@close` handler as dead code.
769
+ *
681
770
  * Silent when the component declares no events at all: that is true of
682
771
  * reference-only surfaces and of anything whose manifest entry predates
683
772
  * event extraction, and guessing there would cry wolf — the same guard
684
773
  * `checkSlot` uses.
774
+ *
775
+ * A third case is neither of those: `@x` where the component declares a
776
+ * PROP called `onX`. Vue compiles every `@x` to an `onX` vnode entry, and
777
+ * prop resolution claims it before listeners are ever considered — so the
778
+ * handler runs, bound to the prop. BbButton's Inertia visit callbacks are
779
+ * exactly this shape (`onSuccess`, `onFinish`, `onCancelToken`, …), which
780
+ * is why `<BbButton href @success>` has always worked. It is not a typo
781
+ * and not a fall-through; it is the documented `:on-success` prop reached
782
+ * by a spelling the guides tell you not to use, because it silently
783
+ * breaks the moment someone adds a modifier or a second handler.
685
784
  */
686
785
  checkEvent(rawName) {
687
786
  if (!rawName) return null;
@@ -700,14 +799,48 @@ export function createComponentChecker(surface, options = {}) {
700
799
  if (name.startsWith('update:') && modelKeys.has(toCamel(name.slice(7))))
701
800
  return null;
702
801
 
703
- const near = nearestName(name, declared);
802
+ // `@success` the declared `onSuccess` PROP. Real, working markup in
803
+ // the one spelling the guides steer away from.
804
+ const camel = toCamel(name);
805
+ const callbackProp = `on${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
806
+ if (propKeys.has(callbackProp)) {
807
+ return {
808
+ attr: `@${rawName}`,
809
+ kind: 'unknown-event',
810
+ fallthrough: true,
811
+ message:
812
+ `${surface.name} does not emit \`${name}\`, but it declares \`${callbackProp}\` as a PROP, ` +
813
+ 'so Vue binds this handler to that prop and it does run. Prefer the prop spelling: ' +
814
+ 'the listener form takes no modifiers, and a second handler for the same name silently ' +
815
+ 'becomes an array the prop is not typed for',
816
+ hint: `use \`:${toKebab(callbackProp)}="…"\``,
817
+ };
818
+ }
819
+
820
+ const near = nearestEventName(name, declared);
821
+ const suggestion = near
822
+ ? `did you mean \`@${near}\`?`
823
+ : `emitted events: ${[...declared].sort().join(', ')}`;
824
+
825
+ if (ELEMENT_DOM_EVENTS.has(name)) {
826
+ return {
827
+ attr: `@${rawName}`,
828
+ kind: 'unknown-event',
829
+ fallthrough: true,
830
+ message:
831
+ `${surface.name} does not emit \`${name}\`, so this listener falls through to its root ` +
832
+ "element as a native DOM listener. It fires on the element's own schedule, not the " +
833
+ "component's, and only if that root happens to be an element that dispatches " +
834
+ `\`${name}\` — which is internal and can change without a breaking release`,
835
+ hint: suggestion,
836
+ };
837
+ }
838
+
704
839
  return {
705
840
  attr: `@${rawName}`,
706
841
  kind: 'unknown-event',
707
842
  message: `event \`${name}\` is not emitted by ${surface.name}`,
708
- hint: near
709
- ? `did you mean \`@${near}\`?`
710
- : `emitted events: ${[...declared].sort().join(', ')}`,
843
+ hint: suggestion,
711
844
  };
712
845
  },
713
846
  checkSlot(rawName) {
@@ -1126,6 +1259,69 @@ function compilerItemsMayBeAsync(node) {
1126
1259
  return false; // `items` not bound at all — defaults to `[]`, definitely sync.
1127
1260
  }
1128
1261
 
1262
+ /**
1263
+ * `BbTabs` → `BbTabs'`, `BbTable` → `BbTable's`. Shared so the CLI, the ESLint
1264
+ * rule and the MCP server all form the same sentence about a component.
1265
+ * @param {string} owner
1266
+ */
1267
+ export const possessive = (owner) =>
1268
+ owner.endsWith('s') ? `${owner}'` : `${owner}'s`;
1269
+
1270
+ /**
1271
+ * Message + hint for a `Bb*` tag that is not a component.
1272
+ *
1273
+ * Three different situations wore one message, and the hint offered the same
1274
+ * remedy to all three — allowlist it — which is right for exactly one:
1275
+ *
1276
+ * - **A public TYPE written as a tag.** `BbTableColumn`, `BbDropdownItem` and
1277
+ * friends are real exports, so a consumer reasonably assumes they are
1278
+ * components. They are not: they type a prop. Allowlisting is the worst
1279
+ * available answer, because the markup IS wrong and the allowlist then
1280
+ * silences every real check on that tag forever. Reported from a consumer app
1281
+ * that took the hint.
1282
+ * - **A typo.** `BbButtn` is one edit from `BbButton`, and the prop checker has
1283
+ * suggested nearest names for years while this one dead-ended at "check the
1284
+ * spelling".
1285
+ * - **An app-owned component** that happens to start with `Bb`. The one case
1286
+ * the allowlist is for — now stated with what it costs.
1287
+ *
1288
+ * @param {string} name
1289
+ * @param {ManifestMap} manifest
1290
+ * @returns {{ message: string, hint: string }}
1291
+ */
1292
+ export function unknownComponentFinding(name, manifest) {
1293
+ const type = manifest.types?.get(name);
1294
+ if (type) {
1295
+ const use = type.usedBy?.[0];
1296
+ const where = use
1297
+ ? `it types ${possessive(use.component)} \`${use.name}\` ${use.surface}`
1298
+ : type.component
1299
+ ? `it belongs to ${type.component}`
1300
+ : 'it is part of the public API surface, not the component set';
1301
+ const importName = type.replacedBy ?? name;
1302
+ const deprecation = type.replacedBy
1303
+ ? ` \`${name}\` is a deprecated alias — import \`${type.replacedBy}\`.`
1304
+ : '';
1305
+ return {
1306
+ message: `\`${name}\` is a type export, not a component`,
1307
+ hint:
1308
+ `${where}, so pass it as a value instead of writing it as a tag: ` +
1309
+ `\`import type { ${importName} } from 'bitboss-ui'\`.${deprecation} ` +
1310
+ 'Do NOT allowlist it — the library owns this name.',
1311
+ };
1312
+ }
1313
+
1314
+ const suggestion = nearestName(name, manifest.keys());
1315
+ return {
1316
+ message: `component \`${name}\` is not in the bitboss-ui manifest`,
1317
+ hint:
1318
+ (suggestion ? `Did you mean \`${suggestion}\`? ` : '') +
1319
+ 'Check the spelling against dist/ai/index.md Consumer API. If your app owns this component, allowlist it: ' +
1320
+ `\`bitboss-ui.allowComponents: ["${name}"]\` in package.json, or \`--allow-component ${name}\` — ` +
1321
+ 'which skips the tag entirely, so no prop, event or slot on it is ever checked again.',
1322
+ };
1323
+ }
1324
+
1129
1325
  /**
1130
1326
  * Validate one `<Bb…>` element node against the manifest surface.
1131
1327
  * @param {{ tag: string, props: Array<Record<string, any>> }} node
@@ -1155,10 +1351,7 @@ function validateElement(node, manifest, findings) {
1155
1351
  attr: '',
1156
1352
  line: tagLine,
1157
1353
  kind: 'unknown-prop',
1158
- message: `component \`${name}\` is not in the bitboss-ui manifest`,
1159
- hint:
1160
- 'Check the spelling against dist/ai/index.md Consumer API. If your app owns this component, allowlist it: ' +
1161
- `\`bitboss-ui.allowComponents: ["${name}"]\` in package.json, or \`--allow-component ${name}\`.`,
1354
+ ...unknownComponentFinding(name, manifest),
1162
1355
  });
1163
1356
  return;
1164
1357
  }