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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +55 -3
  2. package/bin/bitboss-ui-mcp.mjs +419 -9
  3. package/bin/bitboss-ui.mjs +293 -22
  4. package/dist/ai/BbAccordion.md +21 -23
  5. package/dist/ai/BbAlert.md +6 -12
  6. package/dist/ai/BbAvatar.md +60 -24
  7. package/dist/ai/BbBadge.md +12 -20
  8. package/dist/ai/BbBaseButton.md +14 -16
  9. package/dist/ai/BbBaseCheckboxIcon.md +6 -10
  10. package/dist/ai/BbBaseColorInput.md +1 -0
  11. package/dist/ai/BbBaseDatePickerInput.md +1 -0
  12. package/dist/ai/BbBaseNumberInput.md +1 -0
  13. package/dist/ai/BbBaseRadioIcon.md +6 -10
  14. package/dist/ai/BbBaseSelect.md +1 -0
  15. package/dist/ai/BbBaseSwitchIcon.md +5 -9
  16. package/dist/ai/BbBaseTag.md +1 -0
  17. package/dist/ai/BbBaseTextInput.md +2 -1
  18. package/dist/ai/BbBaseTextarea.md +1 -0
  19. package/dist/ai/BbBaseTimePickerInput.md +1 -0
  20. package/dist/ai/BbBreadcrumbs.md +23 -34
  21. package/dist/ai/BbButton.md +35 -47
  22. package/dist/ai/BbCheckbox.md +21 -37
  23. package/dist/ai/BbCheckboxGroup.md +13 -13
  24. package/dist/ai/BbCollapsible.md +14 -26
  25. package/dist/ai/BbColorInput.md +27 -23
  26. package/dist/ai/BbColorPalette.md +23 -35
  27. package/dist/ai/BbConfirm.md +17 -32
  28. package/dist/ai/BbDatePickerInput.md +29 -21
  29. package/dist/ai/BbDialog.md +159 -62
  30. package/dist/ai/BbDropdown.md +21 -25
  31. package/dist/ai/BbDropdownButton.md +13 -30
  32. package/dist/ai/BbDropzone.md +9 -11
  33. package/dist/ai/BbIcon.md +44 -33
  34. package/dist/ai/BbIndicator.md +2 -2
  35. package/dist/ai/BbNumberInput.md +32 -20
  36. package/dist/ai/BbOffCanvas.md +42 -42
  37. package/dist/ai/BbPagination.md +21 -23
  38. package/dist/ai/BbPopover.md +15 -17
  39. package/dist/ai/BbProgress.md +40 -51
  40. package/dist/ai/BbRadio.md +9 -9
  41. package/dist/ai/BbRadioGroup.md +7 -11
  42. package/dist/ai/BbRating.md +8 -12
  43. package/dist/ai/BbSelect.md +46 -36
  44. package/dist/ai/BbSelectPopover.md +19 -31
  45. package/dist/ai/BbSlider.md +15 -19
  46. package/dist/ai/BbSmoothHeight.md +16 -31
  47. package/dist/ai/BbSpinner.md +118 -74
  48. package/dist/ai/BbSwitch.md +25 -18
  49. package/dist/ai/BbSwitchGroup.md +9 -14
  50. package/dist/ai/BbTable.md +102 -55
  51. package/dist/ai/BbTabs.md +59 -82
  52. package/dist/ai/BbTag.md +26 -23
  53. package/dist/ai/BbTextInput.md +63 -27
  54. package/dist/ai/BbTextarea.md +42 -34
  55. package/dist/ai/BbTimePickerInput.md +14 -0
  56. package/dist/ai/BbToast.md +14 -34
  57. package/dist/ai/BbTooltip.md +22 -20
  58. package/dist/ai/BbTree.md +46 -72
  59. package/dist/ai/CommonInputWrapper.md +1 -0
  60. package/dist/ai/changelog.json +170 -62
  61. package/dist/ai/components.json +1678 -11
  62. package/dist/ai/composables/useConfirm.md +5 -2
  63. package/dist/ai/composables/useToast.md +4 -2
  64. package/dist/ai/guides/agent-contract.md +7 -4
  65. package/dist/ai/guides/ai-router.md +37 -22
  66. package/dist/ai/guides/component-picker.md +23 -18
  67. package/dist/ai/guides/design-language.md +151 -21
  68. package/dist/ai/guides/design-tokens.md +29 -4
  69. package/dist/ai/guides/icons-policy.md +46 -21
  70. package/dist/ai/guides/installation-and-plugin-setup.md +286 -17
  71. package/dist/ai/guides/migration/components/bb-rating.md +26 -1
  72. package/dist/ai/guides/migration/components/bb-tooltip.md +18 -0
  73. package/dist/ai/guides/migration/v2-to-v3.md +50 -23
  74. package/dist/ai/index.md +1 -1
  75. package/dist/ai/recipes/inertia/approvals-inbox.md +3 -3
  76. package/dist/ai/recipes/inertia/inline-edit-workspace.md +2 -2
  77. package/dist/ai/recipes/inertia/onboarding.md +2 -2
  78. package/dist/ai/recipes/inertia/ownership-atlas.md +6 -6
  79. package/dist/ai/recipes/inertia/record-form.md +5 -5
  80. package/dist/ai/recipes/inertia/records-workspace.md +4 -4
  81. package/dist/ai/recipes/inertia/upload-center.md +6 -6
  82. package/dist/ai/recipes/inertia/wizard-form.md +5 -5
  83. package/dist/ai/recipes/nuxt/approvals-inbox.md +3 -3
  84. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +2 -2
  85. package/dist/ai/recipes/nuxt/onboarding.md +2 -2
  86. package/dist/ai/recipes/nuxt/record-form.md +5 -5
  87. package/dist/ai/recipes/nuxt/records-workspace.md +4 -4
  88. package/dist/ai/recipes/nuxt/upload-center.md +6 -6
  89. package/dist/ai/recipes/nuxt/wizard-form.md +5 -5
  90. package/dist/ai/recipes/vue/approvals-inbox.md +3 -3
  91. package/dist/ai/recipes/vue/inline-edit-workspace.md +2 -2
  92. package/dist/ai/recipes/vue/onboarding.md +2 -2
  93. package/dist/ai/recipes/vue/record-form.md +10 -10
  94. package/dist/ai/recipes/vue/records-workspace.md +4 -4
  95. package/dist/ai/recipes/vue/upload-center.md +6 -6
  96. package/dist/ai/recipes/vue/wizard-form.md +5 -5
  97. package/dist/ai/source/BbBadge.md +1 -1
  98. package/dist/ai/source/BbBaseCheckbox.md +1 -1
  99. package/dist/ai/source/BbBaseColorInput.md +7 -0
  100. package/dist/ai/source/BbBaseDatePicker.md +5 -5
  101. package/dist/ai/source/BbBaseDatePickerInput.md +9 -2
  102. package/dist/ai/source/BbBaseNumberInput.md +7 -0
  103. package/dist/ai/source/BbBaseRadio.md +1 -1
  104. package/dist/ai/source/BbBaseRating.md +5 -5
  105. package/dist/ai/source/BbBaseSelect.md +8 -1
  106. package/dist/ai/source/BbBaseSlider.md +5 -5
  107. package/dist/ai/source/BbBaseSwitch.md +1 -1
  108. package/dist/ai/source/BbBaseTag.md +7 -0
  109. package/dist/ai/source/BbBaseTextInput.md +7 -0
  110. package/dist/ai/source/BbBaseTextarea.md +7 -0
  111. package/dist/ai/source/BbBaseTimePickerInput.md +8 -1
  112. package/dist/ai/source/BbBreadcrumbs.md +20 -1
  113. package/dist/ai/source/BbButton.md +2 -2
  114. package/dist/ai/source/BbColorInput.md +14 -0
  115. package/dist/ai/source/BbDatePickerInput.md +14 -0
  116. package/dist/ai/source/BbDialog.md +39 -8
  117. package/dist/ai/source/BbDropdown.md +1 -1
  118. package/dist/ai/source/BbDropzone.md +5 -5
  119. package/dist/ai/source/BbIcon.md +23 -10
  120. package/dist/ai/source/BbNumberInput.md +14 -0
  121. package/dist/ai/source/BbOffCanvas.md +50 -28
  122. package/dist/ai/source/BbPagination.md +2 -2
  123. package/dist/ai/source/BbPopover.md +12 -1
  124. package/dist/ai/source/BbSelect.md +14 -0
  125. package/dist/ai/source/BbSelectPopover.md +45 -7
  126. package/dist/ai/source/BbSpinner.md +2 -2
  127. package/dist/ai/source/BbTable.md +11 -11
  128. package/dist/ai/source/BbTag.md +14 -0
  129. package/dist/ai/source/BbTextInput.md +14 -0
  130. package/dist/ai/source/BbTextarea.md +14 -0
  131. package/dist/ai/source/BbTimePickerInput.md +14 -0
  132. package/dist/ai/source/BbTooltip.md +57 -10
  133. package/dist/ai/source/ChipsBox.md +1 -1
  134. package/dist/ai/source/CommaBox.md +1 -1
  135. package/dist/ai/source/CommonInputWrapper.md +128 -33
  136. package/dist/ai/source/GuidesComponent.md +14 -7
  137. package/dist/ai/source/OptionsContainer.md +1 -1
  138. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +1 -1
  139. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +1 -1
  140. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  141. package/dist/components/BbBaseColorInput/BbBaseColorInput.vue_vue_type_script_setup_true_lang.js +4 -1
  142. package/dist/components/BbBaseColorInput/types.d.ts +5 -0
  143. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +4 -4
  144. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +1 -1
  145. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +5 -2
  146. package/dist/components/BbBaseDatePickerInput/types.d.ts +5 -0
  147. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +1 -1
  148. package/dist/components/BbBaseNumberInput/BbBaseNumberInput.vue_vue_type_script_setup_true_lang.js +4 -1
  149. package/dist/components/BbBaseNumberInput/types.d.ts +5 -0
  150. package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +1 -1
  151. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +3 -3
  152. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +4 -1
  153. package/dist/components/BbBaseSelect/types.d.ts +5 -0
  154. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +3 -3
  155. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue_vue_type_script_setup_true_lang.js +1 -1
  156. package/dist/components/BbBaseTag/BbBaseTag.vue_vue_type_script_setup_true_lang.js +4 -1
  157. package/dist/components/BbBaseTag/types.d.ts +5 -0
  158. package/dist/components/BbBaseTextInput/BbBaseTextInput.vue_vue_type_script_setup_true_lang.js +4 -1
  159. package/dist/components/BbBaseTextInput/types.d.ts +5 -0
  160. package/dist/components/BbBaseTextarea/BbBaseTextarea.vue_vue_type_script_setup_true_lang.js +4 -1
  161. package/dist/components/BbBaseTextarea/types.d.ts +5 -0
  162. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +5 -2
  163. package/dist/components/BbBaseTimePickerInput/types.d.ts +5 -0
  164. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +71 -71
  165. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +2 -2
  166. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +5 -2
  167. package/dist/components/BbColorInput/types.d.ts +12 -0
  168. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +4 -2
  169. package/dist/components/BbDatePickerInput/types.d.ts +12 -0
  170. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +102 -97
  171. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +1 -1
  172. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +58 -58
  173. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +2 -2
  174. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +33 -35
  175. package/dist/components/BbIcon/iconCache.d.ts +30 -0
  176. package/dist/components/BbIcon/iconCache.js +21 -3
  177. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +5 -2
  178. package/dist/components/BbNumberInput/types.d.ts +12 -0
  179. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +57 -56
  180. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +2 -2
  181. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +138 -138
  182. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +4 -2
  183. package/dist/components/BbSelect/types.d.ts +12 -0
  184. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +43 -43
  185. package/dist/components/BbSpinner/BbSpinner.vue_vue_type_script_setup_true_lang.js +1 -1
  186. package/dist/components/BbSpinner/types.d.ts +1 -1
  187. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +8 -8
  188. package/dist/components/BbTable/BbTableDataRow.js +2 -2
  189. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +5 -2
  190. package/dist/components/BbTag/types.d.ts +12 -0
  191. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +5 -2
  192. package/dist/components/BbTextInput/types.d.ts +12 -0
  193. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +5 -2
  194. package/dist/components/BbTextarea/types.d.ts +12 -0
  195. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +4 -2
  196. package/dist/components/BbTimePickerInput/types.d.ts +12 -0
  197. package/dist/components/BbToast/BbToastRegion.vue_vue_type_script_setup_true_lang.js +62 -62
  198. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +81 -78
  199. package/dist/components/BbTooltip/types.d.ts +8 -1
  200. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +1 -1
  201. package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +1 -1
  202. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue.d.ts +1 -0
  203. package/dist/components/CommonInputWrapper/CommonInputWrapper.vue_vue_type_script_setup_true_lang.js +4 -2
  204. package/dist/components/CommonInputWrapper/types.d.ts +10 -0
  205. package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +1 -1
  206. package/dist/composables/useConfirm.d.ts +22 -0
  207. package/dist/composables/useConfirm.js +43 -40
  208. package/dist/composables/useSafeAreaInternal.js +1 -1
  209. package/dist/composables/useToast.d.ts +16 -0
  210. package/dist/composables/useToast.js +20 -16
  211. package/dist/composables/useViewportWidth.d.ts +20 -6
  212. package/dist/composables/useViewportWidth.js +8 -7
  213. package/dist/icon-scan.d.ts +276 -0
  214. package/dist/icon-scan.js +485 -0
  215. package/dist/icons-whitelist.d.ts +5 -4
  216. package/dist/index.d.ts +2 -0
  217. package/dist/input-variants.d.ts +18 -0
  218. package/dist/llms-full.txt +2173 -1537
  219. package/dist/llms-medium.txt +504 -82
  220. package/dist/llms.txt +1 -1
  221. package/dist/nuxt-module.d.ts +1 -0
  222. package/dist/nuxt.js +10 -7
  223. package/dist/plugin.js +1 -0
  224. package/dist/runtime/nuxt-plugin.js +2 -1
  225. package/dist/styles.css +1 -1
  226. package/dist/types/Config.d.ts +18 -0
  227. package/dist/types/InputVariant.d.ts +22 -0
  228. package/dist/utils/cssLayers.d.ts +91 -0
  229. package/dist/utils/cssLayers.js +43 -0
  230. package/dist/utils/injectLibraryStyles.d.ts +3 -1
  231. package/dist/utils/injectLibraryStyles.js +24 -23
  232. package/dist/validated/BbColorInput.vue_vue_type_script_setup_true_lang.js +1 -0
  233. package/dist/validated/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +1 -0
  234. package/dist/validated/BbNumberInput.vue_vue_type_script_setup_true_lang.js +1 -0
  235. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  236. package/dist/validated/BbTag.vue_vue_type_script_setup_true_lang.js +1 -0
  237. package/dist/validated/BbTextInput.vue_vue_type_script_setup_true_lang.js +1 -0
  238. package/dist/validated/BbTextarea.vue_vue_type_script_setup_true_lang.js +1 -0
  239. package/dist/validated/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +1 -0
  240. package/dist/validated/index.d.ts +1 -0
  241. package/dist/vite-plugin.d.ts +89 -12
  242. package/dist/vite.js +331 -281
  243. package/llms.txt +1 -1
  244. package/package.json +7 -5
  245. package/scripts/lib/ensure-manifest.mjs +28 -5
  246. package/scripts/lib/eslint-plugin.d.ts +35 -0
  247. package/scripts/lib/eslint-plugin.mjs +299 -3
  248. package/scripts/lib/hand-roll-hints.mjs +50 -6
  249. package/scripts/lib/html-attributes.mjs +42 -0
  250. package/scripts/lib/mcp-config.mjs +38 -34
  251. package/scripts/lib/public-type-exports.mjs +246 -0
  252. package/scripts/lib/validate-bb-markup.mjs +205 -12
