bitboss-ui 3.0.0-beta.35 → 3.0.0-beta.37

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 (354) hide show
  1. package/README.md +31 -12
  2. package/bin/bitboss-ui-mcp.mjs +799 -74
  3. package/bin/bitboss-ui.mjs +6 -3
  4. package/dist/ai/BbAccordion.md +8 -8
  5. package/dist/ai/BbAlert.md +12 -7
  6. package/dist/ai/BbAvatar.md +9 -6
  7. package/dist/ai/BbBadge.md +21 -21
  8. package/dist/ai/BbBadgeButton.md +10 -10
  9. package/dist/ai/BbBaseButton.md +7 -7
  10. package/dist/ai/BbBaseCheckboxIcon.md +1 -1
  11. package/dist/ai/BbBaseRadioIcon.md +1 -1
  12. package/dist/ai/BbBaseSwitchIcon.md +2 -2
  13. package/dist/ai/BbBreadcrumbs.md +17 -10
  14. package/dist/ai/BbButton.md +31 -28
  15. package/dist/ai/BbCalendar.md +29 -21
  16. package/dist/ai/BbCheckbox.md +21 -22
  17. package/dist/ai/BbCheckboxGroup.md +36 -29
  18. package/dist/ai/BbCollapsible.md +2 -2
  19. package/dist/ai/BbColorInput.md +39 -40
  20. package/dist/ai/BbColorPalette.md +10 -10
  21. package/dist/ai/BbConfirm.md +6 -6
  22. package/dist/ai/BbConfirmPortal.md +1 -1
  23. package/dist/ai/BbDatePicker.md +36 -33
  24. package/dist/ai/BbDatePickerInput.md +52 -64
  25. package/dist/ai/BbDialog.md +22 -22
  26. package/dist/ai/BbDropdown.md +32 -28
  27. package/dist/ai/BbDropdownButton.md +39 -28
  28. package/dist/ai/BbDropdownGroup.md +4 -13
  29. package/dist/ai/BbDropzone.md +12 -12
  30. package/dist/ai/BbForm.md +6 -4
  31. package/dist/ai/BbIcon.md +2 -2
  32. package/dist/ai/BbIndicator.md +2 -2
  33. package/dist/ai/BbNumberInput.md +39 -40
  34. package/dist/ai/BbOffCanvas.md +19 -19
  35. package/dist/ai/BbPagination.md +8 -8
  36. package/dist/ai/BbPopover.md +15 -15
  37. package/dist/ai/BbProgress.md +2 -2
  38. package/dist/ai/BbPtScope.md +1 -1
  39. package/dist/ai/BbRadio.md +21 -22
  40. package/dist/ai/BbRadioGroup.md +36 -29
  41. package/dist/ai/BbRating.md +27 -30
  42. package/dist/ai/BbSelect.md +80 -58
  43. package/dist/ai/BbSelectPopover.md +44 -44
  44. package/dist/ai/BbSlider.md +29 -30
  45. package/dist/ai/BbSmoothHeight.md +1 -1
  46. package/dist/ai/BbSpinner.md +6 -6
  47. package/dist/ai/BbSwitch.md +23 -24
  48. package/dist/ai/BbSwitchGroup.md +37 -30
  49. package/dist/ai/BbTable.md +247 -87
  50. package/dist/ai/BbTabs.md +171 -91
  51. package/dist/ai/BbTabsList.md +30 -11
  52. package/dist/ai/{BbTabsPanes.md → BbTabsPanels.md} +28 -10
  53. package/dist/ai/BbTabsRoot.md +8 -58
  54. package/dist/ai/BbTag.md +66 -65
  55. package/dist/ai/BbTextInput.md +35 -34
  56. package/dist/ai/BbTextarea.md +36 -35
  57. package/dist/ai/BbTimePicker.md +22 -17
  58. package/dist/ai/BbTimePickerInput.md +39 -36
  59. package/dist/ai/BbToast.md +128 -31
  60. package/dist/ai/BbToastPortal.md +2 -29
  61. package/dist/ai/BbTooltip.md +9 -9
  62. package/dist/ai/BbTree.md +7 -7
  63. package/dist/ai/ChipsBox.md +20 -20
  64. package/dist/ai/CommaBox.md +2 -2
  65. package/dist/ai/CommonField.md +11 -11
  66. package/dist/ai/CommonFieldInput.md +8 -8
  67. package/dist/ai/CommonPopover.md +12 -12
  68. package/dist/ai/CommonTimeSelector.md +8 -6
  69. package/dist/ai/FlatListBox.md +8 -6
  70. package/dist/ai/GroupedListBox.md +9 -7
  71. package/dist/ai/ListBox.md +13 -11
  72. package/dist/ai/OptionsContainer.md +5 -5
  73. package/dist/ai/changelog.json +151 -19
  74. package/dist/ai/components.json +4734 -1479
  75. package/dist/ai/composables/useBbTableContext.md +8 -3
  76. package/dist/ai/composables/useConfirm.md +2 -0
  77. package/dist/ai/composables/useMobile.md +4 -4
  78. package/dist/ai/composables/useToast.md +18 -17
  79. package/dist/ai/guides/ai-router.md +2 -2
  80. package/dist/ai/guides/app-layout.md +5 -3
  81. package/dist/ai/guides/coherence-playbook.md +8 -8
  82. package/dist/ai/guides/component-picker.md +1 -1
  83. package/dist/ai/guides/design-language.md +33 -33
  84. package/dist/ai/guides/design-tokens.md +34 -19
  85. package/dist/ai/guides/fetch-items-playbook.md +21 -15
  86. package/dist/ai/guides/icons-policy.md +10 -12
  87. package/dist/ai/guides/inline-edit-playbook.md +6 -4
  88. package/dist/ai/guides/installation-and-plugin-setup.md +35 -2
  89. package/dist/ai/guides/migration/components/bb-badge.md +5 -5
  90. package/dist/ai/guides/migration/components/bb-checkbox-group.md +18 -18
  91. package/dist/ai/guides/migration/components/bb-chip.md +6 -6
  92. package/dist/ai/guides/migration/components/bb-date-picker-input.md +9 -10
  93. package/dist/ai/guides/migration/components/bb-dialog.md +4 -4
  94. package/dist/ai/guides/migration/components/bb-dropdown.md +2 -1
  95. package/dist/ai/guides/migration/components/bb-rating.md +12 -11
  96. package/dist/ai/guides/migration/components/bb-select.md +1 -1
  97. package/dist/ai/guides/migration/components/bb-slider.md +7 -0
  98. package/dist/ai/guides/migration/components/bb-table.md +28 -28
  99. package/dist/ai/guides/migration/components/bb-tabs.md +27 -21
  100. package/dist/ai/guides/migration/components/bb-tag.md +9 -8
  101. package/dist/ai/guides/migration/components/bb-toast.md +1 -1
  102. package/dist/ai/guides/migration/components/bb-tree.md +1 -1
  103. package/dist/ai/guides/migration/components/use-confirm.md +1 -1
  104. package/dist/ai/guides/migration/v2-to-v3.md +143 -98
  105. package/dist/ai/guides/migration/v3-beta-css-classes.md +723 -0
  106. package/dist/ai/guides/options-items-playbook.md +11 -6
  107. package/dist/ai/guides/passthrough.md +97 -67
  108. package/dist/ai/guides/ssr-prefill.md +3 -1
  109. package/dist/ai/index.md +5 -4
  110. package/dist/ai/recipes/inertia/inline-edit-workspace.md +10 -10
  111. package/dist/ai/recipes/inertia/record-form.md +5 -5
  112. package/dist/ai/recipes/inertia/records-workspace.md +4 -4
  113. package/dist/ai/recipes/inertia/upload-center.md +28 -28
  114. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +32 -32
  115. package/dist/ai/recipes/nuxt/record-form.md +5 -5
  116. package/dist/ai/recipes/nuxt/records-workspace.md +4 -4
  117. package/dist/ai/recipes/nuxt/upload-center.md +27 -27
  118. package/dist/ai/recipes/vue/approvals-inbox.md +2 -2
  119. package/dist/ai/recipes/vue/inline-edit-workspace.md +33 -33
  120. package/dist/ai/recipes/vue/record-form.md +21 -21
  121. package/dist/ai/recipes/vue/records-workspace.md +4 -4
  122. package/dist/ai/recipes/vue/upload-center.md +27 -27
  123. package/dist/ai/source/BbAccordion.md +3 -2
  124. package/dist/ai/source/BbAvatar.md +6 -0
  125. package/dist/ai/source/BbBadge.md +16 -16
  126. package/dist/ai/source/BbBadgeButton.md +14 -14
  127. package/dist/ai/source/BbBreadcrumbs.md +51 -5
  128. package/dist/ai/source/BbButton.md +81 -7
  129. package/dist/ai/source/BbCalendar.md +98 -45
  130. package/dist/ai/source/BbCheckbox.md +2 -1
  131. package/dist/ai/source/BbCheckboxGroup.md +122 -94
  132. package/dist/ai/source/BbCollapsible.md +6 -0
  133. package/dist/ai/source/BbColorInput.md +14 -2
  134. package/dist/ai/source/BbColorPalette.md +18 -12
  135. package/dist/ai/source/BbConfirm.md +5 -2
  136. package/dist/ai/source/BbConfirmPortal.md +6 -3
  137. package/dist/ai/source/BbDatePicker.md +41 -14
  138. package/dist/ai/source/BbDatePickerInput.md +23 -27
  139. package/dist/ai/source/BbDialog.md +21 -4
  140. package/dist/ai/source/BbDropdown.md +20 -11
  141. package/dist/ai/source/BbDropdownButton.md +32 -5
  142. package/dist/ai/source/BbDropdownGroup.md +15 -9
  143. package/dist/ai/source/BbDropzone.md +7 -6
  144. package/dist/ai/source/BbIcon.md +8 -4
  145. package/dist/ai/source/BbIndicator.md +5 -5
  146. package/dist/ai/source/BbNumberInput.md +15 -2
  147. package/dist/ai/source/BbOffCanvas.md +35 -18
  148. package/dist/ai/source/BbPagination.md +16 -8
  149. package/dist/ai/source/BbPopover.md +37 -7
  150. package/dist/ai/source/BbRadio.md +2 -1
  151. package/dist/ai/source/BbRadioGroup.md +105 -92
  152. package/dist/ai/source/BbRating.md +17 -12
  153. package/dist/ai/source/BbSelect.md +97 -23
  154. package/dist/ai/source/BbSelectPopover.md +48 -37
  155. package/dist/ai/source/BbSlider.md +17 -2
  156. package/dist/ai/source/BbSwitchGroup.md +104 -92
  157. package/dist/ai/source/BbTable.md +350 -170
  158. package/dist/ai/source/BbTabs.md +193 -41
  159. package/dist/ai/source/BbTabsList.md +212 -54
  160. package/dist/ai/source/{BbTabsPanes.md → BbTabsPanels.md} +189 -50
  161. package/dist/ai/source/BbTabsRoot.md +148 -31
  162. package/dist/ai/source/BbTag.md +125 -44
  163. package/dist/ai/source/BbTextInput.md +13 -1
  164. package/dist/ai/source/BbTextarea.md +10 -2
  165. package/dist/ai/source/BbTimePicker.md +60 -13
  166. package/dist/ai/source/BbTimePickerInput.md +53 -10
  167. package/dist/ai/source/BbToast.md +113 -48
  168. package/dist/ai/source/BbToastPortal.md +117 -49
  169. package/dist/ai/source/BbTooltip.md +2 -2
  170. package/dist/ai/source/BbTree.md +6 -4
  171. package/dist/ai/source/ChipsBox.md +17 -7
  172. package/dist/ai/source/CommaBox.md +16 -9
  173. package/dist/ai/source/CommonField.md +7 -4
  174. package/dist/ai/source/CommonFieldInput.md +2 -0
  175. package/dist/ai/source/CommonTimeSelector.md +107 -25
  176. package/dist/ai/source/FlatListBox.md +15 -5
  177. package/dist/ai/source/GroupedListBox.md +15 -5
  178. package/dist/ai/source/ListBox.md +11 -0
  179. package/dist/ai/source/OptionsContainer.md +12 -10
  180. package/dist/components/BbAccordion/types.d.ts +2 -1
  181. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +4 -4
  182. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +2 -2
  183. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +5 -5
  184. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +107 -98
  185. package/dist/components/BbButton/BbButton.vue.d.ts +5 -0
  186. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +1 -1
  187. package/dist/components/BbButton/types.d.ts +64 -0
  188. package/dist/components/BbCalendar/BbCalendar.vue_vue_type_script_setup_true_lang.js +203 -172
  189. package/dist/components/BbCalendar/CalendarMonthPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  190. package/dist/components/BbCalendar/CalendarYearPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  191. package/dist/components/BbCalendar/types.d.ts +18 -8
  192. package/dist/components/BbCalendar/types.js +3 -2
  193. package/dist/components/BbCalendar/useCalendarGrid.js +14 -14
  194. package/dist/components/BbCalendar/useDayGrid.js +1 -1
  195. package/dist/components/BbCheckbox/types.d.ts +2 -1
  196. package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +103 -103
  197. package/dist/components/BbCheckboxGroup/types.d.ts +2 -1
  198. package/dist/components/BbColorInput/BbColorInput.vue.d.ts +2 -2
  199. package/dist/components/BbColorInput/types.d.ts +14 -2
  200. package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +2 -2
  201. package/dist/components/BbColorPalette/BbColorPalette.vue_vue_type_script_setup_true_lang.js +166 -165
  202. package/dist/components/BbColorPalette/types.d.ts +2 -2
  203. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +5 -2
  204. package/dist/components/BbConfirm/BbConfirmPortal.vue.d.ts +1 -0
  205. package/dist/components/BbDatePicker/BbDatePicker.vue.d.ts +2 -2
  206. package/dist/components/BbDatePicker/BbDatePicker.vue_vue_type_script_setup_true_lang.js +89 -72
  207. package/dist/components/BbDatePicker/types.d.ts +13 -5
  208. package/dist/components/BbDatePicker/types.js +3 -2
  209. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue.d.ts +2 -2
  210. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +348 -340
  211. package/dist/components/BbDatePickerInput/types.d.ts +10 -3
  212. package/dist/components/BbDatePickerInput/types.js +3 -2
  213. package/dist/components/BbDialog/BbDialog.vue.d.ts +1 -1
  214. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +131 -122
  215. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +2 -2
  216. package/dist/components/BbDropdown/BbDropdown.vue.d.ts +4 -4
  217. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +177 -176
  218. package/dist/components/BbDropdown/BbDropdownGroup.vue.d.ts +4 -0
  219. package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +5 -5
  220. package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +102 -102
  221. package/dist/components/BbDropdown/types.d.ts +3 -1
  222. package/dist/components/BbDropdownButton/BbDropdownButton.vue.d.ts +4 -4
  223. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +34 -23
  224. package/dist/components/BbDropdownButton/types.d.ts +13 -2
  225. package/dist/components/BbDropdownButton/types.js +2 -0
  226. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +3 -3
  227. package/dist/components/BbDropzone/types.d.ts +2 -1
  228. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +3 -3
  229. package/dist/components/BbIndicator/BbIndicator.vue_vue_type_script_setup_true_lang.js +1 -1
  230. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +4 -1
  231. package/dist/components/BbNumberInput/types.d.ts +8 -1
  232. package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +1 -1
  233. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +186 -178
  234. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +65 -57
  235. package/dist/components/BbPopover/BbPopover.vue.d.ts +1 -1
  236. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +155 -145
  237. package/dist/components/BbPopover/types.d.ts +2 -1
  238. package/dist/components/BbRadio/types.d.ts +2 -1
  239. package/dist/components/BbRadioGroup/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +68 -68
  240. package/dist/components/BbRadioGroup/types.d.ts +2 -1
  241. package/dist/components/BbRating/BbRating.vue_vue_type_script_setup_true_lang.js +6 -6
  242. package/dist/components/BbRating/types.d.ts +5 -2
  243. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +41 -22
  244. package/dist/components/BbSelect/types.d.ts +42 -11
  245. package/dist/components/BbSelect/types.js +17 -5
  246. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +310 -306
  247. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +183 -181
  248. package/dist/components/BbSlider/types.d.ts +2 -1
  249. package/dist/components/BbSwitchGroup/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +102 -102
  250. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +970 -847
  251. package/dist/components/BbTable/BbTableDataRow.d.ts +5 -4
  252. package/dist/components/BbTable/BbTableDataRow.js +18 -8
  253. package/dist/components/BbTable/BbTableExpandRow.js +1 -1
  254. package/dist/components/BbTable/BbTableSelectToggle.d.ts +17 -0
  255. package/dist/components/BbTable/BbTableSelectToggle.js +8 -0
  256. package/dist/components/BbTable/types.d.ts +56 -14
  257. package/dist/components/BbTable/types.js +14 -1
  258. package/dist/components/BbTabs/BbTabs.vue_vue_type_script_setup_true_lang.js +44 -23
  259. package/dist/components/BbTabs/BbTabsList.vue_vue_type_script_setup_true_lang.js +101 -64
  260. package/dist/components/BbTabs/BbTabsPanels.vue.d.ts +12 -0
  261. package/dist/components/BbTabs/BbTabsPanels.vue.js +6 -0
  262. package/dist/components/BbTabs/BbTabsPanels.vue_vue_type_script_setup_true_lang.js +59 -0
  263. package/dist/components/BbTabs/BbTabsRoot.vue_vue_type_script_setup_true_lang.js +0 -10
  264. package/dist/components/BbTabs/types.d.ts +115 -28
  265. package/dist/components/BbTabs/types.js +24 -1
  266. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +168 -151
  267. package/dist/components/BbTag/types.d.ts +47 -14
  268. package/dist/components/BbTag/types.js +19 -5
  269. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +5 -5
  270. package/dist/components/BbTextInput/types.d.ts +8 -1
  271. package/dist/components/BbTextarea/types.d.ts +10 -2
  272. package/dist/components/BbTimePicker/BbTimePicker.vue.d.ts +2 -2
  273. package/dist/components/BbTimePicker/BbTimePicker.vue_vue_type_script_setup_true_lang.js +154 -129
  274. package/dist/components/BbTimePicker/types.d.ts +28 -7
  275. package/dist/components/BbTimePicker/types.js +7 -1
  276. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue.d.ts +2 -2
  277. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +217 -194
  278. package/dist/components/BbTimePickerInput/types.d.ts +28 -5
  279. package/dist/components/BbTimePickerInput/types.js +2 -0
  280. package/dist/components/BbToast/BbToast.vue_vue_type_script_setup_true_lang.js +12 -0
  281. package/dist/components/BbToast/BbToastMessage.vue.d.ts +3 -3
  282. package/dist/components/BbToast/BbToastMessage.vue_vue_type_script_setup_true_lang.js +72 -70
  283. package/dist/components/BbToast/BbToastPortal.vue.d.ts +1 -0
  284. package/dist/components/BbToast/BbToastRegion.vue.d.ts +2 -2
  285. package/dist/components/BbToast/types.d.ts +35 -10
  286. package/dist/components/BbToast/types.js +16 -1
  287. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +1 -1
  288. package/dist/components/BbTree/BbTree.vue_vue_type_script_setup_true_lang.js +35 -33
  289. package/dist/components/BbTree/BbTreeLevel.vue_vue_type_script_setup_true_lang.js +5 -5
  290. package/dist/components/BbTree/context.d.ts +5 -0
  291. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +41 -41
  292. package/dist/components/ChipsBox/types.d.ts +4 -3
  293. package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +2 -2
  294. package/dist/components/CommaBox/types.d.ts +4 -3
  295. package/dist/components/CommonField/CommonField.vue_vue_type_script_setup_true_lang.js +4 -3
  296. package/dist/components/CommonFieldInput/CommonFieldInput.vue_vue_type_script_setup_true_lang.js +5 -1
  297. package/dist/components/CommonPopover/CommonPopover.vue.d.ts +1 -1
  298. package/dist/components/CommonTimeSelector/CommonTimeSelector.vue.d.ts +19 -3
  299. package/dist/components/CommonTimeSelector/CommonTimeSelector.vue_vue_type_script_setup_true_lang.js +114 -89
  300. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +111 -104
  301. package/dist/components/FlatListBox/types.d.ts +7 -0
  302. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +12 -5
  303. package/dist/components/GroupedListBox/types.d.ts +7 -0
  304. package/dist/components/ListBox/ListBox.vue_vue_type_script_setup_true_lang.js +10 -0
  305. package/dist/components/ListBox/types.d.ts +7 -0
  306. package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +5 -3
  307. package/dist/components/OptionsContainer/types.d.ts +2 -2
  308. package/dist/composables/useBbTableContext.d.ts +33 -1
  309. package/dist/composables/useBbTableContext.js +83 -81
  310. package/dist/composables/useBbTabsContext.d.ts +10 -18
  311. package/dist/composables/useBbTabsContext.js +123 -133
  312. package/dist/composables/useCoarsePointer.js +7 -10
  313. package/dist/composables/useEscapeStackEntry.d.ts +13 -0
  314. package/dist/composables/useEscapeStackEntry.js +16 -0
  315. package/dist/composables/useHydrated.d.ts +18 -0
  316. package/dist/composables/useHydrated.js +13 -7
  317. package/dist/composables/useMobile.js +8 -11
  318. package/dist/composables/usePassthrough.d.ts +22 -1
  319. package/dist/composables/usePassthrough.js +76 -63
  320. package/dist/composables/useViewportWidth.d.ts +5 -39
  321. package/dist/composables/useViewportWidth.js +13 -7
  322. package/dist/deprecation/ai-deprecations.json.d.ts +23 -1
  323. package/dist/deprecation/ai-deprecations.json.js +1 -1
  324. package/dist/index.d.ts +2 -2
  325. package/dist/index.js +29 -29
  326. package/dist/llms-full.txt +4532 -3271
  327. package/dist/llms-medium.txt +214 -181
  328. package/dist/llms.txt +4 -3
  329. package/dist/nuxt-auto-imports.js +1 -1
  330. package/dist/nuxt-module.d.ts +7 -0
  331. package/dist/nuxt.js +26 -17
  332. package/dist/plugin.js +24 -17
  333. package/dist/runtime/nuxt-plugin.js +26 -22
  334. package/dist/styles.css +2 -2
  335. package/dist/types/ToastVariant.d.ts +1 -1
  336. package/dist/types/passthrough.d.ts +1 -1
  337. package/dist/types/ptComponentMap.d.ts +13 -11
  338. package/dist/utils/versionCheck.js +1 -1
  339. package/dist/validated/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +32 -16
  340. package/dist/validated/BbSelect.vue.d.ts +36 -18
  341. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +27 -9
  342. package/dist/validated/BbTag.vue_vue_type_script_setup_true_lang.js +16 -7
  343. package/dist/validated/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +18 -0
  344. package/dist/vite-plugin.d.ts +1 -1
  345. package/llms.txt +4 -3
  346. package/package.json +17 -10
  347. package/scripts/lib/component-attr-targets.ts +6 -0
  348. package/scripts/lib/doc-sections.mjs +181 -0
  349. package/scripts/lib/eslint-plugin.d.ts +1 -0
  350. package/scripts/lib/eslint-plugin.mjs +7 -1
  351. package/scripts/lib/validate-bb-markup.mjs +335 -27
  352. package/dist/components/BbTabs/BbTabsPanes.vue.d.ts +0 -16
  353. package/dist/components/BbTabs/BbTabsPanes.vue.js +0 -6
  354. package/dist/components/BbTabs/BbTabsPanes.vue_vue_type_script_setup_true_lang.js +0 -36
