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

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 (350) hide show
  1. package/README.md +72 -33
  2. package/dist/ai/BbAccordion.md +3 -3
  3. package/dist/ai/BbAlert.md +9 -5
  4. package/dist/ai/BbAsterisk.md +17 -0
  5. package/dist/ai/BbAvatar.md +12 -3
  6. package/dist/ai/BbBadge.md +15 -10
  7. package/dist/ai/BbBadgeButton.md +16 -16
  8. package/dist/ai/BbBaseButton.md +16 -16
  9. package/dist/ai/BbBaseCheckbox.md +8 -8
  10. package/dist/ai/BbBaseCheckboxGroup.md +13 -13
  11. package/dist/ai/BbBaseCheckboxIcon.md +26 -9
  12. package/dist/ai/BbBaseColorInput.md +11 -11
  13. package/dist/ai/BbBaseDatePicker.md +6 -6
  14. package/dist/ai/BbBaseDatePickerInput.md +15 -15
  15. package/dist/ai/BbBaseInputContainer.md +12 -12
  16. package/dist/ai/BbBaseNumberInput.md +8 -8
  17. package/dist/ai/BbBaseRadio.md +5 -5
  18. package/dist/ai/BbBaseRadioGroup.md +7 -7
  19. package/dist/ai/BbBaseRadioIcon.md +25 -9
  20. package/dist/ai/BbBaseRating.md +5 -5
  21. package/dist/ai/BbBaseSelect.md +22 -21
  22. package/dist/ai/BbBaseSlider.md +9 -9
  23. package/dist/ai/BbBaseSwitch.md +8 -8
  24. package/dist/ai/BbBaseSwitchGroup.md +13 -13
  25. package/dist/ai/BbBaseSwitchIcon.md +28 -10
  26. package/dist/ai/BbBaseTag.md +10 -10
  27. package/dist/ai/BbBaseTextInput.md +9 -9
  28. package/dist/ai/BbBaseTextarea.md +9 -9
  29. package/dist/ai/BbBaseTimePickerInput.md +13 -13
  30. package/dist/ai/BbBreadcrumbs.md +2 -2
  31. package/dist/ai/BbButton.md +15 -15
  32. package/dist/ai/BbCheckbox.md +18 -17
  33. package/dist/ai/BbCheckboxGroup.md +15 -14
  34. package/dist/ai/BbCollapsible.md +11 -9
  35. package/dist/ai/BbColorInput.md +15 -15
  36. package/dist/ai/BbColorPalette.md +12 -6
  37. package/dist/ai/BbConfirm.md +12 -5
  38. package/dist/ai/BbDatePicker.md +13 -13
  39. package/dist/ai/BbDatePickerInput.md +20 -20
  40. package/dist/ai/BbDialog.md +78 -12
  41. package/dist/ai/BbDropdown.md +132 -22
  42. package/dist/ai/BbDropdownButton.md +12 -12
  43. package/dist/ai/BbDropdownGroup.md +1 -1
  44. package/dist/ai/BbDropzone.md +4 -4
  45. package/dist/ai/BbForm.md +49 -14
  46. package/dist/ai/BbIcon.md +6 -0
  47. package/dist/ai/BbIndicator.md +5 -5
  48. package/dist/ai/BbNumberInput.md +11 -11
  49. package/dist/ai/BbOffCanvas.md +90 -20
  50. package/dist/ai/BbPagination.md +3 -3
  51. package/dist/ai/BbPopover.md +32 -17
  52. package/dist/ai/BbProgress.md +7 -4
  53. package/dist/ai/BbRadio.md +16 -15
  54. package/dist/ai/BbRadioGroup.md +20 -17
  55. package/dist/ai/BbRating.md +11 -11
  56. package/dist/ai/BbSelect.md +138 -37
  57. package/dist/ai/BbSelectPopover.md +40 -41
  58. package/dist/ai/BbSlider.md +37 -22
  59. package/dist/ai/BbSmoothHeight.md +1 -1
  60. package/dist/ai/BbSwitch.md +25 -10
  61. package/dist/ai/BbSwitchGroup.md +17 -14
  62. package/dist/ai/BbTable.md +920 -109
  63. package/dist/ai/BbTabs.md +28 -14
  64. package/dist/ai/BbTabsList.md +1 -1
  65. package/dist/ai/BbTabsPanes.md +1 -1
  66. package/dist/ai/BbTabsRoot.md +10 -10
  67. package/dist/ai/BbTag.md +14 -14
  68. package/dist/ai/BbTextInput.md +16 -15
  69. package/dist/ai/BbTextarea.md +13 -13
  70. package/dist/ai/BbTimePicker.md +10 -10
  71. package/dist/ai/BbTimePickerInput.md +17 -17
  72. package/dist/ai/BbToast.md +1 -1
  73. package/dist/ai/BbToastPortal.md +1 -1
  74. package/dist/ai/BbTooltip.md +12 -10
  75. package/dist/ai/BbTree.md +1 -1
  76. package/dist/ai/ChipsBox.md +1 -1
  77. package/dist/ai/CommonInputWrapper.md +10 -10
  78. package/dist/ai/CommonPopover.md +6 -6
  79. package/dist/ai/FlatListBox.md +5 -5
  80. package/dist/ai/GroupedListBox.md +5 -5
  81. package/dist/ai/ListBox.md +4 -4
  82. package/dist/ai/OptionsContainer.md +4 -4
  83. package/dist/ai/changelog.json +128 -2
  84. package/dist/ai/components.json +917 -100
  85. package/dist/ai/composables/useBbConfig.md +72 -11
  86. package/dist/ai/composables/useConfirm.md +5 -3
  87. package/dist/ai/guides/ai-router.md +9 -4
  88. package/dist/ai/guides/component-picker.md +18 -18
  89. package/dist/ai/guides/design-language.md +14 -14
  90. package/dist/ai/guides/design-tokens.md +25 -18
  91. package/dist/ai/guides/fetch-items-playbook.md +1 -1
  92. package/dist/ai/guides/icons-policy.md +11 -5
  93. package/dist/ai/guides/installation-and-plugin-setup.md +85 -23
  94. package/dist/ai/guides/migration/components/bb-checkbox-group.md +30 -0
  95. package/dist/ai/guides/migration/components/bb-checkbox.md +37 -3
  96. package/dist/ai/guides/migration/components/bb-collapsible.md +5 -5
  97. package/dist/ai/guides/migration/components/bb-dialog.md +62 -34
  98. package/dist/ai/guides/migration/components/bb-offcanvas.md +29 -13
  99. package/dist/ai/guides/migration/components/bb-rating.md +8 -2
  100. package/dist/ai/guides/migration/components/bb-select.md +5 -2
  101. package/dist/ai/guides/migration/components/bb-table.md +560 -28
  102. package/dist/ai/guides/migration/components/bb-text-input.md +6 -6
  103. package/dist/ai/guides/migration/v2-to-v3.md +260 -89
  104. package/dist/ai/guides/ssr-prefill.md +120 -0
  105. package/dist/ai/index.md +10 -9
  106. package/dist/ai/recipes/inertia/approvals-inbox.md +0 -1
  107. package/dist/ai/recipes/inertia/inline-edit-workspace.md +0 -1
  108. package/dist/ai/recipes/inertia/layout-scaffold.md +5 -1
  109. package/dist/ai/recipes/inertia/ownership-atlas.md +49 -29
  110. package/dist/ai/recipes/inertia/record-form.md +0 -1
  111. package/dist/ai/recipes/inertia/records-workspace.md +26 -8
  112. package/dist/ai/recipes/inertia/upload-center.md +0 -1
  113. package/dist/ai/recipes/nuxt/approvals-inbox.md +0 -1
  114. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +0 -1
  115. package/dist/ai/recipes/nuxt/layout-scaffold.md +5 -1
  116. package/dist/ai/recipes/nuxt/record-form.md +0 -1
  117. package/dist/ai/recipes/nuxt/records-workspace.md +26 -8
  118. package/dist/ai/recipes/nuxt/upload-center.md +0 -1
  119. package/dist/ai/recipes/vue/approvals-inbox.md +0 -1
  120. package/dist/ai/recipes/vue/inline-edit-workspace.md +0 -1
  121. package/dist/ai/recipes/vue/layout-scaffold.md +5 -1
  122. package/dist/ai/recipes/vue/records-workspace.md +26 -8
  123. package/dist/ai/recipes/vue/upload-center.md +0 -1
  124. package/dist/ai/source/BbAlert.md +60 -2
  125. package/dist/ai/source/BbAvatar.md +3 -3
  126. package/dist/ai/source/BbBadge.md +60 -15
  127. package/dist/ai/source/BbBadgeButton.md +8 -2
  128. package/dist/ai/source/BbBaseButton.md +3 -1
  129. package/dist/ai/source/BbBaseCheckboxGroup.md +6 -2
  130. package/dist/ai/source/BbBaseDatePicker.md +3 -1
  131. package/dist/ai/source/BbBaseDatePickerInput.md +5 -1
  132. package/dist/ai/source/BbBaseInputContainer.md +52 -1
  133. package/dist/ai/source/BbBaseRadioGroup.md +4 -0
  134. package/dist/ai/source/BbBaseRating.md +3 -1
  135. package/dist/ai/source/BbBaseSelect.md +71 -16
  136. package/dist/ai/source/BbBaseSwitchGroup.md +4 -0
  137. package/dist/ai/source/BbBaseTimePickerInput.md +5 -1
  138. package/dist/ai/source/BbBreadcrumbs.md +31 -0
  139. package/dist/ai/source/BbButton.md +51 -24
  140. package/dist/ai/source/BbCheckbox.md +15 -1
  141. package/dist/ai/source/BbCheckboxGroup.md +2 -1
  142. package/dist/ai/source/BbCollapsible.md +7 -1
  143. package/dist/ai/source/BbColorInput.md +3 -4
  144. package/dist/ai/source/BbColorPalette.md +3 -4
  145. package/dist/ai/source/BbDatePicker.md +5 -1
  146. package/dist/ai/source/BbDatePickerInput.md +5 -1
  147. package/dist/ai/source/BbDialog.md +175 -19
  148. package/dist/ai/source/BbDropdown.md +376 -69
  149. package/dist/ai/source/BbDropdownGroup.md +330 -59
  150. package/dist/ai/source/BbForm.md +93 -5
  151. package/dist/ai/source/BbIcon.md +46 -36
  152. package/dist/ai/source/BbNumberInput.md +3 -4
  153. package/dist/ai/source/BbOffCanvas.md +474 -194
  154. package/dist/ai/source/BbPopover.md +24 -17
  155. package/dist/ai/source/BbProgress.md +2 -2
  156. package/dist/ai/source/BbRadio.md +15 -1
  157. package/dist/ai/source/BbRadioGroup.md +3 -1
  158. package/dist/ai/source/BbSelect.md +50 -2
  159. package/dist/ai/source/BbSelectPopover.md +171 -23
  160. package/dist/ai/source/BbSlider.md +3 -4
  161. package/dist/ai/source/BbSwitch.md +15 -1
  162. package/dist/ai/source/BbSwitchGroup.md +8 -5
  163. package/dist/ai/source/BbTable.md +5092 -1446
  164. package/dist/ai/source/BbTabs.md +6 -7
  165. package/dist/ai/source/BbTabsList.md +6 -7
  166. package/dist/ai/source/BbTabsPanes.md +6 -7
  167. package/dist/ai/source/BbTabsRoot.md +6 -7
  168. package/dist/ai/source/BbTag.md +3 -4
  169. package/dist/ai/source/BbTextInput.md +3 -4
  170. package/dist/ai/source/BbTextarea.md +3 -4
  171. package/dist/ai/source/BbTimePicker.md +4 -1
  172. package/dist/ai/source/BbTimePickerInput.md +5 -1
  173. package/dist/ai/source/BbToast.md +3 -1
  174. package/dist/ai/source/BbTooltip.md +5 -1
  175. package/dist/ai/source/CommonInputWrapper.md +4 -0
  176. package/dist/ai/source/CommonPopover.md +6 -0
  177. package/dist/ai/source/FlatListBox.md +11 -5
  178. package/dist/ai/source/GroupedListBox.md +7 -5
  179. package/dist/components/BbAlert/BbAlert.vue_vue_type_script_setup_true_lang.js +4 -4
  180. package/dist/components/BbAlert/types.d.ts +10 -1
  181. package/dist/components/BbAvatar/BbAvatar.vue.d.ts +1 -1
  182. package/dist/components/BbAvatar/BbAvatar.vue_vue_type_script_setup_true_lang.js +33 -33
  183. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +22 -16
  184. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +36 -15
  185. package/dist/components/BbBadge/badgeButtonContext.d.ts +21 -8
  186. package/dist/components/BbBadge/types.d.ts +8 -2
  187. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +47 -47
  188. package/dist/components/BbBaseButton/RouterComponent.vue_vue_type_script_setup_true_lang.js +14 -14
  189. package/dist/components/BbBaseCheckboxGroup/BbBaseCheckboxGroup.vue_vue_type_script_setup_true_lang.js +30 -28
  190. package/dist/components/BbBaseCheckboxGroup/types.d.ts +2 -2
  191. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +167 -167
  192. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue.d.ts +9 -9
  193. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +78 -78
  194. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue_vue_type_script_setup_true_lang.js +64 -64
  195. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue.d.ts +1 -1
  196. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +138 -138
  197. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue_vue_type_script_setup_true_lang.js +2 -2
  198. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue_vue_type_script_setup_true_lang.js +24 -22
  199. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +40 -40
  200. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +74 -65
  201. package/dist/components/BbBaseSelect/types.d.ts +22 -0
  202. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +5 -5
  203. package/dist/components/BbBaseSwitchGroup/BbBaseSwitchGroup.vue_vue_type_script_setup_true_lang.js +30 -28
  204. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue.d.ts +1 -1
  205. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +104 -104
  206. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +35 -35
  207. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +85 -81
  208. package/dist/components/BbCheckboxGroup/types.d.ts +2 -1
  209. package/dist/components/BbCollapsible/types.d.ts +7 -1
  210. package/dist/components/BbColorInput/BbColorInput.vue.d.ts +1 -1
  211. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +18 -18
  212. package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +2 -2
  213. package/dist/components/BbColorPalette/BbColorPalette.vue_vue_type_script_setup_true_lang.js +50 -50
  214. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +17 -17
  215. package/dist/components/BbDatePicker/BbDatePicker.vue.d.ts +1 -1
  216. package/dist/components/BbDatePicker/BbDatePicker.vue_vue_type_script_setup_true_lang.js +100 -100
  217. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +21 -21
  218. package/dist/components/BbDialog/BbDialog.vue.d.ts +1 -1
  219. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +133 -107
  220. package/dist/components/BbDialog/types.d.ts +29 -2
  221. package/dist/components/BbDropdown/AdaptiveDropdown.vue.d.ts +2 -0
  222. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +99 -100
  223. package/dist/components/BbDropdown/BbDropdown.vue.d.ts +2 -2
  224. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +208 -201
  225. package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +225 -208
  226. package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +21 -21
  227. package/dist/components/BbDropdown/types.d.ts +20 -5
  228. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +5 -5
  229. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +46 -46
  230. package/dist/components/BbIcon/iconCache.d.ts +33 -0
  231. package/dist/components/BbIcon/iconCache.js +17 -0
  232. package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +1 -1
  233. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +15 -15
  234. package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +1 -1
  235. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +254 -208
  236. package/dist/components/BbOffCanvas/types.d.ts +30 -5
  237. package/dist/components/BbPagination/BbPagination.vue.d.ts +1 -1
  238. package/dist/components/BbPopover/BbPopover.vue.d.ts +2 -2
  239. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +149 -146
  240. package/dist/components/BbPopover/types.d.ts +3 -2
  241. package/dist/components/BbRadioGroup/types.d.ts +3 -1
  242. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +27 -23
  243. package/dist/components/BbSelect/types.d.ts +32 -0
  244. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +221 -213
  245. package/dist/components/BbSlider/BbSlider.vue.d.ts +1 -1
  246. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +20 -20
  247. package/dist/components/BbTable/BbTable.vue.d.ts +10 -4
  248. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +1433 -600
  249. package/dist/components/BbTable/BbTableDataRow.d.ts +162 -0
  250. package/dist/components/BbTable/BbTableDataRow.js +136 -0
  251. package/dist/components/BbTable/BbTableExpandRow.d.ts +73 -0
  252. package/dist/components/BbTable/BbTableExpandRow.js +52 -0
  253. package/dist/components/BbTable/types.d.ts +324 -111
  254. package/dist/components/BbTable/utils.d.ts +92 -4
  255. package/dist/components/BbTable/utils.js +71 -36
  256. package/dist/components/BbTabs/types.d.ts +0 -8
  257. package/dist/components/BbTag/BbTag.vue.d.ts +1 -1
  258. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +18 -18
  259. package/dist/components/BbTextInput/BbTextInput.vue.d.ts +1 -1
  260. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +18 -18
  261. package/dist/components/BbTextarea/BbTextarea.vue.d.ts +1 -1
  262. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +54 -54
  263. package/dist/components/BbTimePicker/BbTimePicker.vue.d.ts +1 -1
  264. package/dist/components/BbTimePicker/BbTimePicker.vue_vue_type_script_setup_true_lang.js +80 -80
  265. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +61 -61
  266. package/dist/components/BbToast/BbToast.vue_vue_type_script_setup_true_lang.js +17 -17
  267. package/dist/components/BbToast/BbToastMessage.vue_vue_type_script_setup_true_lang.js +13 -13
  268. package/dist/components/BbTooltip/types.d.ts +5 -1
  269. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +17 -17
  270. package/dist/components/ClearableButton/ClearableButton.vue_vue_type_script_setup_true_lang.js +5 -5
  271. package/dist/components/CommonPopover/CommonPopover.vue_vue_type_script_setup_true_lang.js +2 -1
  272. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +83 -83
  273. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +56 -56
  274. package/dist/composables/useBaseOptions.js +30 -20
  275. package/dist/composables/useBbConfig.d.ts +27 -6
  276. package/dist/composables/useBbConfig.js +8 -2
  277. package/dist/composables/useConfig.d.ts +59 -0
  278. package/dist/composables/useConfig.js +9 -0
  279. package/dist/composables/useItemsGetter.d.ts +11 -0
  280. package/dist/composables/useItemsGetter.js +44 -18
  281. package/dist/composables/useLocale.js +10 -7
  282. package/dist/composables/useMobile.js +5 -5
  283. package/dist/composables/usePrefill.d.ts +21 -2
  284. package/dist/composables/usePrefill.js +25 -19
  285. package/dist/composables/useSsrState.d.ts +20 -0
  286. package/dist/composables/useSsrState.js +13 -0
  287. package/dist/composables/useTableWidthContext.d.ts +24 -4
  288. package/dist/composables/useTableWidthContext.js +31 -28
  289. package/dist/composables/useToast.js +5 -5
  290. package/dist/deprecation/ai-deprecations.json.d.ts +121 -11
  291. package/dist/deprecation/ai-deprecations.json.js +1 -1
  292. package/dist/directives/bbDropdown.d.ts +15 -4
  293. package/dist/directives/bbDropdown.js +31 -9
  294. package/dist/directives/createPopoverDirective.d.ts +62 -0
  295. package/dist/directives/createPopoverDirective.js +63 -23
  296. package/dist/i18n/locales/cs.json +4 -1
  297. package/dist/i18n/locales/da.json +4 -1
  298. package/dist/i18n/locales/de.json +4 -1
  299. package/dist/i18n/locales/el.json +4 -1
  300. package/dist/i18n/locales/en.json +4 -1
  301. package/dist/i18n/locales/es.json +4 -1
  302. package/dist/i18n/locales/fi.json +4 -1
  303. package/dist/i18n/locales/fr.json +4 -1
  304. package/dist/i18n/locales/hu.json +4 -1
  305. package/dist/i18n/locales/it.json +4 -1
  306. package/dist/i18n/locales/ja.json +4 -1
  307. package/dist/i18n/locales/ko.json +4 -1
  308. package/dist/i18n/locales/nb.json +4 -1
  309. package/dist/i18n/locales/nl.json +4 -1
  310. package/dist/i18n/locales/pl.json +4 -1
  311. package/dist/i18n/locales/pt.json +4 -1
  312. package/dist/i18n/locales/ro.json +4 -1
  313. package/dist/i18n/locales/ru.json +4 -1
  314. package/dist/i18n/locales/sv.json +4 -1
  315. package/dist/i18n/locales/tr.json +4 -1
  316. package/dist/i18n/locales/uk.json +4 -1
  317. package/dist/i18n/locales/zh-cn.json +4 -1
  318. package/dist/index.d.ts +7 -2
  319. package/dist/index.js +69 -67
  320. package/dist/llms-full.txt +3997 -1458
  321. package/dist/llms-medium.txt +140 -73
  322. package/dist/llms.txt +7 -6
  323. package/dist/locale-blueprint.json +4 -1
  324. package/dist/nuxt-auto-imports.d.ts +100 -0
  325. package/dist/nuxt-auto-imports.js +55 -0
  326. package/dist/nuxt-module.d.ts +22 -1
  327. package/dist/nuxt.js +22 -8
  328. package/dist/plugin.d.ts +10 -1
  329. package/dist/plugin.js +20 -19
  330. package/dist/runtime/nuxt-plugin.js +28 -21
  331. package/dist/styles.css +1 -1
  332. package/dist/utilities/misc/listboxMetrics.d.ts +3 -3
  333. package/dist/utilities/misc/listboxMetrics.js +2 -2
  334. package/dist/utilities/misc/ssrStateInjectionKey.d.ts +42 -0
  335. package/dist/utilities/misc/ssrStateInjectionKey.js +4 -0
  336. package/dist/validated/BbForm.vue_vue_type_script_setup_true_lang.js +45 -27
  337. package/dist/validated/BbSelect.vue.d.ts +2 -0
  338. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  339. package/dist/validated/useValidatedField.js +25 -23
  340. package/dist/vite.js +7 -3
  341. package/llms.txt +8 -7
  342. package/package.json +2 -2
  343. package/scripts/lib/eslint-plugin.d.ts +19 -1
  344. package/scripts/lib/eslint-plugin.mjs +155 -14
  345. package/scripts/lib/html-attributes.mjs +91 -0
  346. package/scripts/lib/validate-bb-markup.mjs +59 -11
  347. package/dist/assets/svgs/sort.svg_raw.js +0 -4
  348. package/dist/components/BbBadge/BadgeAppendIcon.vue.d.ts +0 -3
  349. package/dist/components/BbBadge/BadgeAppendIcon.vue.js +0 -5
  350. package/dist/components/BbBadge/BadgeAppendIcon.vue_vue_type_script_setup_true_lang.js +0 -22