@@ -21,6 +21,7 @@
21
21
  :prepend:icon="props['prepend:icon']"
22
22
  :readonly="props.readonly"
23
23
  :reserve-append-space="props.clearable"
24
+ :variant="props.variant"
24
25
  @click:clear="$emit('update:modelValue', null)"
25
26
  >
26
27
  <template #prepend-outer><slot name="prepend-outer"></slot></template>
@@ -258,6 +259,7 @@ defineExpose({
258
259
  ## Types
259
260
 
260
261
  ```ts
262
+ import type { InputVariantType } from '@/types/InputVariant';
261
263
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
262
264
  import type { IconType } from '@/types/Icon';
263
265
  import type { MaskInputOptions } from 'maska';
@@ -296,6 +298,11 @@ export type BaseTextInputProps = {
296
298
  */
297
299
  compact?: boolean;
298
300
 
301
+ /**
302
+ * Visual variant of the field box, forwarded to `CommonInputWrapper`.
303
+ */
304
+ variant?: InputVariantType;
305
+
299
306
  /**
300
307
  * Disable all interactions with the input.
301
308
  */
@@ -24,6 +24,7 @@
24
24
  :prepend:icon="props['prepend:icon']"
25
25
  :readonly="props.readonly"
26
26
  :reserve-append-space="clearable"
27
+ :variant="props.variant"
27
28
  @click:clear="$emit('update:modelValue', null)"
28
29
  >
29
30
  <template #prepend-outer><slot name="prepend-outer"></slot></template>
@@ -204,6 +205,7 @@ function resetTextareaHeight() {
204
205
  ## Types
205
206
 
206
207
  ```ts
208
+ import type { InputVariantType } from '@/types/InputVariant';
207
209
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
208
210
  import type { IconType } from '@/types/Icon';
209
211
 
@@ -247,6 +249,11 @@ export type BaseTextareaProps = {
247
249
  */
248
250
  compact?: boolean;
249
251
 
252
+ /**
253
+ * Visual variant of the field box, forwarded to `CommonInputWrapper`.
254
+ */
255
+ variant?: InputVariantType;
256
+
250
257
  /**
251
258
  * Disable all interactions with the textarea.
252
259
  */
@@ -25,6 +25,7 @@
25
25
  :loading="props.loading"
26
26
  :prepend:icon="props['prepend:icon']"
27
27
  :readonly="props.readonly"
28
+ :variant="props.variant"
28
29
  @click:clear="onClickClear"
29
30
  @focusin="onFocusIn"
30
31
  >
@@ -129,7 +130,7 @@
129
130
 
130
131
  <span
131
132
  :id="descriptionId"
132
- class="bb-base-time-picker-input__accessible-description sr-only"
133
+ class="bb-base-time-picker-input__accessible-description bb-sr-only"
133
134
  >{{ formatHint }}</span
134
135
  >
135
136
  <template v-if="$slots.suffix" #suffix><slot name="suffix" /></template>
@@ -822,6 +823,7 @@ onBeforeUnmount(() => {
822
823
  ## Types
823
824
 
824
825
  ```ts
826
+ import type { InputVariantType } from '@/types/InputVariant';
825
827
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
826
828
  import type { IconType } from '@/types/Icon';
827
829
  import type { BbOffCanvasProps } from '../BbOffCanvas/types';
@@ -877,6 +879,11 @@ export type BaseTimePickerInputProps = {
877
879
  /** Applies the compact density styles to the control. */
878
880
  compact?: boolean;
879
881
 
882
+ /**
883
+ * Visual variant of the field box, forwarded to `CommonInputWrapper`.
884
+ */
885
+ variant?: InputVariantType;
886
+
880
887
  /** Disables the input, clock trigger, and popover. */
881
888
  disabled?: boolean;
882
889
 
@@ -174,9 +174,28 @@ const props = withDefaults(defineProps<BbBreadcrumbsProps>(), {
174
174
  defineSlots<BbBreadcrumbsSlots>();
175
175
  const { t } = useLocale();
176
176
  const logger = useLogger();
177
- const FLEX_BASIS_OFFSET_WIDTH = 5;
178
177
  const SAFETY_MARGIN_WIDTH = 20;
179
178
  const COLLAPSE_BUFFER = 1;
179
+ /**
180
+ * Slack added to `--flex-basis` on top of the width the items actually need.
181
+ *
182
+ * It is DERIVED, not tuned: the basis is the width this component asks the flex
183
+ * row for, and `recompute()` must agree that everything fits at exactly that
184
+ * width. Substituting `availableWidth = estimatedRequiredWidth` into
185
+ * `effectiveAvailableWidth` cancels the offset and the safety margin and leaves
186
+ * `FLEX_BASIS_OFFSET_WIDTH - COLLAPSE_BUFFER`, which then has to clear
187
+ * `isNearThreshold`'s `SAFETY_MARGIN_WIDTH / 2` band.
188
+ *
189
+ * At the old hand-picked `5` it did not: the slack came to `5 - 1 = 4`, inside
190
+ * the 10px band, so at its own basis the component always declared one item
191
+ * overflowed. That flipped `--flex-grow` to `1`, the row grew it, everything
192
+ * then fitted, `--flex-grow` went back to `0`, it shrank to the basis, and the
193
+ * cycle repeated every frame — a self-inflicted resize→recompute loop that only
194
+ * showed up once an app stopped overriding `flex-basis` (Tailwind v4 puts
195
+ * utilities in `@layer utilities`, which lose to our unlayered rule).
196
+ */
197
+ const COLLAPSE_BUFFER_AND_BAND = COLLAPSE_BUFFER + SAFETY_MARGIN_WIDTH / 2;
198
+ const FLEX_BASIS_OFFSET_WIDTH = COLLAPSE_BUFFER_AND_BAND + 1;
180
199
  const FONT_WIDTH = 8.5;
181
200
 
182
201
  const slots = useSlots();
@@ -51,10 +51,10 @@
51
51
  class="bb-button__prepend-icon"
52
52
  :icon="props['prepend:icon']"
53
53
  />
54
- <span class="bb-button__content" :class="{ 'sr-only': !!icon }"
54
+ <span class="bb-button__content" :class="{ 'bb-sr-only': !!icon }"
55
55
  ><slot>{{ text }}</slot></span
56
56
  >
57
- <span v-if="loading" class="bb-button__loading-label sr-only">{{
57
+ <span v-if="loading" class="bb-button__loading-label bb-sr-only">{{
58
58
  t('common.loadingText')
59
59
  }}</span>
60
60
 
@@ -51,6 +51,7 @@
51
51
  :readonly="readonly"
52
52
  :required="required"
53
53
  :swatches="swatches"
54
+ :variant="variant"
54
55
  v-bind="eventListeners"
55
56
  >
56
57
  <template #append-outer><slot name="append-outer" /></template>
@@ -139,6 +140,7 @@ const eventListeners = {
139
140
  ## Types
140
141
 
141
142
  ```ts
143
+ import type { InputVariantType } from '@/types/InputVariant';
142
144
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
143
145
  import type { InputDirection } from '@/types/InputDirection';
144
146
  import type { IconType } from '@/types/Icon';
@@ -182,6 +184,18 @@ export type BbColorInputProps = {
182
184
  * Displays the component in a compact version.
183
185
  */
184
186
  compact?: boolean;
187
+
188
+ /**
189
+ * Visual variant of the field box — the same names and tokens as the
190
+ * `BbButton` variants. Colours only: height, padding and border width are
191
+ * identical across variants, so a form never reflows when one changes.
192
+ * `'ghost'` has no border in any state (errors and warnings show through
193
+ * the icon and the messages); every variant keeps the focus ring.
194
+ * Register extra names with the plugin's `inputVariants` option.
195
+ *
196
+ * @defaultValue `'outline'`
197
+ */
198
+ variant?: InputVariantType;
185
199
  /**
186
200
  * Descriptive text displayed below the label and above the input. Unlike the hint it is always
187
201
  * visible, and it is linked to the input via `aria-describedby` (after any
@@ -64,6 +64,7 @@
64
64
  :transition-duration="transitionDuration"
65
65
  :type="type"
66
66
  :utc="utc"
67
+ :variant="variant"
67
68
  v-bind="eventListeners"
68
69
  >
69
70
  <template #prepend-outer><slot name="prepend-outer"></slot></template>
@@ -158,6 +159,7 @@ const eventListeners = {
158
159
  ## Types
159
160
 
160
161
  ```ts
162
+ import type { InputVariantType } from '@/types/InputVariant';
161
163
  import type { DatePickerType } from '@/types/DatePickerType';
162
164
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
163
165
  import type { InputDirection } from '@/types/InputDirection';
@@ -227,6 +229,18 @@ export type BbDatePickerInputProps = {
227
229
  */
228
230
  compact?: boolean;
229
231
 
232
+ /**
233
+ * Visual variant of the field box — the same names and tokens as the
234
+ * `BbButton` variants. Colours only: height, padding and border width are
235
+ * identical across variants, so a form never reflows when one changes.
236
+ * `'ghost'` has no border in any state (errors and warnings show through
237
+ * the icon and the messages); every variant keeps the focus ring.
238
+ * Register extra names with the plugin's `inputVariants` option.
239
+ *
240
+ * @defaultValue `'outline'`
241
+ */
242
+ variant?: InputVariantType;
243
+
230
244
  /**
231
245
  * Direction of the layout of the component. Can either be a predefined value or a pattern separated by a space like `xx xxxxx`.
232
246
  */
@@ -50,6 +50,7 @@
50
50
  'bb-dialog--mobile': isMobile,
51
51
  'bb-dialog--deny-close': denyClose,
52
52
  'bb-dialog--covered': isCovered,
53
+ 'bb-dialog--stacked': isStackedAbove,
53
54
  }"
54
55
  :inert="(!model && !isVisible) || isCovered"
55
56
  :style="{
@@ -430,6 +431,20 @@ const isCovered = computed(() => {
430
431
  return list[list.length - 1] !== stackId;
431
432
  });
432
433
 
434
+ /**
435
+ * True when this dialog sits ABOVE another member of its stack. A stack paints
436
+ * ONE backdrop, owned by its bottom member; everyone above rides on it with a
437
+ * transparent backdrop of their own (see `--stacked` in index.css). Two things
438
+ * fall out: swapping members never fades a backdrop, so the page behind never
439
+ * blinks mid-crossfade, and N stacked dialogs cost one blur pass instead of N.
440
+ */
441
+ const isStackedAbove = computed(() => {
442
+ if (!props.stack) return false;
443
+ const list = stacks.value[props.stack];
444
+ if (!list || !list.includes(stackId)) return false;
445
+ return list[0] !== stackId;
446
+ });
447
+
433
448
  let releaseTopLayer: (() => void) | null = null;
434
449
  const toggleDialog = async (value: boolean) => {
435
450
  if (value) {
@@ -460,13 +475,21 @@ const toggleDialog = async (value: boolean) => {
460
475
  emit('shown');
461
476
  } else {
462
477
  emit('hide');
478
+ // Leave the stack NOW, not after the fade. The member beneath uncovers
479
+ // at once and crossfades in under this one's fade-out, so it is fully
480
+ // visible one transition after the close — not two, with a dead window
481
+ // where nothing was on screen (measured 2026-09-10: 250ms of blank, the
482
+ // parent finishing at ~520ms). Safe to do early: while this dialog is
483
+ // still natively open, the HTML modal algorithm keeps everything outside
484
+ // it inert regardless of our classes, and Escape stays owned through the
485
+ // overlay stack until the listener is removed below.
486
+ partStack();
463
487
  const currentOpenedTimes = openedTimes.value;
464
488
  await wait(props.transitionDuration);
465
- // Reopened during the transition — keep it open, and keep its stack
466
- // membership + top-layer host intact (they were never released).
489
+ // Reopened during the transition — keep it open. The open path already
490
+ // re-joined the stack (on top); the top-layer host was never released.
467
491
  if (currentOpenedTimes !== openedTimes.value) return;
468
492
  removeEscapeListener();
469
- partStack();
470
493
  dialog.value?.close();
471
494
  // Release the top-layer host only after close(): releasing at close-start
472
495
  // would re-home body toasts under a still-modal dialog, freezing them for
@@ -842,14 +865,22 @@ export type { BbDialogSlots as BaseDialogSlots };
842
865
  animation: bb-dialog-deny-bounce 220ms cubic-bezier(0.22, 1, 0.36, 1);
843
866
  }
844
867
 
845
- /* A dialog covered by a later sibling of the same stack fades out entirely —
846
- only the top-most member of the stack stays visible. */
868
+ /* A dialog covered by a later sibling of the same stack fades its BOX out —
869
+ only the top-most member of the stack stays visible. Its backdrop stays:
870
+ the bottom member owns the stack's one backdrop (below). */
847
871
  &.bb-dialog--covered {
848
872
  opacity: 0;
873
+ }
849
874
 
850
- &::backdrop {
851
- opacity: 0;
852
- }
875
+ /* A member above the bottom of its stack rides on the bottom member's
876
+ backdrop and paints none of its own. Swapping members therefore never
877
+ fades a backdrop — no brightness blink mid-crossfade — and the stack costs
878
+ one blur pass however deep it goes. Must follow `--open`, which sets the
879
+ backdrop to 1 at equal specificity. */
880
+ &.bb-dialog--stacked::backdrop {
881
+ -webkit-backdrop-filter: none;
882
+ backdrop-filter: none;
883
+ opacity: 0;
853
884
  }
854
885
 
855
886
  &::backdrop {
@@ -94,7 +94,7 @@
94
94
  menu container: `role="menu"` admits only items and groups, and a
95
95
  reader in menu mode walks items — so the presentational row inside
96
96
  is skipped and an empty search would announce nothing. -->
97
- <span class="sr-only" role="status">{{ statusText }}</span>
97
+ <span class="bb-sr-only" role="status">{{ statusText }}</span>
98
98
  <div v-if="$slots.footer" class="bb-dropdown__footer">
99
99
  <slot
100
100
  :disabled="disabled"
@@ -41,7 +41,7 @@
41
41
  :aria-label="dropzoneAriaLabel"
42
42
  :aria-labelledby="ariaLabelledby"
43
43
  :autofocus="autofocus"
44
- class="sr-only"
44
+ class="bb-sr-only"
45
45
  :disabled="disabled"
46
46
  :multiple="multiple"
47
47
  type="file"
@@ -54,8 +54,8 @@
54
54
  half of that composed name ("<your copy> Select files"), so the
55
55
  name still says the control takes files. Without it the localized
56
56
  `aria-label` is the whole name and this span would be dead
57
- sr-only chatter on every dropzone. -->
58
- <span v-if="hasSlotLabel" :id="defaultLabelId" class="sr-only">{{
57
+ bb-sr-only chatter on every dropzone. -->
58
+ <span v-if="hasSlotLabel" :id="defaultLabelId" class="bb-sr-only">{{
59
59
  dropzoneAriaLabel
60
60
  }}</span>
61
61
  </label>
@@ -330,7 +330,7 @@ const errorContainerId = `${containerId}_errors`;
330
330
  const warningContainerId = `${containerId}_warnings`;
331
331
  /** Handed to the slot as `labelId`; the consumer puts it on their label node. */
332
332
  const slotLabelId = `${containerId}_label`;
333
- /** Our sr-only copy of the localized instruction, appended to that name. */
333
+ /** Our bb-sr-only copy of the localized instruction, appended to that name. */
334
334
  const defaultLabelId = `${containerId}_default_label`;
335
335
 
336
336
  const dragging = ref(false);
@@ -394,7 +394,7 @@ onUpdated(syncSlotLabel);
394
394
 
395
395
  /**
396
396
  * `aria-labelledby` wins over `aria-label`, so the localized instruction has to
397
- * be re-supplied as a referenced node — hence the sr-only span. Consumer copy
397
+ * be re-supplied as a referenced node — hence the bb-sr-only span. Consumer copy
398
398
  * first: WCAG 2.5.3 (Label in Name) wants the visible label at the START of the
399
399
  * accessible name so speech input can match what the user reads.
400
400
  */
@@ -18,7 +18,7 @@
18
18
  import {
19
19
  computed,
20
20
  inject,
21
- ref,
21
+ shallowRef,
22
22
  onServerPrefetch,
23
23
  onBeforeMount,
24
24
  watch,
@@ -36,8 +36,10 @@ import { useConfig } from '@/composables/useConfig';
36
36
  import {
37
37
  cacheKeyFor as cacheKey,
38
38
  EMPTY_ICON,
39
+ MISSING_ICON,
39
40
  parseIcon,
40
41
  parsedIcons,
42
+ resolveRegistryIcon,
41
43
  type ParsedIcon,
42
44
  } from './iconCache';
43
45
  import type { BbIconProps, BbIconSize } from './types';
@@ -52,7 +54,10 @@ if (process.env.NODE_ENV !== 'production')
52
54
  warnRemovedAttrs('BbIcon', removedPropsFor('BbIcon'));
53
55
 
54
56
  const logger = useLogger();
55
- const icons = ref<IconRegistry>();
57
+ // Shallow: the registry is a (possibly Proxy-wrapped) map of loaders — there is
58
+ // nothing to track inside it, and a deep reactive wrapper would proxy the
59
+ // plugin's own Proxy on every lookup.
60
+ const icons = shallowRef<IconRegistry>();
56
61
 
57
62
  const loadIcons = () => {
58
63
  // Prefer the typed Symbol key; fall back to the legacy string key for one
@@ -102,7 +107,10 @@ const cacheKeyFor = (value: string) => cacheKey(value, isSvgPayload(value));
102
107
  * Seeded from the module cache: an icon this page has already resolved renders
103
108
  * complete in the FIRST render pass, with no loader promise and no regex.
104
109
  */
105
- const parsed = ref<ParsedIcon | null>(
110
+ // Shallow on purpose: a parsed icon is immutable, and the placeholder is
111
+ // recognised by IDENTITY (`=== MISSING_ICON`) — a deep ref would hand back a
112
+ // reactive proxy of it and the comparison would never hold.
113
+ const parsed = shallowRef<ParsedIcon | null>(
106
114
  parsedIcons.get(cacheKeyFor(props.icon)) ?? null
107
115
  );
108
116
 
@@ -136,13 +144,13 @@ const getIcon = async () => {
136
144
  logger.throw('Icons have not been loaded');
137
145
  }
138
146
 
139
- const iconLoader = icons.value![key];
140
- if (!iconLoader) {
141
- logger.throw(`Icon "${key}" does not exist.`);
142
- }
143
-
144
- const source = await iconLoader();
145
- parsed.value = parsedIcons.set(key, parseIcon(source)).get(key)!;
147
+ // Never throws: a name that does not resolve yields the built-in
148
+ // placeholder (and one console.error per name per page), so the component
149
+ // tree around the icon stays mounted and the gap is visible where the icon
150
+ // should be — in dev exactly as in production.
151
+ const resolved = await resolveRegistryIcon(key, icons.value!);
152
+ // The prop may have moved on while the loader was in flight.
153
+ if (cacheKeyFor(props.icon) === key) parsed.value = resolved;
146
154
  };
147
155
 
148
156
  onServerPrefetch(async () => {
@@ -217,6 +225,11 @@ const iconAttributes = computed(() => {
217
225
  // Icons should never be focusable or keyboard navigable since they're not interactive
218
226
  base['focusable'] = 'false';
219
227
 
228
+ // The placeholder stands in for a name that did not resolve: sized and
229
+ // coloured exactly like the icon it replaces, and findable in the DOM (and
230
+ // in tests) by the name that was asked for.
231
+ if (parsed.value === MISSING_ICON) base['data-bb-icon-missing'] = props.icon;
232
+
220
233
  return base;
221
234
  });
222
235
  </script>
@@ -55,6 +55,7 @@
55
55
  :readonly="readonly"
56
56
  :required="required"
57
57
  :step="step"
58
+ :variant="variant"
58
59
  v-bind="eventListeners"
59
60
  >
60
61
  <template #append-outer="data"
@@ -146,6 +147,7 @@ const eventListeners = {
146
147
  ## Types
147
148
 
148
149
  ```ts
150
+ import type { InputVariantType } from '@/types/InputVariant';
149
151
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
150
152
  import type { InputDirection } from '@/types/InputDirection';
151
153
  import type { IconType } from '@/types/Icon';
@@ -180,6 +182,18 @@ export type BbNumberInputProps = {
180
182
  */
181
183
  compact?: boolean;
182
184
 
185
+ /**
186
+ * Visual variant of the field box — the same names and tokens as the
187
+ * `BbButton` variants. Colours only: height, padding and border width are
188
+ * identical across variants, so a form never reflows when one changes.
189
+ * `'ghost'` has no border in any state (errors and warnings show through
190
+ * the icon and the messages); every variant keeps the focus ring.
191
+ * Register extra names with the plugin's `inputVariants` option.
192
+ *
193
+ * @defaultValue `'outline'`
194
+ */
195
+ variant?: InputVariantType;
196
+
183
197
  /**
184
198
  * Descriptive text displayed below the label and above the input. Unlike the hint it is always
185
199
  * visible, and it is linked to the input via `aria-describedby` (after any
@@ -28,6 +28,7 @@
28
28
  'bb-offcanvas--swipe-exit': swipeExiting,
29
29
  'bb-offcanvas--instant-stack': instantStack,
30
30
  'bb-offcanvas--covered': isCovered,
31
+ 'bb-offcanvas--stacked': isStackedAbove,
31
32
  'bb-offcanvas--has-handle': draggable,
32
33
  'bb-offcanvas--no-header': !renderHeader(),
33
34
  [`bb-offcanvas--${props.side}`]: true,
@@ -333,13 +334,21 @@ const toggleDialog = async (value: boolean) => {
333
334
  emit('shown');
334
335
  } else {
335
336
  emit('hide');
337
+ // Leave the stack NOW, not after the slide-out. The parents' push offsets
338
+ // derive from the stack registry, so releasing it here lets them slide
339
+ // back in lockstep with this panel leaving — instead of staying pushed
340
+ // for the whole transition and only then starting their own (2026-09-10).
341
+ // Safe to do early: Escape ownership lives in the overlay escape stack,
342
+ // not in `openOrder`, and stays with this panel until the listener is
343
+ // removed below; while still natively open, the modal algorithm keeps
344
+ // everything outside it inert regardless of our classes.
345
+ leaveStack();
336
346
  const currentOpenedTimes = openedTimes.value;
337
347
  await wait(props.transitionDuration);
338
- // Reopened during the transition — keep it open, and keep its stack
339
- // membership + top-layer host intact (they were never released).
348
+ // Reopened during the transition — keep it open. The open path already
349
+ // re-entered the stack (on top); the top-layer host was never released.
340
350
  if (currentOpenedTimes !== openedTimes.value) return;
341
351
  removeEscapeListener();
342
- leaveStack();
343
352
  dialog.value?.close();
344
353
  // Release the top-layer host only after close(): releasing at close-start
345
354
  // would re-home body toasts under a still-modal panel, freezing them for
@@ -522,20 +531,25 @@ const leaveStack = () => {
522
531
  };
523
532
 
524
533
  /**
525
- * The top *two* open offcanvases keep their blurred/dimmed backdrop; every panel
526
- * beneath them is `covered` (transparent backdrop). Two rather than one so that
527
- * when the front panel closes, the panel behind it is already blurred — there is
528
- * no frame where the reinstating blur leaves an unblurred surface showing.
534
+ * `covered`: another open offcanvas sits on top of this one. A covered panel
535
+ * is inert, yields Escape to the front-most panel and drops its `will-change`
536
+ * layer (it cannot be dragged). Backdrops are NOT keyed off this — see
537
+ * `isStackedAbove`.
529
538
  */
530
- const BLURRED_LAYERS = 2;
531
539
  const isCovered = computed(() => {
532
540
  const index = openOrder.value.indexOf(stackId);
533
- return (
534
- isVisible.value &&
535
- index !== -1 &&
536
- index < openOrder.value.length - BLURRED_LAYERS
537
- );
541
+ return isVisible.value && index !== -1 && index < openOrder.value.length - 1;
538
542
  });
543
+ /**
544
+ * One backdrop per pile of open offcanvases, owned by the BOTTOM-most: every
545
+ * panel above it renders a transparent, unfiltered backdrop (`--stacked`). The
546
+ * owner never loses or regains its blur/dim while panels open and close above
547
+ * it, so a closing child's backdrop fade changes nothing on screen — no
548
+ * double-dim brightening to single-dim, and no blur snapping in on a deeper
549
+ * member as the pile shrinks. It also costs one blur pass however deep it goes.
550
+ * Mirrors `bb-dialog--stacked`.
551
+ */
552
+ const isStackedAbove = computed(() => openOrder.value.indexOf(stackId) > 0);
539
553
 
540
554
  // Re-register if the `stack` name, `direction` or `stackGap` changes while open,
541
555
  // so the registry's bookkeeping (order, direction, size, gap) stays in sync.
@@ -1140,20 +1154,12 @@ export type BbOffCanvasSlots = {
1140
1154
  --stack-transition-duration: 0s;
1141
1155
  }
1142
1156
 
1143
- /* Only the top-most open panel blurs + dims its backdrop. Every panel beneath
1144
- it is `covered`: its backdrop goes transparent so N stacked panels cost one
1145
- blur pass instead of N (stacked `backdrop-filter`s each reblur the whole
1146
- scene and choke the GPU past a handful). It also drops its `will-change`
1147
- layer, since a covered panel can't be dragged. Must follow `--visible` to
1148
- win the equal-specificity `will-change`. */
1157
+ /* A `covered` panel (another open panel sits on top) drops its `will-change`
1158
+ layer, since it can't be dragged. Must follow `--visible` to win the
1159
+ equal-specificity `will-change`. Its backdrop is untouched: which panel
1160
+ paints the backdrop is decided by `--stacked` below. */
1149
1161
  &.bb-offcanvas--covered {
1150
1162
  will-change: auto;
1151
-
1152
- &::backdrop {
1153
- -webkit-backdrop-filter: none;
1154
- backdrop-filter: none;
1155
- background-color: transparent;
1156
- }
1157
1163
  }
1158
1164
 
1159
1165
  /* Stack pad: fills from the panel's anchored edge out to the screen edge, so a
@@ -1294,6 +1300,21 @@ export type BbOffCanvasSlots = {
1294
1300
  }
1295
1301
  }
1296
1302
  }
1303
+
1304
+ /* One backdrop per pile of open panels, painted by the BOTTOM-most. Every
1305
+ panel above it is `--stacked`: no blur, no dim, so a child's backdrop
1306
+ fading out on close changes nothing on screen (two half-dims composite
1307
+ lighter than one; before this the pile brightened during the handoff),
1308
+ and N stacked panels cost one blur pass instead of N (stacked
1309
+ `backdrop-filter`s each reblur the whole scene and choke the GPU past a
1310
+ handful). Must follow `--open`, which sets opacity 1 at equal
1311
+ specificity. Mirrors `bb-dialog--stacked`. */
1312
+ &.bb-offcanvas--stacked::backdrop {
1313
+ -webkit-backdrop-filter: none;
1314
+ backdrop-filter: none;
1315
+ opacity: 0;
1316
+ }
1317
+
1297
1318
  &.bb-offcanvas--deny-close {
1298
1319
  animation-duration: 220ms;
1299
1320
  animation-fill-mode: both;
@@ -1319,9 +1340,10 @@ export type BbOffCanvasSlots = {
1319
1340
  backdrop-filter: blur(var(--bb-overlay-blur, 2px));
1320
1341
  background-color: var(--bb-overlay);
1321
1342
  opacity: 0;
1322
- /* Also transition the dim: when a panel becomes `covered` its dim fades out
1323
- in step with the next backdrop's dim fading in, so the total stays
1324
- constant during open/close instead of flicking lighter/darker. */
1343
+ /* Also transition the dim: when the backdrop owner closes under a panel
1344
+ that stays open, the new owner's dim fades in in step with the old one
1345
+ fading out, so the total stays roughly constant instead of flicking
1346
+ lighter/darker. */
1325
1347
  transition:
1326
1348
  opacity var(--transition-duration) var(--bb-ease),
1327
1349
  background-color var(--transition-duration) var(--bb-ease);
@@ -19,7 +19,7 @@
19
19
  <li>
20
20
  <BbBaseButton v-bind="previousAttributes">
21
21
  <slot name="previous">
22
- <span class="bb-pagination__page-label sr-only">{{
22
+ <span class="bb-pagination__page-label bb-sr-only">{{
23
23
  t('pagination.previous').value
24
24
  }}</span>
25
25
  <svg
@@ -65,7 +65,7 @@
65
65
  <li>
66
66
  <BbBaseButton v-bind="nextAttributes">
67
67
  <slot name="next">
68
- <span class="bb-pagination__page-label sr-only">{{
68
+ <span class="bb-pagination__page-label bb-sr-only">{{
69
69
  t('pagination.next').value
70
70
  }}</span>
71
71
  <svg
@@ -389,7 +389,18 @@ useIntersectionObserver(
389
389
  ([{ intersectionRatio }]) => {
390
390
  // A modal sheet is detached from the activator's position, so it must not
391
391
  // close just because the activator scrolls out of view.
392
- if (intersectionRatio !== 1 && isOpen.value && !useOffcanvas.value) close();
392
+ //
393
+ // "Fully visible" is a tolerance, never `=== 1`. The ratio is
394
+ // intersection area over target area, and for an activator with a
395
+ // sub-pixel box (a bare <button> whose width is its text, 29.3px under
396
+ // the shipped reset) Chromium reports 0.9999… for an element that is
397
+ // entirely on screen. A strict `!== 1` then closed the popover in the
398
+ // same millisecond it opened — the observer's very first entry, delivered
399
+ // right after `showModal()` — for any consumer whose activator happened
400
+ // to measure fractionally (2026-09-09). 1% is 0.18px on an 18px button:
401
+ // rounding noise, never a real clip.
402
+ const fullyVisible = intersectionRatio >= 0.99;
403
+ if (!fullyVisible && isOpen.value && !useOffcanvas.value) close();
393
404
  },
394
405
  { threshold: [0, 1] }
395
406
  );
@@ -78,6 +78,7 @@
78
78
  :selected-labels-fn="selectedLabelsFn"
79
79
  :stash="stash"
80
80
  :transition-duration="transitionDuration"
81
+ :variant="variant"
81
82
  v-bind="eventListeners"
82
83
  >
83
84
  <template v-if="$slots.header" #header="data">
@@ -218,6 +219,7 @@ const eventListeners = {
218
219
  ## Types
219
220
 
220
221
  ```ts
222
+ import type { InputVariantType } from '@/types/InputVariant';
221
223
  import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
222
224
  import type { InputDirection } from '@/types/InputDirection';
223
225
  import type { ItemAccessor } from '@/types/ItemAccessor';
@@ -287,6 +289,18 @@ export type BbSelectProps<T> = {
287
289
  */
288
290
  compact?: boolean;
289
291
 
292
+ /**
293
+ * Visual variant of the field box — the same names and tokens as the
294
+ * `BbButton` variants. Colours only: height, padding and border width are
295
+ * identical across variants, so a form never reflows when one changes.
296
+ * `'ghost'` has no border in any state (errors and warnings show through
297
+ * the icon and the messages); every variant keeps the focus ring.
298
+ * Register extra names with the plugin's `inputVariants` option.
299
+ *
300
+ * @defaultValue `'outline'`
301
+ */
302
+ variant?: InputVariantType;
303
+
290
304
  /**
291
305
  * Defines an array of dependencies that will trigger actions in the component upon change.
292
306
  */