@@ -35,6 +35,14 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
35
35
  import { dirname, join, sep } from 'node:path';
36
36
  import { fileURLToPath } from 'node:url';
37
37
  import { z } from 'zod';
38
+ import {
39
+ frontmatter,
40
+ outline,
41
+ pickSections,
42
+ searchSections,
43
+ splitSections,
44
+ stripFrontmatter,
45
+ } from '../scripts/lib/doc-sections.mjs';
38
46
  import {
39
47
  loadManifest,
40
48
  possessive,
@@ -126,13 +134,31 @@ const SEARCH_STOPWORDS = new Set([
126
134
  'element',
127
135
  ]);
128
136
 
129
- /** `'data table view'` → `['data', 'table']`. Order is not significant. */
137
+ /**
138
+ * `'data table view'` → `['data', 'table']`. Order is not significant.
139
+ *
140
+ * Names arrive as tags and identifiers too (review D6-6): `bb-select` used to
141
+ * return all 60 components on the token `bb`, and `BbCheckboxIcon` nothing.
142
+ * So a CamelCase word is also split into its words, the joined word loses a
143
+ * leading `bb`, and a bare `bb` is dropped.
144
+ */
130
145
  function tokenize(query) {
131
146
  const raw = String(query ?? '')
132
- .toLowerCase()
133
- .split(/[^a-z0-9]+/)
134
- .filter(Boolean);
135
- const meaningful = raw.filter((t) => !SEARCH_STOPWORDS.has(t));
147
+ .split(/[^A-Za-z0-9]+/)
148
+ .filter(Boolean)
149
+ .flatMap((word) => {
150
+ const lower = word.toLowerCase();
151
+ const parts = word
152
+ .replace(/^Bb(?=[A-Z])/, '')
153
+ .split(/(?<=[a-z0-9])(?=[A-Z])/)
154
+ .map((part) => part.toLowerCase());
155
+ return [
156
+ lower.replace(/^bb(?=.)/, ''),
157
+ ...(parts.length > 1 ? parts : []),
158
+ ];
159
+ })
160
+ .filter((token) => token && token !== 'bb');
161
+ const meaningful = [...new Set(raw)].filter((t) => !SEARCH_STOPWORDS.has(t));
136
162
  // An all-stopword query ('the view') still deserves its literal reading
137
163
  // rather than an empty token list that matches everything.
138
164
  return meaningful.length > 0 ? meaningful : raw;
@@ -228,22 +254,41 @@ function loadNotInLibrary() {
228
254
  assumed,
229
255
  reality,
230
256
  tokens: aliasTokens(assumed),
257
+ // The names the row is ABOUT: `BbSkeleton` → 'skeleton'.
258
+ names: [...assumed.matchAll(/`(\w+)`/g)].map(([, name]) =>
259
+ name.toLowerCase().replace(/^(bb|use)(?=.)/, '')
260
+ ),
231
261
  });
232
262
  }