@@ -8,8 +8,13 @@
8
8
  <template>
9
9
  <dialog
10
10
  ref="dialog"
11
+ :aria-describedby="
12
+ hasRenderedAtLeastOnce && hasDescription() ? descriptionId : undefined
13
+ "
11
14
  :aria-labelledby="
12
- hasRenderedAtLeastOnce && renderHeader() ? titleId : undefined
15
+ hasRenderedAtLeastOnce && (hasTitle() || $slots.header)
16
+ ? titleId
17
+ : undefined
13
18
  "
14
19
  class="bb-offcanvas"
15
20
  :class="
@@ -20,6 +25,7 @@
20
25
  'bb-offcanvas--fullscreen': isFullscreen,
21
26
  'bb-offcanvas--deny-close': denyClose,
22
27
  'bb-offcanvas--dragging': isDragging,
28
+ 'bb-offcanvas--swipe-exit': swipeExiting,
23
29
  'bb-offcanvas--instant-stack': instantStack,
24
30
  'bb-offcanvas--covered': isCovered,
25
31
  'bb-offcanvas--has-handle': draggable,
@@ -39,7 +45,7 @@
39
45
  @cancel="cancelableClose"
40
46
  @click.self="onBackdropClick"
41
47
  @pointercancel="resetBackdropPointerState"