233
263
  return cachedNotInLibrary;
234
264
  }
235
265
 
236
- /** Picker rows whose assumed-name cell shares a token with the query. */
266
+ /**
267
+ * Picker rows the query is about. A row matches when the query names the
268
+ * row's own component (`skeleton loading` → BbSkeleton, `drawer` → BbDrawer),
269
+ * or when every word of a multi-word query is in the row (`data table`),
270
+ * counting a shared 6-letter stem (`scheduler` / `scheduling`). One shared
271
+ * generic word is not enough any more: 'text input' answered with the
272
+ * rich-text-editor verdict as "the authoritative answer", 'tab' with the
273
+ * cross-tab live-feed row (review D6-7).
274
+ */
237
275
  function matchNotInLibrary(tokens) {
238
- const wanted = new Set(tokens);
276
+ if (!tokens.length) return [];
277
+ const covers = (row, token) =>
278
+ row.tokens.some(
279
+ (t) =>
280
+ t === token ||
281
+ (t.length >= 6 &&
282
+ token.length >= 6 &&
283
+ t.slice(0, 6) === token.slice(0, 6))
284
+ );
239
285
  return loadNotInLibrary()
240
- .map((row) => ({
241
- row,
242
- hits: row.tokens.filter((t) => wanted.has(t)).length,
243
- }))
244
- .filter(({ hits }) => hits > 0)
245
- .sort((a, b) => b.hits - a.hits)
246
- .map(({ row }) => ({ assumed: row.assumed, reality: row.reality }));
286
+ .filter(
287
+ (row) =>
288
+ tokens.some((t) => row.names.includes(t)) ||
289
+ (tokens.length > 1 && tokens.every((t) => covers(row, t)))
290
+ )
291
+ .map((row) => ({ assumed: row.assumed, reality: row.reality }));
247
292
  }
248
293
 