42
- @pointerdown="onPanelPointerDown"
48
+ @pointerdown="onBackdropPointerDown"
43
49
  >
44
50
  <template v-if="hasRenderedAtLeastOnce">
45
51
  <div
@@ -49,9 +55,18 @@
49
55
  ></div>
50
56
  <div v-if="renderHeader()" class="bb-offcanvas__header">
51
57
  <slot :close="close" name="header" :title="title" :title-id="titleId">
52
- <span :id="titleId" class="bb-offcanvas__title">
58
+ <span v-if="hasTitle()" :id="titleId" class="bb-offcanvas__title">
53
59
  <slot name="title" :text="title">{{ title }}</slot>
54
60
  </span>
61
+ <p
62
+ v-if="hasDescription()"
63
+ :id="descriptionId"
64
+ class="bb-offcanvas__description"
65
+ >
66
+ <slot name="description" :text="description">{{
67
+ description
68
+ }}</slot>
69
+ </p>
55
70
  <CloseButton
56
71
  v-if="!hideClose"
57
72
  :aria-label="t('offCanvas.closeLabel').value"
@@ -62,6 +77,34 @@
62
77
  <div ref="body" class="bb-offcanvas__body scrollbar-border">
63
78
  <div class="bb-offcanvas__body-content">
64
79
  <slot :close="close"></slot>
80
+
81
+ <!--
82
+ Q18.12 — the headerless close.
83
+ Without it a panel with no `title` and no `#header` had NO close
84
+ control at all: for an adaptive BbDialog that meant a floating ×
85
+ on desktop and none on the phone, from one template. The
86
+ positioning CSS for this button already shipped
87
+ (`index.css` `.bb-offcanvas__body-content > .bb-close-button`) —
88
+ it was dead, waiting for the element.
89
+
90
+ The guard is the dialog's, so delegating the close stays
91
+ possible the same two ways: `hide-close` drops it entirely, and
92
+ a `#header` slot means you are rendering your own control.
93
+
94
+ AFTER the slot, unlike BbDialog, and that is load-bearing.
95
+ `showModal()` focuses the first focusable element in the dialog,
96
+ so rendering the × first made it steal the opening focus — it
97
+ broke BbColorPalette, whose contract is that opening the sheet
98
+ focuses the palette. The button is `position: absolute`, so DOM
99
+ order does not move it visually; it only decides who the browser
100
+ focuses and where the × sits in the tab order (last, after the
101
+ content it closes).
102
+ -->
103
+ <CloseButton
104
+ v-if="!renderHeader() && !hideClose"
105
+ :aria-label="t('offCanvas.closeLabel').value"
106
+ @click="onCloseButtonClick"
107
+ />
65
108
  </div>
66
109
  </div>
67
110
 
@@ -82,6 +125,7 @@ import {
82
125
  watch,
83
126
  type Ref,
84
127
  } from 'vue';
128
+ import { useSwipe } from '@vueuse/core';
85
129
  import { parseSize } from '@/utilities/functions/parseSize';