249
294
  function searchComponents({ query, limit, includeReferenceOnly } = {}) {
@@ -260,6 +305,17 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
260
305
  ? manifest.components
261
306
  : manifest.components.filter((c) => !c.referenceOnly);
262
307
 
308
+ // No query: the catalog, one line per component (review D6-19) — the
309
+ // answer to "which components exist?", which no tool gave before.
310
+ if (!needle || needle === '*') {
311
+ return {
312
+ query: query ?? '',
313
+ count: pool.length,
314
+ catalog: pool.map((c) => `${c.name} (${c.group}): ${c.description}`),
315
+ hint: 'The whole consumer catalog. get_component({ name }) for one; search_components({ query }) to rank by a need.',
316
+ };
317
+ }
318
+
263
319
  const tokens = tokenize(needle);
264
320
  const notInLibrary = matchNotInLibrary(tokens);
265
321
 
@@ -291,6 +347,16 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
291
347
 
292
348
  const shown = scored.slice(0, max);
293
349
 
350
+ // Internal components an exact search would have found (review D6-15):
351
+ // 'CommonPopover' answered "No manifest match" although it is in the
352
+ // manifest, only filtered out as reference-only.
353
+ const hiddenReferenceOnly = includeReferenceOnly
354
+ ? []
355
+ : manifest.components
356
+ .filter((c) => c.referenceOnly)
357
+ .filter((c) => scoreComponent(c, tokens).score >= 80)
358
+ .map((c) => c.name);
359
+
294
360
  /**
295
361
  * Public type exports whose name matches the query. Scored the same way as
296
362
  * a component name (exact, prefix, substring) but never mixed into the
@@ -316,6 +382,8 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
316
382
  ...(type.deprecated ? { deprecated: true } : {}),
317
383
  ...(type.replacedBy ? { replacedBy: type.replacedBy } : {}),
318
384
  usedBy: type.usedBy ?? [],
385
+ // The short answer only; get_component(name) has the definition.
386
+ ...(type.values ? { values: type.values } : {}),
319
387
  import: `import type { ${type.replacedBy ?? type.name} } from 'bitboss-ui';`,
320
388
  }));
321
389
 
@@ -345,6 +413,13 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
345
413
  // consumer who hit this concluded the export was undocumented and
346
414
  // allowlisted the tag.
347
415
  ...(matchedTypes.length > 0 ? { types: matchedTypes } : {}),
416
+ ...(hiddenReferenceOnly.length
417
+ ? {
418
+ hiddenReferenceOnly,
419
+ referenceOnlyHint:
420
+ 'Internal building blocks, not consumer API: never write them in a template. Use the ranked components; pass includeReferenceOnly: true only to read how one works.',
421
+ }
422
+ : {}),
348
423
  guidance:
349
424
  notInLibrary.length > 0
350
425
  ? 'The `notInLibrary` entries are the authoritative answer for this query — prefer them over the ranked components, and do not hand-roll what they redirect you to.'
@@ -401,9 +476,154 @@ function truncateMarkdown(text, maxChars, readHint) {
401
476
  };
402
477
  }
403
478
 
404
- function getComponent({ name, maxChars } = {}) {
479
+ /**
480
+ * Names an agent brings from v2, from an older beta or from habit, mapped to
481
+ * what replaced them. Parsed from the shipped docs so there is one source of
482
+ * truth: the v2 → v3 guide's § 5.1 component table, and changelog rows that
483
+ * say "BbX is renamed BbY". Without it `get_component('BbChip')` answered
484
+ * "unknown component" although the guide had the exact answer (review D6-5).
485
+ */
486
+ let cachedRetired = null;
487
+ function loadRetired() {
488
+ if (cachedRetired) return cachedRetired;
489
+ cachedRetired = new Map();
490
+ const guidePath = join(AI_ROOT, 'guides', 'migration', 'v2-to-v3.md');
491
+ if (existsSync(guidePath)) {
492
+ const md = readFileSync(guidePath, 'utf-8');
493
+ const table = md.split(/^### 5\.1 .*$/m)[1]?.split(/^### /m)[0] ?? '';
494
+ for (const line of table.split('\n')) {
495
+ if (!line.startsWith('|')) continue;
496
+ const [was, now, todo] = line
497
+ .replace(/^\|/, '')
498
+ .replace(/\|\s*$/, '')
499
+ .split('|')
500
+ .map((cell) => cell.trim());
501
+ if (!was || !now || /^-+$/.test(was) || was === 'v2') continue;
502
+ for (const [, retired] of was.matchAll(/`(\w+)`/g)) {
503
+ cachedRetired.set(retired, {
504
+ now: now.replace(/\*\*/g, ''),
505
+ todo: (todo ?? '').replace(/\*\*/g, '').split(/(?<=\.)\s/)[0],
506
+ guide: 'migration/v2-to-v3.md',
507
+ section: '5.1 Components renamed, split, or removed',
508
+ });
509
+ }
510
+ }
511
+ }
512
+ if (existsSync(CHANGELOG_PATH)) {
513
+ const doc = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8'));
514
+ for (const row of doc.breaking ?? []) {
515
+ const match = String(row.description ?? '').match(
516
+ /\b(Bb[A-Z]\w+) is renamed (Bb[A-Z]\w+)/
517
+ );
518
+ if (match && !cachedRetired.has(match[1])) {
519
+ cachedRetired.set(match[1], {
520
+ now: `\`${match[2]}\``,
521
+ todo: row.description.split(/(?<=\.)\s|—/)[0].trim(),
522
+ guide: row.guide ?? null,
523
+ firstReleasedIn: row.firstReleasedIn ?? null,
524
+ });
525
+ }
526
+ }
527
+ }
528
+ return cachedRetired;
529
+ }
530
+
531
+ /** `bb-date-picker-input` → `BbDatePickerInput`; `button` → `Button`. */
532
+ const pascal = (value) =>
533
+ String(value ?? '')
534
+ .split(/[^A-Za-z0-9]+/)
535
+ .filter(Boolean)
536
+ .map((part) => part[0].toUpperCase() + part.slice(1))
537
+ .join('');
538
+
539
+ /** Levenshtein distance, small inputs only. */
540
+ function editDistance(a, b) {
541
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
542
+ for (let i = 1; i <= a.length; i += 1) {
543
+ let previous = row[0];
544
+ row[0] = i;
545
+ for (let j = 1; j <= b.length; j += 1) {
546
+ const current = row[j];
547
+ row[j] = Math.min(
548
+ row[j] + 1,
549
+ row[j - 1] + 1,
550
+ previous + (a[i - 1] === b[j - 1] ? 0 : 1)
551
+ );
552
+ previous = current;
553
+ }
554
+ }
555
+ return row[b.length];
556
+ }
557
+
558
+ /**
559
+ * Find the component an agent meant. Exact first; then the spellings agents
560
+ * really send — `bb-button`, `bbbutton`, `Button` (review D6-2) — before
561
+ * giving up with the nearest names.
562
+ */
563
+ function resolveComponent(manifest, name) {
564
+ const raw = String(name ?? '').trim();
565
+ const exact = manifest.components.find((c) => c.name === raw);
566
+ if (exact) return { entry: exact };
567
+ const byLower = new Map(
568
+ manifest.components.map((c) => [c.name.toLowerCase(), c])
569
+ );
570
+ const candidates = [raw, pascal(raw)].flatMap((value) => [
571
+ value.toLowerCase(),
572
+ `bb${value.toLowerCase().replace(/^bb/, '')}`,
573
+ ]);
574
+ for (const candidate of candidates) {
575
+ const hit = byLower.get(candidate);
576
+ if (hit) return { entry: hit, resolvedFrom: raw };
577
+ }
578
+ return null;
579
+ }
580
+
581
+ function nearestComponents(manifest, name, count = 3) {
582
+ const needle = pascal(name).toLowerCase().replace(/^bb/, '');
583
+ return manifest.components
584
+ .filter((c) => !c.referenceOnly)
585
+ .map((c) => ({
586
+ name: c.name,
587
+ distance: editDistance(needle, c.name.toLowerCase().replace(/^bb/, '')),
588
+ }))
589
+ .sort((a, b) => a.distance - b.distance)
590
+ .slice(0, count)
591
+ .filter(({ distance }) => distance <= Math.max(3, needle.length / 2))
592
+ .map(({ name: found }) => found);
593
+ }
594
+
595
+ /** Guide sections of a contract: everything under `## Usage & Guidelines`. */
596
+ function contractGuideSections(markdown) {
597
+ const all = splitSections(markdown);
598
+ const start = all.findIndex(
599
+ (s) => s.level === 2 && /^Usage & Guidelines/i.test(s.title)
600
+ );
601
+ if (start === -1) return [];
602
+ const end = all.findIndex((s, i) => i > start && s.level === 2);
603
+ return all.slice(start + 1, end === -1 ? undefined : end);
604
+ }
605
+
606
+ /** Sections every agent needs, served whole in the default answer. */
607
+ const ALWAYS_SECTIONS = [/^gotchas/i, /^accessibility/i];
608
+
609
+ /**
610
+ * `get_component({ name, sections?, include?, maxChars? })`.
611
+ *
612
+ * COMPACT BY DEFAULT (release review 2026-09-23, D6-4). The old answer was
613
+ * the manifest entry plus the whole contract cut at 40 000 chars: 76–101 KB,
614
+ * and the cut dropped `Gotchas` on six components while the tail it kept
615
+ * repeated the structured entry. Blind Haiku answerers could not read it and
616
+ * guessed. Now the default is the structured entry (props never cut — a
617
+ * partial prop list is what makes agents invent API) minus the parts that
618
+ * repeat it (`pt:*` prop rows: the `pt` section lists every part and state),
619
+ * the guide's outline, and its Gotchas + Accessibility sections. Guide
620
+ * sections, the component tree, the CSS variables with their descriptions
621
+ * and the whole contract come on request.
622
+ */
623
+ function getComponent({ name, sections, include, maxChars } = {}) {
405
624
  const manifest = loadComponentsManifest();
406
- const entry = manifest.components.find((c) => c.name === name);
625
+ const resolved = resolveComponent(manifest, name);
626
+ const entry = resolved?.entry;
407
627
  if (!entry) {
408
628
  /*
409
629
  * A public TYPE asked for as a component. `BbTableColumn`,
@@ -425,18 +645,67 @@ function getComponent({ name, maxChars } = {}) {
425
645
  ...(type.replacedBy ? { replacedBy: type.replacedBy } : {}),
426
646
  component: type.component,
427
647
  usedBy: type.usedBy ?? [],
648
+ // What it is (Q50.2): the declaration, the accepted values when
649
+ // finite (open when `extensibleVia` names a registry), and the
650
+ // resolved fields when the declaration leans on a private type.
651
+ ...(type.definition ? { definition: type.definition } : {}),
652
+ ...(type.values ? { values: type.values } : {}),
653
+ ...(type.extensibleVia ? { extensibleVia: type.extensibleVia } : {}),
654
+ ...(type.fields ? { fields: type.fields } : {}),
428
655
  import: `import type { ${type.replacedBy ?? name} } from 'bitboss-ui';`,
429
656
  hint: use
430
- ? `A type, not a component: it types ${possessive(use.component)} \`${use.name}\` ${use.surface}. Call get_component('${use.component}') for the contract that documents its fields.`
657
+ ? `A type, not a component: it types ${possessive(use.component)} \`${use.name}\` ${use.surface}. \`definition\` above is the type itself; call get_component('${use.component}') to see it in use.`
431
658
  : 'A type, not a component — import it for the value you pass, never write it as a tag.',
432
659
  };
433
660
  }
661
+ // A directive (`v-bb-tooltip`, `vBbTooltip`) — review D3-13.
662
+ const directiveName = String(name ?? '')
663
+ .trim()
664
+ .replace(/^vBb([A-Z]\w*)$/, (_, word) => `v-bb-${word.toLowerCase()}`)
665
+ .toLowerCase();
666
+ const directive = (manifest.directives ?? []).find(
667
+ (d) => d.name === directiveName
668
+ );
669
+ if (directive) {
670
+ const value = (manifest.types ?? []).find(
671
+ (t) => t.name === directive.value
672
+ );
673
+ return {
674
+ found: true,
675
+ kind: 'directive',
676
+ ...directive,
677
+ ...(value?.definition ? { valueDefinition: value.definition } : {}),
678
+ ...(value?.fields ? { valueFields: value.fields } : {}),
679
+ hint: `A directive, the floating twin of ${directive.mirrors}: read get_component({ name: '${directive.mirrors}', sections: ['directive'] }) for its guide section.`,
680
+ };
681
+ }
682
+ const retired = loadRetired().get(String(name ?? '').trim());
683
+ if (retired) {
684
+ return {
685
+ found: false,
686
+ name,
687
+ retired: true,
688
+ now: retired.now,
689
+ todo: retired.todo,
690
+ guide: retired.guide,
691
+ ...(retired.section ? { section: retired.section } : {}),
692
+ hint: `\`${name}\` is not in this version. \`now\` says what replaced it; read get_guide({ name: '${retired.guide}'${retired.section ? `, sections: ['${retired.section}']` : ''} }) for the full row.`,
693
+ };
694
+ }
695
+ const picker = matchNotInLibrary(tokenize(name));
696
+ const didYouMean = nearestComponents(manifest, name);
434
697
  return {
435
698
  found: false,
436
699
  name,
437
- hint: 'Unknown component name — try search_components first.',
700
+ ...(didYouMean.length ? { didYouMean } : {}),
701
+ ...(picker.length ? { notInLibrary: picker.slice(0, 3) } : {}),
702
+ hint: picker.length
703
+ ? "Not a component here — `notInLibrary` is the picker's verdict for this name."
704
+ : 'Unknown component name. Try one of `didYouMean`, or search_components with a plain noun.',
438
705
  };
439
706
  }
707
+
708
+ const wanted = new Set(include ?? []);
440
709
  const contractPath = entry.contract ?? null;
441
710
  const contractAbsPath = contractPath
442
711
  ? join(PACKAGE_ROOT, 'dist', contractPath)
@@ -444,12 +713,9 @@ function getComponent({ name, maxChars } = {}) {
444
713
  const raw =
445
714
  contractAbsPath && existsSync(contractAbsPath)
446
715
  ? readFileSync(contractAbsPath, 'utf-8')
447
- : null;
448
- const { content, ...meta } = truncateMarkdown(
449
- raw,
450
- maxChars,
451
- `The structured \`component\` field above is COMPLETE (all props/events/slots) — the cut text is guidance prose only.`
452
- );
716
+ : '';
717
+ const guide = contractGuideSections(raw);
718
+
453
719
  /*
454
720
  * The types this component's own API names, so "what do I import to build
455
721
  * the value for `columns`?" is answered where the question is asked. Without
@@ -465,19 +731,98 @@ function getComponent({ name, maxChars } = {}) {
465
731
  .filter((type) => !type.deprecated)
466
732
  .map((type) => ({
467
733
  name: type.name,
468
- usage: type.usage,
469
734
  usedBy: (type.usedBy ?? []).filter((use) => use.component === entry.name),
470
735
  import: `import type { ${type.name} } from 'bitboss-ui';`,
736
+ ...(type.values ? { values: type.values } : {}),
737
+ ...(type.extensibleVia ? { extensibleVia: type.extensibleVia } : {}),
738
+ ...(type.fields ? { fields: type.fields } : {}),
739
+ ...(!type.values && !type.fields && type.definition
740
+ ? { definition: type.definition }
741
+ : {}),
471
742
  }));
472
743
 
744
+ const ptRows = entry.props.filter(
745
+ (p) => p.name === 'pt' || p.name.startsWith('pt:')
746
+ );
747
+ const component = {
748
+ ...entry,
749
+ props: (wanted.has('ptProps')
750
+ ? entry.props
751
+ : entry.props.filter((p) => !ptRows.includes(p))
752
+ ).map(({ required, ...prop }) => (required ? { ...prop, required } : prop)),
753
+ ...(entry.cssProps && !wanted.has('cssProps')
754
+ ? { cssProps: entry.cssProps.map((c) => c.name ?? c) }
755
+ : {}),
756
+ };
757
+ if (!wanted.has('tree')) delete component.tree;
758
+ // `removed` and `renamed` repeat each other with migration metadata; one
759
+ // line per old name answers "what happened to X" (include 'history' for
760
+ // the records the fixers use).
761
+ if (
762
+ !wanted.has('history') &&
763
+ (entry.removed?.length || entry.renamed?.length)
764
+ ) {
765
+ const renamedFrom = new Set((entry.renamed ?? []).map((r) => r.from));
766
+ component.history = [
767
+ ...(entry.renamed ?? []).map(
768
+ (r) =>
769
+ `${r.from} → ${r.to}${r.fix?.kind && r.fix.kind !== 'rename' ? ` (${r.fix.kind})` : ''}`
770
+ ),
771
+ ...(entry.removed ?? [])
772
+ .filter((r) => !renamedFrom.has(r.name))
773
+ .map((r) => `${r.name} removed${r.new ? ` → ${r.new}` : ''}`),
774
+ ];
775
+ delete component.removed;
776
+ delete component.renamed;
777
+ }
778
+
779
+ const { picked, missing } = pickSections(guide, sections);
780
+ const always = guide.filter(
781
+ (s) => ALWAYS_SECTIONS.some((re) => re.test(s.title)) && !picked.includes(s)
782
+ );
783
+ const contract = wanted.has('contract')
784
+ ? truncateMarkdown(
785
+ raw,
786
+ maxChars,
787
+ 'The structured `component` field above is complete; the cut text is guide prose.'
788
+ )
789
+ : null;
790
+
473
791
  return {
474
792
  found: true,
475
- // Never truncated: the precision-critical surface.
476
- component: entry,
793
+ ...(resolved.resolvedFrom ? { resolvedFrom: resolved.resolvedFrom } : {}),
794
+ component,
477
795
  ...(types.length > 0 ? { types } : {}),
796
+ ...(ptRows.length && !wanted.has('ptProps')
797
+ ? {
798
+ ptProps: `${ptRows.length} pt:* props not listed: every \`pt:<part>\` and \`pt:<part>:<state>\` in \`component.pt\` is a prop taking a class string (or object / array), and \`pt\` takes the same keys as one object.`,
799
+ }
800
+ : {}),
801
+ guide: {
802
+ sections: guide
803
+ .filter((s) => s.level === 3)
804
+ .map((s) => ({ title: s.title, chars: s.text.length })),
805
+ ...(picked.length
806
+ ? { requested: picked.map((s) => ({ title: s.title, text: s.text })) }
807
+ : {}),
808
+ ...(missing.length ? { notFound: missing } : {}),
809
+ ...(always.length
810
+ ? { always: always.map((s) => ({ title: s.title, text: s.text })) }
811
+ : {}),
812
+ },
478
813
  contractPath,
479
- contractContent: content,
480
- ...meta,
814
+ ...(contract
815
+ ? {
816
+ contract: contract.content,
817
+ ...(contract.truncated
818
+ ? {
819
+ contractTruncated: true,
820
+ contractHint: contract.hint,
821
+ }
822
+ : {}),
823
+ }
824
+ : {}),
825
+ hint: `Structured API above is complete. Read guide sections with get_component({ name: '${entry.name}', sections: ['<title from guide.sections>'] }); add include: ['tree' | 'cssProps' | 'ptProps' | 'history' | 'contract'] for the component tree, CSS variables with descriptions, every pt:* row, the full v2/beta rename records, or the whole contract. search_docs finds a topic across all docs.`,
481
826
  };
482
827
  }
483
828
 
@@ -546,6 +891,11 @@ const tokenKind = (inKnobBlock, inDerivedBlock) =>
546
891
  * tolerant of the missing final semicolon that minified CSS emits) is the
547
892
  * cheapest way to keep the scope — and this file ships in the package, so it
548
893
  * may not reach for postcss the way the dev-only `check:tokens` script does.
894
+ *
895
+ * `when` carries the enclosing `@supports` condition(s), if any: a value there
896
+ * is not the theme value but one browser path — `--bb-primary-fg` ships an
897
+ * auto-contrast formula with literal fallbacks, and folding the fallbacks into
898
+ * the theme values tells an agent to pin a literal that defeats the formula.
549
899
  */
550
900
  function* eachTokenDeclaration(css) {
551
901
  const stack = [];
@@ -566,11 +916,23 @@ function* eachTokenDeclaration(css) {
566
916
  quote = char;
567
917
  buffer += char;
568
918
  } else if (char === '{') {
569
- stack.push(buffer.trim());
919
+ // A comment before a rule would otherwise prefix its selector or
920
+ // hide the `@supports` it opens.
921
+ stack.push(buffer.replace(/\/\*[\s\S]*?\*\//g, '').trim());
570
922
  buffer = '';
571
923
  } else if (char === '}' || char === ';') {
572
924
  const decl = declaration();
573
- if (decl) yield { ...decl, selector: stack[stack.length - 1] ?? '' };
925
+ if (decl) {
926
+ const supports = stack
927
+ .slice(0, -1)
928
+ .filter((scope) => scope.startsWith('@supports'))
929
+ .map((scope) => scope.replace(/^@supports\s*/, ''));
930
+ yield {
931
+ ...decl,
932
+ selector: stack[stack.length - 1] ?? '',
933
+ ...(supports.length ? { when: supports.join(' and ') } : {}),
934
+ };
935
+ }
574
936
  if (char === '}') stack.pop();
575
937
  } else {
576
938
  buffer += char;
@@ -611,16 +973,27 @@ function listTokens({ filter } = {}) {
611
973
  }
612
974
  const css = readFileSync(STYLES_CSS, 'utf-8');
613
975
  const byName = new Map();
614
- for (const { name, value, selector } of eachTokenDeclaration(css)) {
976
+ for (const { name, value, selector, when } of eachTokenDeclaration(css)) {
615
977
  const entry = byName.get(name) ?? {
616
978
  theme: [],
617
979
  local: [],
980
+ conditional: [],
618
981
  knobBlock: false,
619
982
  derivedBlock: false,
620
983
  };
621
984
  if (isThemeSelector(selector)) {
622
985
  if (isDerivedBlock(selector)) entry.derivedBlock = true;
623
986
  else entry.knobBlock = true;
987
+ }
988
+ if (when) {
989
+ // One browser path, not the theme value nor a dark override.
990
+ if (
991
+ !entry.conditional.some(
992
+ (c) => c.when === when && c.selector === selector
993
+ )
994
+ )
995
+ entry.conditional.push({ when, selector, value });
996
+ } else if (isThemeSelector(selector)) {
624
997
  // Light first, dark after: report both rather than let one win.
625
998
  if (!entry.theme.includes(value)) entry.theme.push(value);
626
999
  } else if (!entry.local.some((r) => r.selector === selector)) {
@@ -637,12 +1010,13 @@ function listTokens({ filter } = {}) {
637
1010
  const tokens = [...byName.entries()]
638
1011
  .filter(([name]) => !needle || name.toLowerCase().includes(needle))
639
1012
  .sort(([a], [b]) => a.localeCompare(b))
640
- .map(([name, { theme, local, knobBlock, derivedBlock }]) => ({
1013
+ .map(([name, { theme, local, conditional, knobBlock, derivedBlock }]) => ({
641
1014
  name,
642
1015
  kind: tokenKind(knobBlock, derivedBlock),
643
1016
  value: theme[0],
644
1017
  ...(theme.length > 1 ? { themeOverrides: theme.slice(1) } : {}),
645
1018
  ...(local.length > 0 ? { localRebinds: local } : {}),
1019
+ ...(conditional.length > 0 ? { conditional } : {}),
646
1020
  }));
647
1021
  return {
648
1022
  count: tokens.length,
@@ -651,7 +1025,7 @@ function listTokens({ filter } = {}) {
651
1025
  hint:
652
1026
  needle && tokens.length === 0
653
1027
  ? `No token name contains '${needle}'. Call list_tokens with no filter for all ${byName.size} — the names are semantic (primary/danger/panel/text/border/radius/…), not literal colours.`
654
- : 'Theme tokens are set by the consumer app (e.g. on :root / .dark); `themeOverrides` is the dark-theme value. `localRebinds` is a component rebinding the token inside its own subtree — not something to copy. `kind` says what you may set: "knob" is yours; "derived" is a formula recomputed at every theme boundary, so setting it stops it tracking the knob it came from — set that knob instead; "knob-tracking" is a knob that follows another until you pin it, and the pin must be repeated at every boundary you use, .dark included. Component-level unprefixed tokens are documented per component; see the design-tokens guide via get_guide.',
1028
+ : 'Theme tokens are set by the consumer app (e.g. on :root / .dark); `themeOverrides` is the dark-theme value. `localRebinds` is a component rebinding the token inside its own subtree — not something to copy. `conditional` is a value that applies only under an @supports condition (`when`) — e.g. an auto-contrast formula with literal fallbacks for older browsers; pinning a fallback literal defeats the formula. `kind` says what you may set: "knob" is yours; "derived" is a formula recomputed at every theme boundary, so setting it stops it tracking the knob it came from — set that knob instead; "knob-tracking" is a knob that follows another until you pin it, and the pin must be repeated at every boundary you use, .dark included. Component-level unprefixed tokens are documented per component; see the design-tokens guide via get_guide.',
655
1029
  };
656
1030
  }
657
1031
 
@@ -670,41 +1044,276 @@ function collectGuidePaths(dir, prefix = '') {
670
1044
 
671
1045
  /** `list_guides()` — the shipped prose guides an agent can read. */
672
1046
  function listGuides() {
673
- const guides = collectGuidePaths(join(AI_ROOT, 'guides')).sort();
1047
+ const guidesDir = join(AI_ROOT, 'guides');
1048
+ const all = collectGuidePaths(guidesDir).sort();
1049
+ /*
1050
+ * Title + summary per guide (review D6-13): 57 bare paths could not tell an
1051
+ * agent which one answers its question. The 40 per-component migration
1052
+ * pages collapse to one row — get_guide('migration/components/<kebab>').
1053
+ */
1054
+ const perComponent = all.filter((p) => p.startsWith('migration/components/'));
1055
+ const guides = all
1056
+ .filter((p) => !perComponent.includes(p))
1057
+ .map((path) => {
1058
+ const markdown = readFileSync(join(guidesDir, path), 'utf-8');
1059
+ const meta = frontmatter(markdown);
1060
+ const title =
1061
+ meta.title ??
1062
+ stripFrontmatter(markdown)
1063
+ .match(/^# (.+)$/m)?.[1]
1064
+ ?.trim();
1065
+ return {
1066
+ name: path,
1067
+ ...(title ? { title } : {}),
1068
+ ...(meta.summary ? { summary: meta.summary } : {}),
1069
+ };
1070
+ });
674
1071
  return {
675
- count: guides.length,
1072
+ count: all.length,
676
1073
  guides,
677
- hint: 'Read one with get_guide({ name }). Start at `ai-router.md`, then `agent-contract.md` — do not read them all.',
1074
+ ...(perComponent.length
1075
+ ? {
1076
+ perComponentMigration: `${perComponent.length} pages: get_guide({ name: 'migration/components/<kebab-name>' }), e.g. 'migration/components/bb-select'.`,
1077
+ }
1078
+ : {}),
1079
+ hint: 'Read one with get_guide({ name }); a long guide answers with its outline — then pass sections. search_docs finds the section for a question across every guide. Start at `ai-router.md` — do not read them all.',
678
1080
  };
679
1081
  }
680
1082
 
681
- /** `get_guide({ name, maxChars })` — read one guide by relative path. */
682
- function getGuide({ name, maxChars } = {}) {
683
- const rel = String(name ?? '').replace(/^\/+/, '');
684
- // Contain reads to the guides dir — no traversal out of the knowledge base.
685
- if (!rel || rel.includes('..')) {
1083
+ /** Guides this size or larger answer with an outline unless asked for more. */
1084
+ // The router (13 KB, the documented entry point) still comes whole.
1085
+ const GUIDE_OUTLINE_OVER = 16000;
1086
+
1087
+ /**
1088
+ * The shipped guide a pointer means. Pointers come from everywhere — the
1089
+ * changelog (`components/bb-table.md (CSS table)`), contracts
1090
+ * (`guides/passthrough.md`), the repo layout (`ai/guides/…`) — and every form
1091
+ * used to answer "Guide not found" (review D6-11).
1092
+ */
1093
+ function resolveGuidePath(name) {
1094
+ const guidesDir = join(AI_ROOT, 'guides');
1095
+ const all = collectGuidePaths(guidesDir);
1096
+ let rel = String(name ?? '')
1097
+ .trim()
1098
+ .replace(/\s*\(.*\)\s*$/, '')
1099
+ .replace(/^\/+/, '')
1100
+ .replace(/^(dist\/)?(ai\/)?(guides\/)?/, '');
1101
+ if (!rel || rel.includes('..')) return null;
1102
+ if (!rel.endsWith('.md')) rel = `${rel}.md`;
1103
+ const tries = [rel, `migration/${rel}`, `migration/components/${rel}`];
1104
+ for (const attempt of tries) if (all.includes(attempt)) return attempt;
1105
+ const base = rel.split('/').pop();
1106
+ const byBase = all.filter((p) => p.split('/').pop() === base);
1107
+ return byBase.length === 1 ? byBase[0] : null;
1108
+ }
1109
+
1110
+ /** `get_guide({ name, sections?, maxChars? })`. */
1111
+ function getGuide({ name, sections, maxChars } = {}) {
1112
+ const resolved = resolveGuidePath(name);
1113
+ if (!resolved) {
1114
+ const all = collectGuidePaths(join(AI_ROOT, 'guides'));
1115
+ const needle = String(name ?? '').toLowerCase();
1116
+ const near = all
1117
+ .filter((p) =>
1118
+ needle
1119
+ .split(/[^a-z0-9]+/)
1120
+ .some((w) => w.length > 3 && p.toLowerCase().includes(w))
1121
+ )
1122
+ .slice(0, 5);
686
1123
  return {
687
1124
  found: false,
688
- error: `Invalid guide name '${name}'.`,
689
- hint: 'Call list_guides for the available names.',
1125
+ error: `Guide not found: ${name}`,
1126
+ ...(near.length ? { didYouMean: near } : {}),
1127
+ hint: 'Call list_guides for the names, or search_docs to find the section by topic.',
690
1128
  };
691
1129
  }
692
- const fileName = rel.endsWith('.md') ? rel : `${rel}.md`;
693
- const guidesDir = join(AI_ROOT, 'guides');
694
- const filePath = join(guidesDir, fileName);
695
- if (!filePath.startsWith(guidesDir) || !existsSync(filePath)) {
1130
+ const markdown = readFileSync(join(AI_ROOT, 'guides', resolved), 'utf-8');
1131
+ const meta = frontmatter(markdown);
1132
+ const all = splitSections(markdown);
1133
+ const base = {
1134
+ found: true,
1135
+ name: resolved,
1136
+ ...(resolved !== name ? { resolvedFrom: name } : {}),
1137
+ ...(meta.title ? { title: meta.title } : {}),
1138
+ };
1139
+ if (sections?.length) {
1140
+ const { picked, missing } = pickSections(all, sections);
696
1141
  return {
697
- found: false,
698
- error: `Guide not found: ${fileName}`,
699
- hint: 'Call list_guides for the available names.',
1142
+ ...base,
1143
+ sections: picked.map((s) => ({ title: s.title, text: s.text })),
1144
+ ...(missing.length ? { notFound: missing, outline: outline(all) } : {}),
700
1145
  };
701
1146
  }
702
- const { content, ...meta } = truncateMarkdown(
703
- readFileSync(filePath, 'utf-8'),
1147
+ const body = stripFrontmatter(markdown);
1148
+ if (maxChars === undefined && body.length >= GUIDE_OUTLINE_OVER) {
1149
+ const intro = body.split(/^## /m)[0].trim();
1150
+ return {
1151
+ ...base,
1152
+ ...(meta.summary ? { summary: meta.summary } : {}),
1153
+ chars: body.length,
1154
+ intro: intro.slice(0, 2000),
1155
+ outline: outline(all),
1156
+ hint: `This guide is ${body.length} chars. Read what you need: get_guide({ name: '${resolved}', sections: ['<title from outline>'] }) — a ## section includes its ### children. maxChars: 0 returns the whole guide.`,
1157
+ };
1158
+ }
1159
+ const { content, ...cut } = truncateMarkdown(
1160
+ markdown,
704
1161
  maxChars,
705
- `Guides are ordered narrative — the cut tail is later sections.`
1162
+ 'Guides are ordered narrative — the cut tail is later sections.'
1163
+ );
1164
+ return { ...base, content, ...cut };
1165
+ }
1166
+
1167
+ /**
1168
+ * `search_docs({ query, limit? })` — the section that answers a question,
1169
+ * across guides, composables, component guides, component props and recipes.
1170
+ *
1171
+ * WHY (review 2026-09-23). The facts Haiku missed were all shipped — the
1172
+ * `.dark` class, `cssLayers`, `passthroughDeduper`, `ssrViewportWidth`, the
1173
+ * `ai-init` and `changelog` commands, `prefill: false` — but each sat in one
1174
+ * section of a long document, and no tool could say which. Each result is a
1175
+ * snippet plus the exact call that reads the whole section.
1176
+ */
1177
+ let cachedDocIndex = null;
1178
+ function docIndex() {
1179
+ if (cachedDocIndex) return cachedDocIndex;
1180
+ const entries = [];
1181
+ const addSections = (source, markdown, read) => {
1182
+ for (const section of splitSections(markdown)) {
1183
+ if (section.own.length < 40) continue;
1184
+ entries.push({
1185
+ source,
1186
+ section: section.title,
1187
+ text: section.own,
1188
+ read: read(section.title),
1189
+ });
1190
+ }
1191
+ };
1192
+ const guidesDir = join(AI_ROOT, 'guides');
1193
+ for (const path of collectGuidePaths(guidesDir)) {
1194
+ addSections(
1195
+ `guide ${path}`,
1196
+ readFileSync(join(guidesDir, path), 'utf-8'),
1197
+ (title) => ({
1198
+ tool: 'get_guide',
1199
+ args: { name: path, sections: [title] },
1200
+ })
1201
+ );
1202
+ }
1203
+ const composablesDir = join(AI_ROOT, 'composables');
1204
+ if (existsSync(composablesDir)) {
1205
+ for (const file of readdirSync(composablesDir).filter((f) =>
1206
+ f.endsWith('.md')
1207
+ )) {
1208
+ const markdown = readFileSync(join(composablesDir, file), 'utf-8');
1209
+ const name = file.slice(0, -3);
1210
+ entries.push({
1211
+ source: `composable ${name}`,
1212
+ section: frontmatter(markdown).summary ?? name,
1213
+ text: stripFrontmatter(markdown).slice(0, 4000),
1214
+ read: { tool: 'get_composable', args: { name } },
1215
+ });
1216
+ }
1217
+ }
1218
+ const manifest = loadComponentsManifest();
1219
+ for (const component of manifest.components.filter((c) => !c.referenceOnly)) {
1220
+ const path = join(PACKAGE_ROOT, 'dist', component.contract ?? '');
1221
+ if (component.contract && existsSync(path)) {
1222
+ for (const section of contractGuideSections(
1223
+ readFileSync(path, 'utf-8')
1224
+ )) {
1225
+ if (section.own.length < 40) continue;
1226
+ entries.push({
1227
+ source: `component ${component.name}`,
1228
+ section: section.title,
1229
+ text: section.own,
1230
+ read: {
1231
+ tool: 'get_component',
1232
+ args: { name: component.name, sections: [section.title] },
1233
+ },
1234
+ });
1235
+ }
1236
+ }
1237
+ for (const prop of component.props) {
1238
+ if (prop.name.startsWith('pt')) continue;
1239
+ entries.push({
1240
+ source: `component ${component.name}`,
1241
+ section: `prop ${prop.name}`,
1242
+ text: `${prop.name}: ${prop.type}${prop.default ? ` = ${prop.default}` : ''}. ${prop.description ?? ''}`,
1243
+ read: { tool: 'get_component', args: { name: component.name } },
1244
+ });
1245
+ }
1246
+ for (const event of component.events ?? []) {
1247
+ entries.push({
1248
+ source: `component ${component.name}`,
1249
+ section: `event ${event.name}`,
1250
+ text: `@${event.name} ${event.signature}. ${event.description ?? ''}`,
1251
+ read: { tool: 'get_component', args: { name: component.name } },
1252
+ });
1253
+ }
1254
+ }
1255
+ for (const platform of RECIPE_PLATFORMS) {
1256
+ const dir = join(AI_ROOT, 'recipes', platform);
1257
+ if (!existsSync(dir)) continue;
1258
+ for (const file of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
1259
+ const name = file.slice(0, -3);
1260
+ const markdown = readFileSync(join(dir, file), 'utf-8');
1261
+ entries.push({
1262
+ source: `recipe ${platform}/${name}`,
1263
+ section: frontmatter(markdown).title ?? name,
1264
+ text: frontmatter(markdown).summary ?? name,
1265
+ read: { tool: 'get_recipe', args: { platform, name } },
1266
+ });
1267
+ }
1268
+ }
1269
+ const readme = join(PACKAGE_ROOT, 'README.md');
1270
+ if (existsSync(readme)) {
1271
+ addSections('README', readFileSync(readme, 'utf-8'), () => ({
1272
+ tool: 'read file',
1273
+ args: { path: 'node_modules/bitboss-ui/README.md' },
1274
+ }));
1275
+ }
1276
+ cachedDocIndex = entries;
1277
+ return entries;
1278
+ }
1279
+
1280
+ function searchDocs({ query, limit } = {}) {
1281
+ const max = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 20) : 8;
1282
+ const { tokens, total, results } = searchSections(docIndex(), query, max);
1283
+ return {
1284
+ query: query ?? '',
1285
+ tokens,
1286
+ count: results.length,
1287
+ ...(total > results.length ? { totalMatches: total } : {}),
1288
+ results,
1289
+ hint: results.length
1290
+ ? 'Each result is one section; `read` is the call that returns it whole. Prefer the top results: they match the most query words.'
1291
+ : 'Nothing matched. Use fewer, more specific words (a prop, option or component name), or search_components for components.',
1292
+ };
1293
+ }
1294
+
1295
+ /** `get_composable({ name })` — one composable's doc (useToast, useConfirm…). */
1296
+ function getComposable({ name } = {}) {
1297
+ const dir = join(AI_ROOT, 'composables');
1298
+ const files = existsSync(dir)
1299
+ ? readdirSync(dir).filter((f) => f.endsWith('.md'))
1300
+ : [];
1301
+ const wanted = String(name ?? '').replace(/\.md$/, '');
1302
+ const file = files.find(
1303
+ (f) => f.slice(0, -3).toLowerCase() === wanted.toLowerCase()
706
1304
  );
707
- return { found: true, name: fileName, content, ...meta };
1305
+ if (!file) {
1306
+ return {
1307
+ found: false,
1308
+ error: `Composable not found: ${name}`,
1309
+ available: files.map((f) => f.slice(0, -3)),
1310
+ };
1311
+ }
1312
+ return {
1313
+ found: true,
1314
+ name: file.slice(0, -3),
1315
+ content: stripFrontmatter(readFileSync(join(dir, file), 'utf-8')),
1316
+ };
708
1317
  }
709
1318
 
710
1319
  function listRecipes({ platform } = {}) {
@@ -716,21 +1325,40 @@ function listRecipes({ platform } = {}) {
716
1325
  error: `Unknown platform(s): ${invalid.join(', ')} — expected one of ${RECIPE_PLATFORMS.join(', ')}`,
717
1326
  };
718
1327
  }
719
- const recipes = [];
1328
+ /*
1329
+ * One row per recipe with its platforms, title, summary and size (review
1330
+ * D6-13): records-workspace / inline-edit-workspace / approvals-inbox could
1331
+ * not be told apart by name, and each costs 11–61 KB to open.
1332
+ */
1333
+ const byName = new Map();
720
1334
  for (const p of platforms) {
721
1335
  const dir = join(AI_ROOT, 'recipes', p);
722
1336
  if (!existsSync(dir)) continue;
723
1337
  for (const file of readdirSync(dir)) {
724
- if (file.endsWith('.md')) {
725
- recipes.push({ platform: p, name: file.slice(0, -3) });
726
- }
1338
+ if (!file.endsWith('.md')) continue;
1339
+ const name = file.slice(0, -3);
1340
+ const markdown = readFileSync(join(dir, file), 'utf-8');
1341
+ const meta = frontmatter(markdown);
1342
+ const row = byName.get(name) ?? {
1343
+ name,
1344
+ ...(meta.title ? { title: meta.title } : {}),
1345
+ ...(meta.summary ? { summary: meta.summary } : {}),
1346
+ platforms: [],
1347
+ chars: {},
1348
+ };
1349
+ row.platforms.push(p);
1350
+ row.chars[p] = markdown.length;
1351
+ byName.set(name, row);
727
1352
  }
728
1353
  }
729
- return { recipes };
1354
+ return {
1355
+ recipes: [...byName.values()],
1356
+ hint: 'get_recipe({ platform, name }) returns the whole recipe (they are copied whole); pass sections to read only part of a long one.',
1357
+ };
730
1358
  }
731
1359
 
732
1360
  /** `get_recipe({ platform, name })` — read one recipe's markdown content. */
733
- function getRecipe({ platform, name, maxChars } = {}) {
1361
+ function getRecipe({ platform, name, sections, maxChars } = {}) {
734
1362
  if (!RECIPE_PLATFORMS.includes(platform)) {
735
1363
  return {
736
1364
  found: false,
@@ -760,9 +1388,26 @@ function getRecipe({ platform, name, maxChars } = {}) {
760
1388
  hint: 'Call list_recipes for the available names.',
761
1389
  };
762
1390
  }
1391
+ const markdown = readFileSync(filePath, 'utf-8');
1392
+ if (sections?.length) {
1393
+ const all = splitSections(markdown);
1394
+ const { picked, missing } = pickSections(all, sections);
1395
+ return {
1396
+ found: true,
1397
+ platform,
1398
+ name: fileName.slice(0, -3),
1399
+ sections: picked.map((section) => ({
1400
+ title: section.title,
1401
+ text: section.text,
1402
+ })),
1403
+ ...(missing.length ? { notFound: missing, outline: outline(all) } : {}),
1404
+ };
1405
+ }
1406
+ // Whole by default (review D6-14): a recipe is copied whole, and the old
1407
+ // 40 000-char default cut 7 of 9 vue recipes inside their page code.
763
1408
  const { content, ...meta } = truncateMarkdown(
764
- readFileSync(filePath, 'utf-8'),
765
- maxChars,
1409
+ markdown,
1410
+ maxChars ?? 0,
766
1411
  `Recipes are ordered narrative — the cut tail is later sections.`
767
1412
  );
768
1413
  return {
@@ -825,7 +1470,12 @@ function compareVersions(a, b) {
825
1470
  * halves are fixed: rows are stamped with `firstReleasedIn` at build time, and
826
1471
  * this serves the slice.
827
1472
  */
828
- function listChanges({ since, until } = {}) {
1473
+ function listChanges({ since: rawSince, until: rawUntil } = {}) {
1474
+ // The repo's own tags are spelled `v3.0.0-beta.33` (review D6-12).
1475
+ const since =
1476
+ rawSince == null ? rawSince : String(rawSince).replace(/^v/, '');
1477
+ const until =
1478
+ rawUntil == null ? rawUntil : String(rawUntil).replace(/^v/, '');
829
1479
  if (!existsSync(CHANGELOG_PATH)) {
830
1480
  return {
831
1481
  error: `changelog.json not found at ${CHANGELOG_PATH} — reinstall bitboss-ui (or run \`npm run build\` when working in the library repo).`,
@@ -895,7 +1545,8 @@ function toolCallback(fn) {
895
1545
  try {
896
1546
  const result = fn(args ?? {});
897
1547
  return {
898
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1548
+ // Compact JSON: indentation was a quarter of every answer (review D6).
1549
+ content: [{ type: 'text', text: JSON.stringify(result) }],
899
1550
  isError: false,
900
1551
  };
901
1552
  } catch (error) {
@@ -910,11 +1561,25 @@ function toolCallback(fn) {
910
1561
 
911
1562
  const platformSchema = z.enum(RECIPE_PLATFORMS);
912
1563
 
1564
+ /**
1565
+ * Sent once at initialize (review D6-1): an MCP-only agent otherwise had no
1566
+ * way to know which tool answers which question, and read whole guides.
1567
+ */
1568
+ const SERVER_INSTRUCTIONS = [
1569
+ 'bitboss-ui: a Vue 3 component library. Answer from these tools, never from memory — agents that guess invent props.',
1570
+ '- A topic or "how do I…" question (dark mode, cascade layers, SSR, a plugin option, a CLI command): search_docs, then open the section it points to.',
1571
+ '- Which component: search_components (empty query = the whole catalog). One component: get_component({ name }) — compact; pass sections for guide prose, include for more.',
1572
+ '- A type in a props table (BbTableColumn, ButtonVariantType): get_component({ name }) returns its definition and values.',
1573
+ '- After writing markup: validate. Before custom CSS: list_tokens. Before bumping the version: list_changes({ since }).',
1574
+ '- Composables (useToast, useConfirm…): get_composable. Whole pages: list_recipes, get_recipe.',
1575
+ '- Long guides answer with an outline: pass sections.',
1576
+ ].join('\n');
1577
+
913
1578
  /** Register the knowledge-base tools on a fresh McpServer instance. */
914
1579
  function buildServer() {
915
1580
  const server = new McpServer(
916
1581
  { name: 'bitboss-ui', version: packageVersion() },
917
- { capabilities: { tools: {} } }
1582
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
918
1583
  );
919
1584
 
920
1585
  server.registerTool(
@@ -949,16 +1614,30 @@ function buildServer() {
949
1614
  'get_component',
950
1615
  {
951
1616
  description:
952
- 'Get the full manifest entry (props/events/slots/models, removed/renamed history) for one bitboss-ui component by exact name, plus the rendered contract markdown content.',
1617
+ "One bitboss-ui component: every prop, event, slot, v-model and pt part (never cut), the types its API uses with their values, the guide's outline and its Gotchas + Accessibility sections. Compact by default; pass `sections` for guide prose and `include` for more. Also answers for a public TYPE name (its definition and values), a v2/beta name that was removed or renamed (what replaced it), and near-miss spellings ('bb-button', 'Button').",
953
1618
  inputSchema: {
954
- name: z.string().describe("Exact component name, e.g. 'BbButton'."),
1619
+ name: z
1620
+ .string()
1621
+ .describe("Component name, e.g. 'BbButton' ('bb-button' works too)."),
1622
+ sections: z
1623
+ .array(z.string())
1624
+ .optional()
1625
+ .describe(
1626
+ "Guide sections to return whole, by title from `guide.sections`, e.g. ['Toggle buttons']. Case-insensitive; a partial title matches."
1627
+ ),
1628
+ include: z
1629
+ .array(z.enum(['tree', 'cssProps', 'ptProps', 'history', 'contract']))
1630
+ .optional()
1631
+ .describe(
1632
+ 'Extra parts: tree = the component tree, cssProps = CSS variables with descriptions, ptProps = every pt:* prop row, history = full v2/beta rename records, contract = the whole contract markdown.'
1633
+ ),
955
1634
  maxChars: z
956
1635
  .number()
957
1636
  .int()
958
1637
  .min(0)
959
1638
  .optional()
960
1639
  .describe(
961
- `Cap on the returned contract markdown (default ${MARKDOWN_DEFAULT_MAX_CHARS}; 0 = no cap). The structured props/events/slots are never truncated.`
1640
+ `With include: ['contract'] only — cap on the contract markdown (default ${MARKDOWN_DEFAULT_MAX_CHARS}; 0 = no cap).`
962
1641
  ),
963
1642
  },
964
1643
  },
@@ -1019,13 +1698,19 @@ function buildServer() {
1019
1698
  'get_guide',
1020
1699
  {
1021
1700
  description:
1022
- "Read one bitboss-ui guide by name (as listed by list_guides, e.g. 'ai-router.md' or 'migration/v2-to-v3.md').",
1701
+ "Read one bitboss-ui guide (names from list_guides, e.g. 'design-tokens' or 'migration/v2-to-v3'). A long guide answers with its outline: then pass `sections`. Pointers such as 'ai/guides/passthrough.md' or 'components/bb-table.md' resolve too.",
1023
1702
  inputSchema: {
1024
1703
  name: z
1025
1704
  .string()
1026
1705
  .describe(
1027
1706
  "Guide path relative to the guides dir, e.g. 'design-tokens.md'. The '.md' suffix is optional."
1028
1707
  ),
1708
+ sections: z
1709
+ .array(z.string())
1710
+ .optional()
1711
+ .describe(
1712
+ "Section titles from the outline, e.g. ['Dark mode']. A ## section includes its ### children."
1713
+ ),
1029
1714
  maxChars: z
1030
1715
  .number()
1031
1716
  .int()
@@ -1043,7 +1728,7 @@ function buildServer() {
1043
1728
  'list_recipes',
1044
1729
  {
1045
1730
  description:
1046
- 'List available page-level recipes (whole-page compositions), optionally filtered by platform.',
1731
+ 'List the page-level recipes (whole-page compositions) with title, summary, platforms and size, optionally filtered by platform.',
1047
1732
  inputSchema: {
1048
1733
  platform: platformSchema
1049
1734
  .optional()
@@ -1057,8 +1742,12 @@ function buildServer() {
1057
1742
  'get_recipe',
1058
1743
  {
1059
1744
  description:
1060
- 'Read the full markdown content of one recipe by platform and name.',
1745
+ 'Read one recipe by platform and name — whole by default (recipes are copied whole); pass `sections` to read part of a long one.',
1061
1746
  inputSchema: {
1747
+ sections: z
1748
+ .array(z.string())
1749
+ .optional()
1750
+ .describe('Section titles to return instead of the whole recipe.'),
1062
1751
  platform: platformSchema.describe(
1063
1752
  'Which platform variant of the recipe to read.'
1064
1753
  ),
@@ -1070,12 +1759,44 @@ function buildServer() {
1070
1759
  .int()
1071
1760
  .min(0)
1072
1761
  .optional()
1762
+ .describe('Optional cap on the returned markdown (default: no cap).'),
1763
+ },
1764
+ },
1765
+ toolCallback(getRecipe)
1766
+ );
1767
+
1768
+ server.registerTool(
1769
+ 'search_docs',
1770
+ {
1771
+ description:
1772
+ 'Find the section that answers a question, across every guide, component guide, component prop and event, composable, recipe and the README. Use it for "how do I…" and "is there…" questions (dark mode, cascade layers, SSR, a plugin option, a CLI command, a migration). Each result is a snippet plus the exact call that reads the whole section.',
1773
+ inputSchema: {
1774
+ query: z
1775
+ .string()
1073
1776
  .describe(
1074
- `Cap on the returned markdown (default ${MARKDOWN_DEFAULT_MAX_CHARS}; 0 = no cap).`
1777
+ "A few specific words, e.g. 'dark mode class' or 'tailwind cascade layers'."
1075
1778
  ),
1779
+ limit: z
1780
+ .number()
1781
+ .int()
1782
+ .positive()
1783
+ .optional()
1784
+ .describe('Max results (default 8, max 20).'),
1076
1785
  },
1077
1786
  },
1078
- toolCallback(getRecipe)
1787
+ toolCallback(searchDocs)
1788
+ );
1789
+
1790
+ server.registerTool(
1791
+ 'get_composable',
1792
+ {
1793
+ description:
1794
+ "Read one composable's doc (useToast, useConfirm, useBbTableContext, useMobile…): what it returns, what host it needs, examples.",
1795
+ inputSchema: {
1796
+ name: z.string().describe("Composable name, e.g. 'useToast'."),
1797
+ },
1798
+ },
1799
+ toolCallback(getComposable)
1079
1800
  );
1080
1801
 
1081
1802
  server.registerTool(
@@ -1132,7 +1853,11 @@ export const start = main;
1132
1853
  * concurrently. Importing this module does not start a server.
1133
1854
  */
1134
1855
  export {
1856
+ buildServer,
1857
+ SERVER_INSTRUCTIONS,
1135
1858
  searchComponents,
1859
+ searchDocs,
1860
+ getComposable,
1136
1861
  getComponent,
1137
1862
  listTokens,
1138
1863
  listGuides,