86
130
  import {
87
131
  pushEscapeEntry,
@@ -90,6 +134,7 @@ import {
90
134
  } from '@/utils/overlayEscapeStack';
91
135
  import { useId } from '@/composables/useId';
92
136
  import { useLocale } from '@/composables/useLocale';
137
+ import { useLogger } from '@/composables/useLogger';
93
138
  import { useMobile } from '@/composables/useMobile';
94
139
  import { viewportWidth, viewportHeight } from '@/composables/useViewportWidth';
95
140
  import { useBreakpoint } from '@/composables/useBreakpoint';
@@ -99,7 +144,7 @@ import { warnRemovedAttrs } from '@/utilities/functions/warnRemovedAttrs';
99
144
  import { wait } from '@/utilities/functions/wait';
100
145
  import { transitions } from '@/utilities/misc/transitions';
101
146
  import { overlaySurfaceIsolation } from '@/utils/overlaySurfaceIsolation';
102
- import { config } from 'virtual:bitboss-ui/config';
147
+ import { useConfig } from '@/composables/useConfig';
103
148
  import CloseButton from '../CloseIcon/CloseButton.vue';
104
149
  import { useOffCanvasStack } from './useOffCanvasStack';
105
150
  import { registerTopLayerHost } from '@/composables/useTopLayer';
@@ -109,7 +154,6 @@ import type {
109
154
  BbOffCanvasSlots,
110
155
  Sizes,
111
156
  } from './types';
112
- export type { BbOffCanvasProps, BbOffCanvasEvents, BbOffCanvasSlots, Sizes };
113
157
 
114
158
  /**
115
159
  * Used by v-model to open / close the panel. A panel with no `v-model`
@@ -118,7 +162,6 @@ export type { BbOffCanvasProps, BbOffCanvasEvents, BbOffCanvasSlots, Sizes };
118
162
  * @defaultValue `false`
119
163
  */
120
164
  const model = defineModel<boolean>({ default: false });
121
-
122
165
  const props = withDefaults(defineProps<BbOffCanvasProps>(), {
123
166
  side: 'left',
124
167
  size: 'sm',
@@ -126,10 +169,10 @@ const props = withDefaults(defineProps<BbOffCanvasProps>(), {
126
169
  draggable: false,
127
170
  stackGap: 60,
128
171
  });
129
-
130
172
  const emit = defineEmits<BbOffCanvasEvents>();
131
-
132
173
  const slots = defineSlots<BbOffCanvasSlots>();
174
+ const config = useConfig();
175
+ export type { BbOffCanvasProps, BbOffCanvasEvents, BbOffCanvasSlots, Sizes };
133
176
 
134
177
  if (process.env.NODE_ENV !== 'production')
135
178
  warnRemovedAttrs('BbOffCanvas', removedPropsFor('BbOffCanvas'));
@@ -139,7 +182,15 @@ const { t } = useLocale();
139
182
  // computed would latch its first value and miss a slot that appears later (e.g.
140
183
  // BbDropdown's adaptive sheet only provides `#header` once you drill into a
141
184
  // submenu). Evaluated per render, it tracks the slot's presence correctly.
142
- const renderHeader = () => !!props.title || !!slots.header;
185
+ // `#title` counts as a title, exactly like the prop — the twin of BbDialog's
186
+ // `renderHeader`. Without it a panel that only slotted its title rendered no
187
+ // header, so the declared slot was silently inert (and an adaptive BbDialog
188
+ // forwards `#title` straight through to here).
189
+ const hasTitle = () => !!props.title || !!slots.title;
190
+ // A description raises the header on its own — a describing line with no title
191
+ // is a legitimate shape — but it is NOT a name, so `aria-labelledby` keys off
192
+ // `hasTitle()` while the header keys off both.
193
+ const renderHeader = () => hasTitle() || !!slots.header || hasDescription();
143
194
  const shouldPreventClose = computed(
144
195
  () => !!(props.disabled || props.persistent)
145
196
  );
@@ -154,6 +205,11 @@ const hasRenderedAtLeastOnce: Ref<boolean> = useUntil(
154
205
  );
155
206
 
156
207
  const titleId = `title_${useId().id.value}`;
208
+ // Twin of BbDialog's: `aria-describedby` points at the dedicated description
209
+ // and nothing else, which is what makes the wiring safe here (see the note on
210
+ // BbDialog's `descriptionId`).
211
+ const descriptionId = `desc_${useId().id.value}`;
212
+ const hasDescription = () => !!props.description || !!slots.description;
157
213
  const openedTimes = ref(0);
158
214
  const isVisible = ref(false);
159
215
 
@@ -291,19 +347,41 @@ const toggleDialog = async (value: boolean) => {
291
347
  releaseTopLayer?.();
292
348
  releaseTopLayer = null;
293
349
  isVisible.value = false;
350
+ swipeDismissPending = false;
351
+ swipeExiting.value = false;
294
352
  clearDragVisual();
295
353
  emit('hidden');
296
354
  }
297
355
  };
298
356
 
357
+ /*
358
+ * Q18.13 — see the twin in BbDialog. `aria-labelledby` points at `titleId` as
359
+ * soon as a `#header` slot exists, but the heading inside it is the author's,
360
+ * so a forgotten id leaves the panel announcing as unnamed with no signal.
361
+ * Warn once, in dev, after the header is in the DOM.
362
+ */
363
+ const warnIfHeaderNameIsDangling = () => {
364
+ if (process.env.NODE_ENV === 'production') return;
365
+ if (!slots.header || props.title) return;
366
+ nextTick(() => {
367
+ if (!dialog.value || document.getElementById(titleId)) return;
368
+ useLogger().warn(
369
+ `BbOffCanvas: the #header slot does not contain an element with id="${titleId}", so aria-labelledby points at nothing and the panel announces as unnamed. Bind the slot's \`titleId\` to your heading: <template #header="{ titleId }"><h2 :id="titleId">…</h2></template>.`
370
+ );
371
+ });
372
+ };
373
+
299
374
  watch(model, toggleDialog);
300
375
  onMounted(() => {
301
376
  if (model.value) toggleDialog(true);
377
+ warnIfHeaderNameIsDangling();
302
378
  });
303
379
  onBeforeUnmount(() => {
304
380
  clearTimeout(denyCloseTimer);
305
381
  removeEscapeListener();
306
- teardownGestureListeners();
382
+ body.value?.removeEventListener('scroll', onBodyScroll);
383
+ bodyResizeObserver?.disconnect();
384
+ bodyResizeObserver = null;
307
385
  leaveStack();
308
386
  releaseTopLayer?.();
309
387
  releaseTopLayer = null;
@@ -526,50 +604,108 @@ watch(
526
604
  );
527
605
 
528
606
  // ---------------------------------------------------------------------------
529
- // Pointer drag — drag-to-dismiss with velocity, rubber-band and scroll handoff.
607
+ // Swipe-to-dismiss VueUse `useSwipe` (fixed px threshold, not panel height).
530
608
  // ---------------------------------------------------------------------------
531
609
  const body = ref<HTMLElement | null>(null);
532
610
  const isDragging = ref(false);
611
+ /** Fades `::backdrop` in parallel with the swipe-exit `transform` (Vaul overlay). */
612
+ const swipeExiting = ref(false);
533
613
 
534
- /** Pointer travel before a gesture is classified as a drag (vs. a tap/scroll). */
535
- const DRAG_SLOP = 6;
536
- /** Fraction of the panel that must be dragged away to dismiss on release. */
537
- const DISMISS_DISTANCE_RATIO = 0.25;
538
- /** Release velocity (px/ms, toward close) that dismisses regardless of distance. */
539
- const DISMISS_VELOCITY = 0.5;
614
+ /**
615
+ * Absolute travel toward the exit edge before the sheet dismisses.
616
+ * Height-ratio thresholds made tall filter sheets feel sticky and short ones
617
+ * nearly impossible; VueUse's default 50px reads the same on both.
618
+ */
619
+ const SWIPE_THRESHOLD = 50;
620
+ /**
621
+ * Start following the finger well below the dismiss threshold. `useSwipe`'s
622
+ * threshold also gates `onSwipe`, so a 50px track delay left the first 50px to
623
+ * native pan — on a row `<button>` that pan never reached the sheet body.
624
+ */
625
+ const SWIPE_TRACK_PX = 8;
626
+ /**
627
+ * Vaul sheet motion — 500ms / `cubic-bezier(0.32, 0.72, 0, 1)` (iOS sheet curve).
628
+ * Used for spring-back AND swipe-dismiss; both stay on the drag `transform`
629
+ * layer (never a CSS keyframe swap — that samples identity and flashes).
630
+ * @see https://emilkowal.ski/ui/building-a-drawer-component
631
+ */
632
+ const SWIPE_EXIT_MS = 500;
633
+ const SWIPE_EXIT_EASE = 'cubic-bezier(0.32, 0.72, 0, 1)';
634
+
635
+ /** True while a swipe-dismiss transform is finishing — blocks clear mid-exit. */
636
+ let swipeDismissPending = false;
637
+
638
+ let bodyResizeObserver: ResizeObserver | null = null;
639
+ /** Candidate gesture (touch on the panel, not the backdrop). */
640
+ let swipeCandidate = false;
641
+ /** Started inside the scrollable body — may need to hand off to scroll. */
642
+ let swipeStartedInBody = false;
643
+ /** Sheet is following the finger (dismiss-axis past threshold). */
644
+ let swipeClaimed = false;
645
+ /**
646
+ * Vaul `shouldDrag`: decided at touchstart, not per-move. Live `scrollTop`
647
+ * during the first 50px lets native pan and dismiss fight (tall auto sheets
648
+ * only overflow by a few px, so the edge flickers). Once this gesture is
649
+ * scrolling, it stays scrolling.
650
+ */
651
+ let swipeLockedToScroll = false;
540
652
 
541
- type DragGesture = {
542
- pointerId: number;
543
- startX: number;
544
- startY: number;
545
- size: number;
546
- startedInBody: boolean;
547
- decided: boolean;
548
- active: boolean;
549
- samples: Array<{ t: number; pos: number }>;
653
+ /**
654
+ * Allow vertical panning on the body so a tall sheet can scroll. Dismiss vs
655
+ * scroll is latched in `useSwipe` (`scrollLocksThisGesture`); directional
656
+ * `pan-up` / `pan-down` at the edge blocked scrolling filter sheets on mobile.
657
+ * `pan-y` lives in CSS; this only clears the inline `none` written during drag.
658
+ */
659
+ const syncBodyDismissTouchAction = () => {
660
+ body.value?.style.removeProperty('touch-action');
550
661
  };
551
- let gesture: DragGesture | null = null;
552
662
 
553
- const axisDelta = (event: PointerEvent, g: DragGesture) =>
554
- dragAxis.value === 'x' ? event.clientX - g.startX : event.clientY - g.startY;
663
+ const onBodyScroll = () => syncBodyDismissTouchAction();
664
+
665
+ watch(
666
+ body,
667
+ (el, prev) => {
668
+ prev?.removeEventListener('scroll', onBodyScroll);
669
+ bodyResizeObserver?.disconnect();
670
+ bodyResizeObserver = null;
671
+ if (!el) return;
672
+ el.addEventListener('scroll', onBodyScroll, { passive: true });
673
+ if (typeof ResizeObserver !== 'undefined') {
674
+ bodyResizeObserver = new ResizeObserver(() =>
675
+ syncBodyDismissTouchAction()
676
+ );
677
+ bodyResizeObserver.observe(el);
678
+ }
679
+ syncBodyDismissTouchAction();
680
+ },
681
+ { flush: 'post' }
682
+ );
555
683
 
556
- const axisPos = (event: PointerEvent) =>
557
- dragAxis.value === 'x' ? event.clientX : event.clientY;
684
+ watch([() => props.draggable, () => props.side, isVisible] as const, () =>
685
+ syncBodyDismissTouchAction()
686
+ );
558
687
 
559
688
  /**
560
- * The drag is driven imperatively (direct style writes, no per-frame Vue render)
561
- * to stay smooth, and the transition is toggled inline so restoring it and the
562
- * resting offset happen in the same synchronous tick — no race with Vue's flush.
689
+ * Travel toward the exit edge in px. `useSwipe`'s lengthX/Y are
690
+ * `start end`, so a finger moving down yields a negative lengthY.
563
691
  */
692
+ const closeAmountFromSwipe = () => {
693
+ if (dragAxis.value === 'y') {
694
+ return closeSign.value === 1 ? -lengthY.value : lengthY.value;
695
+ }
696
+ return closeSign.value === 1 ? -lengthX.value : lengthX.value;
697
+ };
698
+
564
699
  const beginDragVisual = () => {
565
700
  if (dialog.value) dialog.value.style.transition = 'none';
701
+ if (body.value && dragAxis.value === 'y') {
702
+ body.value.style.touchAction = 'none';
703
+ }
566
704
  };
567
705
 
568
706
  const applyDragVisual = (closeAmount: number) => {
569
707
  const el = dialog.value;
570
708
  if (!el) return;
571
- // Follow the pointer toward the exit edge with a direct compositor translate
572
- // (no CSS var, no calc). Over-opening does nothing — no elastic stretch.
573
709
  const px = closeAmount > 0 ? closeAmount * closeSign.value : 0;
574
710
  el.style.transform =
575
711
  dragAxis.value === 'x'
@@ -577,159 +713,185 @@ const applyDragVisual = (closeAmount: number) => {
577
713
  : `translate3d(0, ${px}px, 0)`;
578
714
  };
579
715
 
580
- /** Restore the transition; on spring-back also animate the transform to rest. */
716
+ /** Fully off-screen on the drag layer same units as Vaul's `slideTo*` `to`. */
717
+ const applyDismissTransform = () => {
718
+ const el = dialog.value;
719
+ if (!el) return;
720
+ el.style.transform =
721
+ props.side === 'bottom'
722
+ ? 'translate3d(0, 100%, 0)'
723
+ : props.side === 'top'
724
+ ? 'translate3d(0, -100%, 0)'
725
+ : props.side === 'left'
726
+ ? 'translate3d(-100%, 0, 0)'
727
+ : 'translate3d(100%, 0, 0)';
728
+ };
729
+
730
+ /**
731
+ * Vaul `resetDrawer`: spring to rest on the same inline `transform` that drag
732
+ * wrote — never clear to identity then re-enable transition (one-frame flash).
733
+ */
581
734
  const endDragVisual = (springBack: boolean) => {
582
735
  const el = dialog.value;
583
736
  if (!el) return;
584
- el.style.transition = ''; // restore the CSS transition synchronously
585
- // Spring back animates the transform to rest. On dismiss we keep it so the
586
- // exit `translate` continues seamlessly from where the finger let go.
587
- if (springBack) el.style.transform = '';
737
+ if (springBack) {
738
+ el.style.transition = `transform ${SWIPE_EXIT_MS}ms ${SWIPE_EXIT_EASE}`;
739
+ el.style.transform = 'translate3d(0, 0, 0)';
740
+ window.setTimeout(() => {
741
+ if (!el.isConnected || swipeDismissPending) return;
742
+ el.style.removeProperty('transform');
743
+ el.style.removeProperty('transition');
744
+ syncBodyDismissTouchAction();
745
+ }, SWIPE_EXIT_MS);
746
+ return;
747
+ }
748
+ el.style.transition = '';
749
+ syncBodyDismissTouchAction();
588
750
  };
589
751
 
590
- /** Hard reset with no animation, once the panel is fully hidden. */
591
752
  const clearDragVisual = () => {
592
753
  const el = dialog.value;
593
754
  if (!el) return;
755
+ // Leave the frozen off-screen `transform` until `toggleDialog` has waited
756
+ // out the backdrop fade and is about to `dialog.close()`. Clearing earlier
757
+ // restores the stylesheet `transform` transition while `--open` can still
758
+ // be at rest — that is the on-screen flash.
759
+ if (swipeDismissPending) return;
594
760
  el.style.removeProperty('transition');
595
761
  el.style.removeProperty('transform');
596
762
  };
597
763
 
598
- /** Whether the scrollable body should absorb the gesture as a scroll instead. */
599
- const bodyAbsorbsScroll = (closeAmount: number) => {
600
- if (dragAxis.value !== 'y' || !body.value) return false;
601
- if (closeAmount <= 0) return true; // over-opening: let content scroll
602
- return closeSign.value === 1
603
- ? body.value.scrollTop > 0 // bottom drawer: scroll up before dismissing
604
- : body.value.scrollTop + body.value.clientHeight <
605
- body.value.scrollHeight - 1; // top drawer: scroll down first
606
- };
607
-
608
- const teardownGestureListeners = () => {
609
- window.removeEventListener('pointermove', onPanelPointerMove);
610
- window.removeEventListener('pointerup', onPanelPointerUp);
611
- window.removeEventListener('pointercancel', onPanelPointerCancel);
764
+ /**
765
+ * Vaul dismiss: drawer and overlay move together on one clock, both on their
766
+ * own properties (drawer = inline `transform`, overlay = `::backdrop` opacity).
767
+ * `close()` is called only after the drawer is off-screen; `toggleDialog` then
768
+ * waits `transitionDuration` so the already-fading backdrop can finish before
769
+ * `dialog.close()` tears it down. Styles stay frozen until that wait ends.
770
+ */
771
+ const finishDismissFromDrag = async () => {
772
+ const el = dialog.value;
773
+ if (!el) {
774
+ close();
775
+ return;
776
+ }
777
+ swipeDismissPending = true;
778
+ swipeExiting.value = true;
779
+ el.style.transition = `transform ${SWIPE_EXIT_MS}ms ${SWIPE_EXIT_EASE}`;
780
+ applyDismissTransform();
781
+ await wait(SWIPE_EXIT_MS);
782
+ if (!model.value) {
783
+ swipeDismissPending = false;
784
+ swipeExiting.value = false;
785
+ el.style.removeProperty('transform');
786
+ el.style.removeProperty('transition');
787
+ syncBodyDismissTouchAction();
788
+ return;
789
+ }
790
+ // Freeze the off-screen pose. Keep `transition: none` so `close()` cannot
791
+ // run a CSS `translate` close on the same element. Backdrop keeps fading
792
+ // via `--swipe-exit` / `--open` removal — those live on `::backdrop`.
793
+ el.style.transition = 'none';
794
+ applyDismissTransform();
795
+ close();
612
796
  };
613
797
 
614
- const abandonGesture = () => {
615
- teardownGestureListeners();
616
- gesture = null;
798
+ /** Whether the scrollable body should absorb the gesture as a scroll instead. */
799
+ const isVerticallyScrollable = (el: HTMLElement) => {
800
+ const { overflowY } = getComputedStyle(el);
801
+ if (overflowY !== 'auto' && overflowY !== 'scroll') return false;
802
+ return el.scrollHeight > el.clientHeight + 1;
617
803
  };
618
804
 
619
- const onPanelPointerDown = (event: PointerEvent) => {
620
- // Preserve backdrop-intent tracking used by the outside-click close.
621
- onBackdropPointerDown(event);
622
- // Drag-to-dismiss is opt-in via `draggable`.
623
- if (!props.draggable) return;
624
- // The dialog element itself is the backdrop — handled by click, not drag.
625
- if (event.target === event.currentTarget) return;
626
- if (event.button !== 0 || !dialog.value) return;
627
- if (gesture) abandonGesture();
628
-
629
- const rect = dialog.value.getBoundingClientRect();
630
- gesture = {
631
- pointerId: event.pointerId,
632
- startX: event.clientX,
633
- startY: event.clientY,
634
- size: dragAxis.value === 'x' ? rect.width : rect.height,
635
- startedInBody: !!(event.target as HTMLElement).closest?.(
636
- '.bb-offcanvas__body'
637
- ),
638
- decided: false,
639
- active: false,
640
- samples: [{ t: performance.now(), pos: axisPos(event) }],
641
- };
642
- window.addEventListener('pointermove', onPanelPointerMove, {
643
- passive: false,
644
- });
645
- window.addEventListener('pointerup', onPanelPointerUp);
646
- window.addEventListener('pointercancel', onPanelPointerCancel);
647
- };
805
+ const isAtDismissEdge = (el: HTMLElement) =>
806
+ closeSign.value === 1
807
+ ? el.scrollTop <= 1
808
+ : el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
648
809
 
649
- const onPanelPointerMove = (event: PointerEvent) => {
650
- if (!gesture || event.pointerId !== gesture.pointerId) return;
651
- const along = axisDelta(event, gesture);
652
- const cross =
653
- dragAxis.value === 'x'
654
- ? event.clientY - gesture.startY
655
- : event.clientX - gesture.startX;
656
-
657
- if (!gesture.decided) {
658
- if (Math.abs(along) < DRAG_SLOP && Math.abs(cross) < DRAG_SLOP) return;
659
- gesture.decided = true;
660
- const closeAmount = along * closeSign.value;
661
- const dominantAlongAxis = Math.abs(along) >= Math.abs(cross);
662
- // Cross-axis swipe, or one the body can scroll: hand off to native scroll.
663
- if (
664
- !dominantAlongAxis ||
665
- (gesture.startedInBody && bodyAbsorbsScroll(closeAmount))
666
- ) {
667
- abandonGesture();
668
- return;
810
+ /**
811
+ * True when a scrollable ancestor under the finger is not at its dismiss edge.
812
+ * Latched at start so a 1px overflow on a `fit-content` (then capped) sheet
813
+ * cannot flip mid-gesture as native pan moves `scrollTop`.
814
+ */
815
+ const scrollLocksThisGesture = (target: Element) => {
816
+ const root = dialog.value;
817
+ if (!root) return false;
818
+ let el: Element | null = target;
819
+ while (el && el !== root) {
820
+ if (el instanceof HTMLElement && isVerticallyScrollable(el)) {
821
+ if (!isAtDismissEdge(el)) return true;
669
822
  }
670
- gesture.active = true;
671
- isDragging.value = true;
672
- beginDragVisual();
823
+ el = el.parentElement;
673
824
  }
674
- if (!gesture.active) return;
675
-
676
- event.preventDefault();
677
- gesture.samples.push({ t: performance.now(), pos: axisPos(event) });
678
- if (gesture.samples.length > 6) gesture.samples.shift();
679
-
680
- // Write synchronously (no rAF hop): the panel stays glued to the pointer with
681
- // no added latency. Multiple moves per frame just overwrite the inline
682
- // `transform`; the browser still paints once, at the frame boundary.
683
- applyDragVisual(along * closeSign.value);
825
+ return false;
684
826
  };
685
827
 
686
- /** Close-direction velocity (px/ms) from the most recent ~50ms of samples. */
687
- const releaseVelocity = (samples: DragGesture['samples']) => {
688
- if (samples.length < 2) return 0;
689
- const last = samples[samples.length - 1];
690
- let start = samples[0];
691
- for (let i = samples.length - 2; i >= 0; i--) {
692
- start = samples[i];
693
- if (last.t - samples[i].t >= 50) break;
694
- }
695
- const dt = last.t - start.t;
696
- return dt > 0 ? ((last.pos - start.pos) / dt) * closeSign.value : 0;
828
+ const resetSwipeState = () => {
829
+ swipeCandidate = false;
830
+ swipeStartedInBody = false;
831
+ swipeClaimed = false;
832
+ swipeLockedToScroll = false;
833
+ isDragging.value = false;
697
834
  };
698
835
 
699
- const onPanelPointerUp = (event: PointerEvent) => {
700
- if (!gesture || event.pointerId !== gesture.pointerId) return;
701
- const { active, size, samples } = gesture;
702
- const closeAmount = axisDelta(event, gesture) * closeSign.value;
703
- teardownGestureListeners();
704
- gesture = null;
705
- if (!active) return;
706
-
707
- isDragging.value = false; // restore the transition for settle / exit
708
- const dismiss =
709
- closeAmount > size * DISMISS_DISTANCE_RATIO ||
710
- (releaseVelocity(samples) > DISMISS_VELOCITY && closeAmount > DRAG_SLOP);
711
-
712
- if (dismiss && !shouldPreventClose.value) {
713
- // Restore the transition but keep the offset so the exit continues from
714
- // where the finger let go; it is reset once the panel is fully hidden.
715
- endDragVisual(false);
716
- close();
717
- return;
718
- }
719
- endDragVisual(true); // spring back to fully open
720
- if (dismiss) triggerDenyCloseFeedback(300);
721
- };
836
+ const { lengthX, lengthY } = useSwipe(dialog, {
837
+ threshold: SWIPE_TRACK_PX,
838
+ // Non-passive so we can preventDefault once the sheet is following the
839
+ // finger otherwise Chrome cancels the gesture for native pan.
840
+ passive: false,
841
+ onSwipeStart: (event) => {
842
+ resetSwipeState();
843
+ if (!props.draggable || !isVisible.value) return;
844
+ // The dialog element itself is the backdrop click closes, not swipe.
845
+ if (event.target === dialog.value) return;
846
+ const target = event.target;
847
+ if (!(target instanceof Element)) return;
848
+ swipeCandidate = true;
849
+ swipeStartedInBody = !!target.closest('.bb-offcanvas__body');
850
+ swipeLockedToScroll = swipeStartedInBody && scrollLocksThisGesture(target);
851
+ },
852
+ onSwipe: (event) => {
853
+ if (!swipeCandidate || !props.draggable || swipeLockedToScroll) return;
854
+ const closeAmount = closeAmountFromSwipe();
855
+
856
+ if (!swipeClaimed) {
857
+ // Finger moving into content: this gesture stays a scroll even if they
858
+ // later reverse toward the dismiss edge.
859
+ if (swipeStartedInBody && closeAmount < -SWIPE_TRACK_PX) {
860
+ swipeLockedToScroll = true;
861
+ return;
862
+ }
863
+ // Require the dismiss-axis itself to clear the track slop. A mostly
864
+ // horizontal flick on a short sheet must not latch the panel just
865
+ // because |dx| exceeded the track threshold.
866
+ if (closeAmount < SWIPE_TRACK_PX) return;
867
+ swipeClaimed = true;
868
+ isDragging.value = true;
869
+ beginDragVisual();
870
+ }
722
871
 
723
- const onPanelPointerCancel = (event: PointerEvent) => {
724
- if (!gesture || event.pointerId !== gesture.pointerId) return;
725
- const wasActive = gesture.active;
726
- teardownGestureListeners();
727
- gesture = null;
728
- if (wasActive) {
729
- isDragging.value = false;
872
+ // Once the sheet is following the finger, this gesture stays a drag
873
+ // until lift even after reversing to rest. `touch-action` is sampled
874
+ // at touchstart (`pan-y`), so dropping preventDefault here would let
875
+ // native scroll start mid-drag (sheet at 0, list jumping).
876
+ event.preventDefault();
877
+ applyDragVisual(closeAmount);
878
+ },
879
+ onSwipeEnd: () => {
880
+ if (!swipeClaimed) {
881
+ resetSwipeState();
882
+ return;
883
+ }
884
+ const closeAmount = closeAmountFromSwipe();
885
+ resetSwipeState();
886
+ // Fixed px threshold — same on a tall filters sheet and a short menu.
887
+ if (closeAmount >= SWIPE_THRESHOLD && !shouldPreventClose.value) {
888
+ void finishDismissFromDrag();
889
+ return;
890
+ }
730
891
  endDragVisual(true);
731
- }
732
- };
892
+ if (closeAmount >= SWIPE_THRESHOLD) triggerDenyCloseFeedback(300);
893
+ },
894
+ });
733
895
  </script>
734
896
 
735
897
  <style lang="postcss">
@@ -747,6 +909,18 @@ export type { Sizes };
747
909
  type OffCanvasSize = NonNullable<Size<Sizes>['size']> | 'auto';
748
910
 
749
911
  export type BbOffCanvasProps = {
912
+ /**
913
+ * One short line under the title, in the header — the shadcn
914
+ * `DialogDescription` shape: same block as the title, lower hierarchy,
915
+ * muted.
916
+ *
917
+ * It is also what `aria-describedby` points at. That wiring was dropped in
918
+ * v3 along with the old `description` prop, on the reasoning that pointing
919
+ * it at arbitrary body content would read whole forms aloud — true, and the
920
+ * reason it aims at THIS element only. Keep it to a sentence; anything
921
+ * longer belongs in the body.
922
+ */
923
+ description?: string;
750
924
  /**
751
925
  * Disable EVERY user-initiated dismissal: backdrop click, Escape,
752
926
  * drag-to-dismiss AND the header close (×) button (denied attempts play the
@@ -769,10 +943,10 @@ export type BbOffCanvasProps = {
769
943
  */
770
944
  fullscreen?: boolean | 'mobile';
771
945
  /**
772
- * Enable drag-to-dismiss. The panel can be dragged from anywhere toward its
773
- * edge to close, and a grab handle is shown on the inner edge as the
774
- * affordance. When `false`, the panel is static (dismiss via the close button,
775
- * backdrop, or Escape only).
946
+ * Enable drag-to-dismiss via VueUse `useSwipe` (fixed ~50px threshold toward
947
+ * the exit edge, including from the scrollable body once it is at its scroll
948
+ * edge). A grab handle on the inner edge is the visual cue. When `false`, the
949
+ * panel is static (dismiss via the close button, backdrop, or Escape only).
776
950
  * @defaultValue `false`
777
951
  */
778
952
  draggable?: boolean;
@@ -849,18 +1023,32 @@ export type BbOffCanvasHeaderSlotProps = {
849
1023
  };
850
1024
 
851
1025
  /** Props exposed by the `title` slot. */
1026
+ /** Props exposed by the `description` slot. */
1027
+ export type BbOffCanvasDescriptionSlotProps = {
1028
+ /** The `description` prop value. */
1029
+ text: BbOffCanvasProps['description'];
1030
+ };
1031
+
852
1032
  export type BbOffCanvasTitleSlotProps = {
853
1033
  /** The `title` prop value. */
854
1034
  text: BbOffCanvasProps['title'];
855
1035
  };
856
1036
 
857
1037
  export type BbOffCanvasSlots = {
1038
+ /**
1039
+ * Replaces the description text under the title. Renders the header even
1040
+ * when `title` and `description` are unset — a slotted description is a
1041
+ * description, the same rule `#title` follows.
1042
+ */
1043
+ description?: (props: BbOffCanvasDescriptionSlotProps) => any;
858
1044
  /**
859
1045
  * Replaces the entire off-canvas header section (title + close button). Maintains accessibility wiring via the provided props.
860
1046
  */
861
1047
  header?: (props: BbOffCanvasHeaderSlotProps) => any;
862
1048
  /**
863
- * Replaces the default title text inside the header. Remains linked to the panel through `aria-labelledby`.
1049
+ * Replaces the default title text inside the header. Remains linked to the
1050
+ * panel through `aria-labelledby`. Providing this slot renders the header even
1051
+ * when `title` is unset — a slotted title is a title.
864
1052
  */
865
1053
  title?: (props: BbOffCanvasTitleSlotProps) => any;
866
1054
  /**
@@ -892,6 +1080,13 @@ export type BbOffCanvasSlots = {
892
1080
  /* Duration for the stack shift + pad. Zeroed while a peer resizes so parents
893
1081
  track the live size instead of lagging behind a 250ms animation. */
894
1082
  --stack-transition-duration: var(--transition-duration);
1083
+ /* Axis split of --bb-panel-p: --px (inline) / --py
1084
+ (block), each branching off it. Set the pair only when the axes differ;
1085
+ setting --bb-panel-p alone keeps them equal. Local on this root class
1086
+ (not :root) so scoped --bb-panel-p overrides keep reaching the slots;
1087
+ for per-part asymmetry, scope --py to the part class. */
1088
+ --px: var(--bb-panel-p);
1089
+ --py: var(--bb-panel-p);
895
1090
  --transition-timing-function: var(--bb-ease);
896
1091
  background-color: var(--bb-panel);
897
1092
  border: 1px solid var(--bb-border);
@@ -1067,6 +1262,11 @@ export type BbOffCanvasSlots = {
1067
1262
  transform-origin: center bottom;
1068
1263
  }
1069
1264
 
1265
+ &.bb-offcanvas--top .bb-offcanvas__body,
1266
+ &.bb-offcanvas--bottom .bb-offcanvas__body {
1267
+ touch-action: pan-y;
1268
+ }
1269
+
1070
1270
  &.bb-offcanvas--fullscreen {
1071
1271
  border-radius: 0;
1072
1272
  height: 100%;
@@ -1127,7 +1327,20 @@ export type BbOffCanvasSlots = {
1127
1327
  background-color var(--transition-duration) var(--bb-ease);
1128
1328
  }
1129
1329
 
1130
- /* Grab handle: reliable drag surface on touch, centred on the inner edge. */
1330
+ /* Vaul overlay: fade the dim with the swipe-exit, not after it. `--open`
1331
+ stays on for the 500ms so `dialog.close()` does not tear the backdrop
1332
+ down mid-fade; this only overrides opacity. */
1333
+ &.bb-offcanvas--open.bb-offcanvas--swipe-exit::backdrop {
1334
+ opacity: 0;
1335
+ transition:
1336
+ opacity 500ms cubic-bezier(0.32, 0.72, 0, 1),
1337
+ background-color 500ms cubic-bezier(0.32, 0.72, 0, 1);
1338
+ }
1339
+
1340
+ /* Grab handle: reliable drag surface on touch, centred on the inner edge.
1341
+ z-index above the body: a sheet search (flyout leftover `z-index: 1`, or
1342
+ `position: relative`) would otherwise paint over the 4px pill once the
1343
+ headerless body scrolls its padding-top away. */
1131
1344
  .bb-offcanvas__handle {
1132
1345
  background-color: var(--bb-border);
1133
1346
  border-radius: 999px;
@@ -1135,7 +1348,7 @@ export type BbOffCanvasSlots = {
1135
1348
  flex-shrink: 0;
1136
1349
  position: absolute;
1137
1350
  touch-action: none;
1138
- z-index: 1;
1351
+ z-index: 2;
1139
1352
  }
1140
1353
 
1141
1354
  &.bb-offcanvas--bottom .bb-offcanvas__handle,
@@ -1171,24 +1384,27 @@ export type BbOffCanvasSlots = {
1171
1384
  `:first-child` because an absolutely-positioned drag handle can precede the
1172
1385
  body in the DOM, which would defeat `:first-child`. */
1173
1386
  &.bb-offcanvas--no-header .bb-offcanvas__body-content {
1174
- padding-top: var(--bb-panel-p);
1387
+ padding-top: var(--py);
1175
1388
  }
1176
1389
 
1177
1390
  /* Make room for the handle so it does not overlap header content. */
1178
1391
  &.bb-offcanvas--has-handle.bb-offcanvas--bottom .bb-offcanvas__header,
1179
1392
  &.bb-offcanvas--has-handle.bb-offcanvas--bottom.bb-offcanvas--no-header
1180
1393
  .bb-offcanvas__body-content {
1181
- padding-top: calc(var(--bb-panel-p) + 12px);
1394
+ padding-top: calc(var(--py) + 12px);
1182
1395
  }
1183
1396
 
1184
1397
  .bb-offcanvas__header {
1398
+ /* Shared by the close control and by the space the title reserves for it,
1399
+ * so the two can never drift apart. */
1400
+ --close-size: 28px;
1401
+
1185
1402
  align-items: center;
1186
1403
  column-gap: 12px;
1187
1404
  display: flex;
1188
1405
  flex-shrink: 0;
1189
1406
  justify-content: space-between;
1190
- padding: var(--bb-panel-p) var(--bb-panel-p) calc(var(--bb-panel-p) / 2)
1191
- var(--bb-panel-p);
1407
+ padding: var(--py) var(--px) calc(var(--py) / 2) var(--px);
1192
1408
  position: relative;
1193
1409
  /* Header is a dependable drag surface even when content scrolls. */
1194
1410
  touch-action: none;
@@ -1197,38 +1413,102 @@ export type BbOffCanvasSlots = {
1197
1413
  font-size: var(--dialog-title-fs, calc(var(--bb-fs) + 2px));
1198
1414
  font-weight: 500;
1199
1415
  line-height: var(--dialog-title-fs, calc(var(--bb-fs) + 2px));
1416
+ /* Long titles wrap rather than truncate — the title usually names the
1417
+ * record you are acting on, so hiding the end of it is worse than a
1418
+ * second line. `anywhere` because what overflows is typically one
1419
+ * unbroken id with nowhere to break. Matters more here than on the
1420
+ * dialog: an adaptive sheet is as narrow as the phone. */
1421
+ min-width: 0;
1422
+ overflow-wrap: anywhere;
1423
+
1424
+ /* The close control is out of flow (below), so `column-gap` has
1425
+ * nothing to push against and the title would run under it. Reserve
1426
+ * the space here instead — only when there is actually a control to
1427
+ * clear, so `hideClose` keeps the full width. */
1428
+ &:has(~ .bb-close-button) {
1429
+ padding-right: calc(var(--close-size) + 4px);
1430
+ }
1431
+ }
1432
+ /* Only when a description is present. A header without one keeps its
1433
+ original single-row, vertically-centred layout exactly — this must not
1434
+ move any dialog that does not opt in. */
1435
+ &:has(.bb-offcanvas__description) {
1436
+ align-items: flex-start;
1437
+ flex-direction: column;
1438
+ row-gap: 2px;
1200
1439
  }
1201
1440
 
1441
+ .bb-offcanvas__description {
1442
+ color: var(--bb-text-muted);
1443
+ font-size: var(--bb-fs);
1444
+ font-weight: 400;
1445
+ line-height: 20px;
1446
+ margin: 0;
1447
+ /* Clears the absolutely-positioned close button the same way the title
1448
+ does. The × sits at the top-right, and a one-line title is short
1449
+ enough that the description's first line can still run under it. */
1450
+ &:has(~ .bb-close-button) {
1451
+ padding-right: calc(var(--close-size) + 4px);
1452
+ }
1453
+ }
1454
+
1455
+ /* Absolute on purpose: pinned to the panel corner, so a title that wraps
1456
+ * to two lines does not drag the × down with it, and the control never
1457
+ * adds its 28px to the header's height (the title's line box sets that).
1458
+ * The negative offsets align the ICON's edges with the padding box —
1459
+ * the button's own `--p` sits outside it. */
1202
1460
  > .bb-close-button {
1203
- --size: 28px;
1461
+ --size: var(--close-size);
1204
1462
  --p: 7px;
1205
1463
  position: absolute;
1206
- right: calc(var(--bb-panel-p) - var(--p));
1207
- top: calc(var(--bb-panel-p) - var(--p));
1464
+ right: calc(var(--px) - var(--p));
1465
+ top: calc(var(--py) - var(--p));
1208
1466
  }
1209
1467
  }
1210
1468
 
1211
1469
  .bb-offcanvas__body {
1212
1470
  flex: 1 1 auto;
1471
+ /* Keep descendant z-index (sticky/relative search) inside this scrollport
1472
+ so it cannot cover the absolutely-positioned grab handle. */
1473
+ isolation: isolate;
1474
+ /* Cap to the panel, not to the content — default `min-height: auto` lets a
1475
+ tall sheet (filter dropdown) grow the flex item past `max-height: 100%`
1476
+ and then nothing scrolls. */
1477
+ min-height: 0;
1213
1478
  overflow: auto;
1479
+ overscroll-behavior: contain;
1214
1480
  position: relative;
1215
1481
 
1216
- &:first-child {
1217
- .bb-offcanvas__body-content {
1218
- padding-top: var(--bb-panel-p);
1219
-
1220
- > .bb-close-button {
1221
- --size: 28px;
1222
- --p: 7px;
1223
- position: absolute;
1224
- right: calc(var(--bb-panel-p) - var(--p));
1225
- top: calc(var(--bb-panel-p) - var(--p));
1226
- }
1227
- }
1228
- }
1229
-
1230
1482
  .bb-offcanvas__body-content {
1231
- padding: 0px var(--bb-panel-p) var(--dialog-gap, var(--bb-panel-p));
1483
+ padding: 0px var(--px) var(--dialog-gap, var(--py));
1484
+
1485
+ /*
1486
+ * The headerless close control, pinned to the panel corner.
1487
+ *
1488
+ * NOT keyed on `.bb-offcanvas__body:first-child`, which is what this
1489
+ * rule used to sit under. A DRAGGABLE sheet renders
1490
+ * `.bb-offcanvas__handle` before the body, so `:first-child` never
1491
+ * matched there, the button fell back to `position: static`, and it
1492
+ * painted in normal flow AFTER the content — i.e. at the bottom of the
1493
+ * sheet. Every adaptive `BbSelectPopover` hit this: its sheet is
1494
+ * `draggable: true` and titleless, so it takes exactly this path.
1495
+ * Measured before the fix: `position: static`, 228px below the body top.
1496
+ *
1497
+ * The same `:first-child` fragility was already fixed a few rules up
1498
+ * for the no-header padding, with the reason spelled out
1499
+ * ("an absolutely-positioned drag handle can precede the body"); this
1500
+ * rule simply did not get the same treatment. No condition is needed
1501
+ * at all — the element only renders when there is no header
1502
+ * (`v-if="!renderHeader() && !hideClose"`), which is what the comment
1503
+ * beside it in the SFC already claimed this selector did.
1504
+ */
1505
+ > .bb-close-button {
1506
+ --size: 28px;
1507
+ --p: 7px;
1508
+ position: absolute;
1509
+ right: calc(var(--px) - var(--p));
1510
+ top: calc(var(--py) - var(--p));
1511
+ }
1232
1512
  }
1233
1513
 
1234
1514
  /* Keep nested overlays out of flow while closed, mirroring dialog behavior. */
@@ -1240,7 +1520,7 @@ export type BbOffCanvasSlots = {
1240
1520
 
1241
1521
  .bb-offcanvas__footer {
1242
1522
  border-top: 1px solid var(--bb-border);
1243
- padding: var(--dialog-gap, var(--bb-panel-p)) var(--bb-panel-p);
1523
+ padding: var(--dialog-gap, var(--py)) var(--px);
1244
1524
  pointer-events: auto;
1245
1525
 
1246
1526
  &:empty {