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

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 (183) 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/BbBaseRadioIcon.md +6 -10
  11. package/dist/ai/BbBaseSwitchIcon.md +5 -9
  12. package/dist/ai/BbBreadcrumbs.md +23 -34
  13. package/dist/ai/BbButton.md +35 -47
  14. package/dist/ai/BbCheckbox.md +21 -37
  15. package/dist/ai/BbCheckboxGroup.md +13 -13
  16. package/dist/ai/BbCollapsible.md +14 -26
  17. package/dist/ai/BbColorInput.md +13 -23
  18. package/dist/ai/BbColorPalette.md +23 -35
  19. package/dist/ai/BbConfirm.md +17 -32
  20. package/dist/ai/BbDatePickerInput.md +15 -21
  21. package/dist/ai/BbDialog.md +159 -62
  22. package/dist/ai/BbDropdown.md +21 -25
  23. package/dist/ai/BbDropdownButton.md +13 -30
  24. package/dist/ai/BbDropzone.md +9 -11
  25. package/dist/ai/BbIcon.md +44 -33
  26. package/dist/ai/BbIndicator.md +2 -2
  27. package/dist/ai/BbNumberInput.md +18 -20
  28. package/dist/ai/BbOffCanvas.md +42 -42
  29. package/dist/ai/BbPagination.md +21 -23
  30. package/dist/ai/BbPopover.md +15 -17
  31. package/dist/ai/BbProgress.md +40 -51
  32. package/dist/ai/BbRadio.md +9 -9
  33. package/dist/ai/BbRadioGroup.md +7 -11
  34. package/dist/ai/BbRating.md +8 -12
  35. package/dist/ai/BbSelect.md +32 -36
  36. package/dist/ai/BbSelectPopover.md +19 -31
  37. package/dist/ai/BbSlider.md +15 -19
  38. package/dist/ai/BbSmoothHeight.md +16 -31
  39. package/dist/ai/BbSpinner.md +118 -74
  40. package/dist/ai/BbSwitch.md +25 -18
  41. package/dist/ai/BbSwitchGroup.md +9 -14
  42. package/dist/ai/BbTable.md +102 -55
  43. package/dist/ai/BbTabs.md +59 -82
  44. package/dist/ai/BbTag.md +12 -23
  45. package/dist/ai/BbTextInput.md +20 -26
  46. package/dist/ai/BbTextarea.md +28 -34
  47. package/dist/ai/BbToast.md +14 -34
  48. package/dist/ai/BbTooltip.md +22 -20
  49. package/dist/ai/BbTree.md +46 -72
  50. package/dist/ai/changelog.json +169 -62
  51. package/dist/ai/components.json +1467 -9
  52. package/dist/ai/composables/useConfirm.md +5 -2
  53. package/dist/ai/composables/useToast.md +4 -2
  54. package/dist/ai/guides/agent-contract.md +7 -4
  55. package/dist/ai/guides/ai-router.md +37 -22
  56. package/dist/ai/guides/component-picker.md +23 -18
  57. package/dist/ai/guides/design-language.md +141 -13
  58. package/dist/ai/guides/design-tokens.md +29 -4
  59. package/dist/ai/guides/icons-policy.md +46 -21
  60. package/dist/ai/guides/installation-and-plugin-setup.md +253 -17
  61. package/dist/ai/guides/migration/components/bb-rating.md +26 -1
  62. package/dist/ai/guides/migration/components/bb-tooltip.md +18 -0
  63. package/dist/ai/guides/migration/v2-to-v3.md +50 -23
  64. package/dist/ai/index.md +1 -1
  65. package/dist/ai/recipes/inertia/approvals-inbox.md +3 -3
  66. package/dist/ai/recipes/inertia/inline-edit-workspace.md +2 -2
  67. package/dist/ai/recipes/inertia/onboarding.md +2 -2
  68. package/dist/ai/recipes/inertia/ownership-atlas.md +6 -6
  69. package/dist/ai/recipes/inertia/record-form.md +5 -5
  70. package/dist/ai/recipes/inertia/records-workspace.md +4 -4
  71. package/dist/ai/recipes/inertia/upload-center.md +6 -6
  72. package/dist/ai/recipes/inertia/wizard-form.md +5 -5
  73. package/dist/ai/recipes/nuxt/approvals-inbox.md +3 -3
  74. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +2 -2
  75. package/dist/ai/recipes/nuxt/onboarding.md +2 -2
  76. package/dist/ai/recipes/nuxt/record-form.md +5 -5
  77. package/dist/ai/recipes/nuxt/records-workspace.md +4 -4
  78. package/dist/ai/recipes/nuxt/upload-center.md +6 -6
  79. package/dist/ai/recipes/nuxt/wizard-form.md +5 -5
  80. package/dist/ai/recipes/vue/approvals-inbox.md +3 -3
  81. package/dist/ai/recipes/vue/inline-edit-workspace.md +2 -2
  82. package/dist/ai/recipes/vue/onboarding.md +2 -2
  83. package/dist/ai/recipes/vue/record-form.md +10 -10
  84. package/dist/ai/recipes/vue/records-workspace.md +4 -4
  85. package/dist/ai/recipes/vue/upload-center.md +6 -6
  86. package/dist/ai/recipes/vue/wizard-form.md +5 -5
  87. package/dist/ai/source/BbBadge.md +1 -1
  88. package/dist/ai/source/BbBaseCheckbox.md +1 -1
  89. package/dist/ai/source/BbBaseDatePicker.md +5 -5
  90. package/dist/ai/source/BbBaseDatePickerInput.md +2 -2
  91. package/dist/ai/source/BbBaseRadio.md +1 -1
  92. package/dist/ai/source/BbBaseRating.md +5 -5
  93. package/dist/ai/source/BbBaseSelect.md +1 -1
  94. package/dist/ai/source/BbBaseSlider.md +5 -5
  95. package/dist/ai/source/BbBaseSwitch.md +1 -1
  96. package/dist/ai/source/BbBaseTimePickerInput.md +1 -1
  97. package/dist/ai/source/BbBreadcrumbs.md +20 -1
  98. package/dist/ai/source/BbButton.md +2 -2
  99. package/dist/ai/source/BbDialog.md +39 -8
  100. package/dist/ai/source/BbDropdown.md +1 -1
  101. package/dist/ai/source/BbDropzone.md +5 -5
  102. package/dist/ai/source/BbIcon.md +23 -10
  103. package/dist/ai/source/BbOffCanvas.md +50 -28
  104. package/dist/ai/source/BbPagination.md +2 -2
  105. package/dist/ai/source/BbPopover.md +12 -1
  106. package/dist/ai/source/BbSelectPopover.md +45 -7
  107. package/dist/ai/source/BbSpinner.md +2 -2
  108. package/dist/ai/source/BbTable.md +11 -11
  109. package/dist/ai/source/BbTooltip.md +57 -10
  110. package/dist/ai/source/ChipsBox.md +1 -1
  111. package/dist/ai/source/CommaBox.md +1 -1
  112. package/dist/ai/source/GuidesComponent.md +14 -7
  113. package/dist/ai/source/OptionsContainer.md +1 -1
  114. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +1 -1
  115. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +1 -1
  116. package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
  117. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +4 -4
  118. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +1 -1
  119. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +1 -1
  120. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +1 -1
  121. package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +1 -1
  122. package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +3 -3
  123. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  124. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +3 -3
  125. package/dist/components/BbBaseSwitch/BbBaseSwitch.vue_vue_type_script_setup_true_lang.js +1 -1
  126. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +1 -1
  127. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +71 -71
  128. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +2 -2
  129. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +102 -97
  130. package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +1 -1
  131. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +58 -58
  132. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +2 -2
  133. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +33 -35
  134. package/dist/components/BbIcon/iconCache.d.ts +30 -0
  135. package/dist/components/BbIcon/iconCache.js +21 -3
  136. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +57 -56
  137. package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +2 -2
  138. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +138 -138
  139. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +43 -43
  140. package/dist/components/BbSpinner/BbSpinner.vue_vue_type_script_setup_true_lang.js +1 -1
  141. package/dist/components/BbSpinner/types.d.ts +1 -1
  142. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +8 -8
  143. package/dist/components/BbTable/BbTableDataRow.js +2 -2
  144. package/dist/components/BbToast/BbToastRegion.vue_vue_type_script_setup_true_lang.js +62 -62
  145. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +81 -78
  146. package/dist/components/BbTooltip/types.d.ts +8 -1
  147. package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +1 -1
  148. package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +1 -1
  149. package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +1 -1
  150. package/dist/composables/useConfirm.d.ts +22 -0
  151. package/dist/composables/useConfirm.js +43 -40
  152. package/dist/composables/useSafeAreaInternal.js +1 -1
  153. package/dist/composables/useToast.d.ts +16 -0
  154. package/dist/composables/useToast.js +20 -16
  155. package/dist/composables/useViewportWidth.d.ts +20 -6
  156. package/dist/composables/useViewportWidth.js +8 -7
  157. package/dist/icon-scan.d.ts +276 -0
  158. package/dist/icon-scan.js +485 -0
  159. package/dist/icons-whitelist.d.ts +5 -4
  160. package/dist/llms-full.txt +1979 -1527
  161. package/dist/llms-medium.txt +461 -74
  162. package/dist/llms.txt +1 -1
  163. package/dist/nuxt.js +10 -7
  164. package/dist/plugin.js +1 -0
  165. package/dist/runtime/nuxt-plugin.js +2 -1
  166. package/dist/styles.css +1 -1
  167. package/dist/types/Config.d.ts +18 -0
  168. package/dist/utils/cssLayers.d.ts +91 -0
  169. package/dist/utils/cssLayers.js +43 -0
  170. package/dist/utils/injectLibraryStyles.d.ts +3 -1
  171. package/dist/utils/injectLibraryStyles.js +24 -23
  172. package/dist/vite-plugin.d.ts +74 -11
  173. package/dist/vite.js +292 -253
  174. package/llms.txt +1 -1
  175. package/package.json +7 -5
  176. package/scripts/lib/ensure-manifest.mjs +28 -5
  177. package/scripts/lib/eslint-plugin.d.ts +35 -0
  178. package/scripts/lib/eslint-plugin.mjs +299 -3
  179. package/scripts/lib/hand-roll-hints.mjs +50 -6
  180. package/scripts/lib/html-attributes.mjs +42 -0
  181. package/scripts/lib/mcp-config.mjs +38 -34
  182. package/scripts/lib/public-type-exports.mjs +246 -0
  183. package/scripts/lib/validate-bb-markup.mjs +205 -12
@@ -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 thissee
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
  );
@@ -109,7 +109,7 @@
109
109
  aria-expanded="true"
110
110
  :aria-invalid="computedAriaInvalid"
111
111
  :aria-label="searchInputAriaLabel"
112
- class="bb-select-popover__focus-proxy sr-only"
112
+ class="bb-select-popover__focus-proxy bb-sr-only"
113
113
  role="combobox"
114
114
  tabindex="-1"
115
115
  type="button"
@@ -642,6 +642,22 @@ const onOutsideInteraction = async (event: Event) => {
642
642
  const insideActivator = positionAnchorEl.value?.contains(target);
643
643
  const insidePopover = !!target.closest('.bb-common-popover');
644
644
  if (insideActivator || insidePopover) return;
645
+ // Focus landing on an ANCESTOR of the activator is a focus fallback, not
646
+ // the user going elsewhere. A mousedown on a non-focusable node inside a
647
+ // modal `<dialog>` — the field's own `<label>` — parks focus on the dialog
648
+ // element itself, which arrived here as an outside `focusin` and closed
649
+ // the panel one tick before the label's click reached the activator and
650
+ // reopened it: a close/open flash that a label outside any dialog never
651
+ // showed (focus falls to `body` there, which fires no `focusin`). Scoped
652
+ // to `focusin` on purpose: a CLICK whose target is that ancestor (the
653
+ // dialog's own padding) is a real outside click and must still close.
654
+ if (
655
+ event.type === 'focusin' &&
656
+ positionAnchorEl.value &&
657
+ target.contains(positionAnchorEl.value)
658
+ ) {
659
+ return;
660
+ }
645
661
  const label =
646
662
  target instanceof HTMLLabelElement ? target : target.closest('label');
647
663
  if (label?.control && positionAnchorEl.value?.contains(label.control)) return;
@@ -1231,8 +1247,22 @@ const open = async ({
1231
1247
  // Join the shared Escape stack only in flyout mode — the sheet returned
1232
1248
  // above and BbOffCanvas registers itself instead.
1233
1249
  pushEscapeEntry(escapeKey);
1234
- document.addEventListener('click', onOutsideInteraction);
1235
- document.addEventListener('focusin', onOutsideInteraction);
1250
+ // CAPTURE, not bubble. Every overlay root — a `<dialog>`, an off-canvas,
1251
+ // another popover — binds `overlaySurfaceIsolation`, which stops `click`
1252
+ // from bubbling past the surface so handlers BEHIND it never hear a click
1253
+ // aimed at it. A select declared INSIDE such a surface is behind nothing,
1254
+ // but its document listener sits above the boundary all the same: a click
1255
+ // on the dialog's own chrome was swallowed at the dialog and never
1256
+ // reached a bubble-phase watcher here, so it could not close the panel.
1257
+ // (It used to close by accident: mousedown parked focus on the dialog and
1258
+ // the `focusin` path treated that as leaving — see the ancestor guard.)
1259
+ // Capture runs before the boundary and before any consumer `.stop`, and
1260
+ // the inside/label guards in `onOutsideInteraction` already carry the
1261
+ // meaning, so nothing inside the select's own chrome is affected.
1262
+ document.addEventListener('click', onOutsideInteraction, { capture: true });
1263
+ document.addEventListener('focusin', onOutsideInteraction, {
1264
+ capture: true,
1265
+ });
1236
1266
  document.addEventListener('keydown', onEsc, { capture: true });
1237
1267
 
1238
1268
  if (!focusPopoverContent) return;
@@ -1259,8 +1289,12 @@ const close = async () => {
1259
1289
 
1260
1290
  pause();
1261
1291
  removeEscapeEntry(escapeKey);
1262
- document.removeEventListener('click', onOutsideInteraction);
1263
- document.removeEventListener('focusin', onOutsideInteraction);
1292
+ document.removeEventListener('click', onOutsideInteraction, {
1293
+ capture: true,
1294
+ });
1295
+ document.removeEventListener('focusin', onOutsideInteraction, {
1296
+ capture: true,
1297
+ });
1264
1298
  document.removeEventListener('keydown', onEsc, { capture: true });
1265
1299
  };
1266
1300
 
@@ -1575,8 +1609,12 @@ onBeforeUnmount(() => {
1575
1609
  // `shown` is a plain local ref that unmounting never resets, so the orphaned
1576
1610
  // capture-phase handler keeps passing its own guard and swallowing the key.
1577
1611
  removeEscapeEntry(escapeKey);
1578
- document.removeEventListener('click', onOutsideInteraction);
1579
- document.removeEventListener('focusin', onOutsideInteraction);
1612
+ document.removeEventListener('click', onOutsideInteraction, {
1613
+ capture: true,
1614
+ });
1615
+ document.removeEventListener('focusin', onOutsideInteraction, {
1616
+ capture: true,
1617
+ });
1580
1618
  document.removeEventListener('keydown', onEsc, { capture: true });
1581
1619
  });
1582
1620
  /**
@@ -8,7 +8,7 @@
8
8
  <template>
9
9
  <span v-if="label" role="status">
10
10
  <BbIcon class="bb-spinner" :icon="spinnerSvg" :size="size" />
11
- <span class="sr-only">{{ label }}</span>
11
+ <span class="bb-sr-only">{{ label }}</span>
12
12
  </span>
13
13
  <BbIcon v-else class="bb-spinner" :icon="spinnerSvg" :size="size" />
14
14
  </template>
@@ -47,7 +47,7 @@ export type Size = IconSize;
47
47
  export type BbSpinnerProps = Pick<BbIconProps, 'size'> & {
48
48
  /**
49
49
  * Accessible loading announcement. When set, the spinner is wrapped in a
50
- * `role="status"` element containing this text rendered `sr-only`, so
50
+ * `role="status"` element containing this text rendered `bb-sr-only`, so
51
51
  * screen readers announce the loading state. When omitted the markup is
52
52
  * unchanged and the spinner is purely decorative.
53
53
  */
@@ -16,7 +16,7 @@
16
16
  :inert="disabled"
17
17
  :style="containerStyle"
18
18
  >
19
- <legend v-if="selectable" class="sr-only">{{ legendText }}</legend>
19
+ <legend v-if="selectable" class="bb-sr-only">{{ legendText }}</legend>
20
20
  <!-- The caption lives OUTSIDE the table role (a `caption` role is invalid
21
21
  there for a div table) and names the table through `aria-labelledby`. -->
22
22
  <div
@@ -24,7 +24,7 @@
24
24
  :id="captionId"
25
25
  class="bb-table-caption"
26
26
  :class="{
27
- 'sr-only': !displayCaption,
27
+ 'bb-sr-only': !displayCaption,
28
28
  }"
29
29
  >
30
30
  {{ caption }}
@@ -40,7 +40,7 @@
40
40
  <div
41
41
  v-if="reorderable || resizable"
42
42
  :id="headerInstructionsId"
43
- class="sr-only"
43
+ class="bb-sr-only"
44
44
  >
45
45
  <template v-if="reorderable"
46
46
  >{{ t('table.reorderInstructions').value }}
@@ -63,14 +63,14 @@
63
63
  from outside `role="menu"`. -->
64
64
  <span
65
65
  aria-live="polite"
66
- class="bb-table-loading__text sr-only"
66
+ class="bb-table-loading__text bb-sr-only"
67
67
  role="status"
68
68
  >{{ loading ? loadingText || t('common.loadingText').value : '' }}</span
69
69
  >
70
70
  <template v-if="reorderable">
71
71
  <span
72
72
  aria-live="polite"
73
- class="bb-table__reorder-status sr-only"
73
+ class="bb-table__reorder-status bb-sr-only"
74
74
  role="status"
75
75
  >{{ columnOrderStatus }}</span
76
76
  >
@@ -175,13 +175,13 @@
175
175
  :name="`select_all_${name || ''}`"
176
176
  :readonly="readonly"
177
177
  />
178
- <span class="bb-table-check__label-text sr-only"
178
+ <span class="bb-table-check__label-text bb-sr-only"
179
179
  >{{ selectAllLabel || t('table.selectAllLabel').value }}
180
180
  </span>
181
181
  </label>
182
182
  <span
183
183
  v-else-if="!multiple"
184
- class="bb-table-radio__label-text sr-only"
184
+ class="bb-table-radio__label-text bb-sr-only"
185
185
  >{{ selectText || t('table.selectText').value }}</span
186
186
  >
187
187
  </span>
@@ -306,7 +306,7 @@
306
306
  name="header:actions"
307
307
  :text="actionsText || t('table.actionsText').value"
308
308
  >
309
- <span class="bb-table-header__content sr-only">
309
+ <span class="bb-table-header__content bb-sr-only">
310
310
  {{ actionsText || t('table.actionsText').value }}
311
311
  </span>
312
312
  </slot>
@@ -821,7 +821,7 @@ const {
821
821
  /** Id of the caption element the grid root is labelled by. */
822
822
  const captionId = computed(() => `${tableId.value}__caption`);
823
823
 
824
- /** Id of the sr-only keyboard instructions every data header is described by. */
824
+ /** Id of the bb-sr-only keyboard instructions every data header is described by. */
825
825
  const headerInstructionsId = computed(
826
826
  () => `${tableId.value}__header_instructions`
827
827
  );
@@ -6537,7 +6537,7 @@ export type BbTableSlots<Item = any> = {
6537
6537
 
6538
6538
  /* The label is an inline-block so the affixes keep their inline flow, and
6539
6539
  it ellipsizes within the cell; the select/actions headers keep their
6540
- sr-only / control content untouched. */
6540
+ bb-sr-only / control content untouched. */
6541
6541
  &-header:not(.bb-table-header--select):not(.bb-table-header--actions)
6542
6542
  > .bb-table-header__content {
6543
6543
  display: inline-block;
@@ -7115,7 +7115,7 @@ export type BbTableSlots<Item = any> = {
7115
7115
  dependency change (a filter, a poll, an autosave round-trip). Keep the rows
7116
7116
  on screen, mark them stale, and run an indeterminate bar along the header/body
7117
7117
  boundary. `aria-busy` on the root already carries the semantics and the
7118
- sr-only row in the body still announces, so the bar is decorative — hence a
7118
+ bb-sr-only row in the body still announces, so the bar is decorative — hence a
7119
7119
  pseudo-element rather than a node. `.bb-table--empty` is "nothing to keep". */
7120
7120
  .bb-table--loading:not(.bb-table--empty) {
7121
7121
  & .bb-table__body {
@@ -6,7 +6,7 @@
6
6
 
7
7
  ```vue
8
8
  <template>
9
- <span v-if="resolvedDescription" :id="descId" class="sr-only">{{
9
+ <span v-if="resolvedDescription" :id="descId" class="bb-sr-only">{{
10
10
  resolvedDescription
11
11
  }}</span>
12
12
 
@@ -48,7 +48,7 @@
48
48
  :id="id"
49
49
  class="bb-tooltip__content"
50
50
  role="tooltip"
51
- :style="{ width: parsedWidth }"
51
+ :style="{ '--max-w': parsedWidth }"
52
52
  >
53
53
  <slot>{{ text }}</slot>
54
54
  </span>
@@ -73,7 +73,6 @@ import { warnRemovedAttrs } from '@/utilities/functions/warnRemovedAttrs';
73
73
  import CommonPopover from '../CommonPopover/CommonPopover.vue';
74
74
  import { useElementSize } from '@vueuse/core';
75
75
  import { extractDomContainer } from '@/utilities/functions/extractDomContainer';
76
- import { parseWidthString } from '@/utilities/functions/parseWidthString';
77
76
  import type { BbTooltipProps, BbTooltipSlots } from './types';
78
77
  import { transitions } from '@/utilities/misc/transitions';
79
78
  export type { BbTooltipProps, BbTooltipSlots };
@@ -338,10 +337,14 @@ onBeforeUnmount(() => {
338
337
  document.removeEventListener('keydown', onEsc);
339
338
  });
340
339
 
340
+ /** A bare number, or a number written as a string — both mean pixels. */
341
+ const bareNumberPattern = /^-?\d*\.?\d+$/;
342
+ const percentPattern = /^(-?\d*\.?\d+)%$/;
343
+
341
344
  const percentWidth = computed(() => {
342
- if (!props.width) return undefined;
343
- const { value, unit } = parseWidthString(props.width);
344
- return unit === '%' ? value : undefined;
345
+ if (typeof props.width !== 'string') return undefined;
346
+ const match = props.width.trim().match(percentPattern);
347
+ return match ? parseFloat(match[1]) : undefined;
345
348
  });
346
349
 
347
350
  // Only `%` widths need the activator measured — skip the ResizeObserver for
@@ -353,13 +356,29 @@ const { width: activatorWidth } = useElementSize(
353
356
  box: 'border-box',
354
357
  }
355
358
  );
359
+
360
+ /**
361
+ * `width` resolved to the `--max-w` cap. `undefined` leaves the stylesheet's
362
+ * default measure in place, which is also what an unmeasured percentage falls
363
+ * back to — a `0px` cap would collapse the bubble on the first frame.
364
+ *
365
+ * A percentage is resolved here against the activator, because CSS would
366
+ * resolve it against the bubble's own containing block (the viewport) instead.
367
+ * Every other value is handed to CSS verbatim, so `calc()`, `min()` and the
368
+ * width keywords work and a typo styles nothing rather than throwing during
369
+ * render.
370
+ */
356
371
  const parsedWidth = computed(() => {
357
- if (!props.width) return undefined;
372
+ if (props.width == null || props.width === '') return undefined;
358
373
  if (percentWidth.value != null) {
359
- if (!activatorEl.value) return '0px';
374
+ if (!activatorWidth.value) return undefined;
360
375
  return `${activatorWidth.value * (percentWidth.value / 100)}px`;
361
376
  }
362
- return `${parseWidthString(props.width).value}px`;
377
+ if (typeof props.width === 'number') {
378
+ return Number.isFinite(props.width) ? `${props.width}px` : undefined;
379
+ }
380
+ const raw = props.width.trim();
381
+ return bareNumberPattern.test(raw) ? `${raw}px` : raw;
363
382
  });
364
383
  </script>
365
384
 
@@ -466,7 +485,14 @@ export type BbTooltipProps = {
466
485
  */
467
486
  delay?: number;
468
487
  /**
469
- * Width of the tooltip bubble in pixels, or as a percentage of the activator.
488
+ * Caps the width of the tooltip bubble: it wraps at this measure but still
489
+ * shrinks to its text, so a short hint never sits in an oversized bubble.
490
+ * A bare number is read as pixels, a percentage is measured against the
491
+ * activator, and any other CSS length (`'20rem'`, `'min(320px, 90vw)'`)
492
+ * is passed through.
493
+ *
494
+ * Replaces the default cap of `40ch`, which is also reachable in CSS as
495
+ * `--max-w` on the tooltip root.
470
496
  */
471
497
  width?: number | string;
472
498
  };
@@ -524,6 +550,16 @@ export type BbTooltipSlots = {
524
550
  --fg: var(--bb-primary-fg);
525
551
  --border-color: var(--bb-primary);
526
552
  --fs: calc(var(--bb-fs) - 2px);
553
+ /*
554
+ * Default measure. CommonPopover caps the bubble at the viewport minus its
555
+ * `padding`, which on a wide screen is a single line running the width of
556
+ * the page — unreadable, and the reason a hint needs a cap of its own.
557
+ * `ch` (not px) so the column tracks the font: the same 40ch measure the
558
+ * dropdown item description uses. The `width` prop overrides it by writing
559
+ * `--max-w` inline on the content; a consumer can override it in CSS on
560
+ * this root.
561
+ */
562
+ --max-w: 40ch;
527
563
  --px: 12px;
528
564
  --py: 6px;
529
565
 
@@ -553,6 +589,17 @@ export type BbTooltipSlots = {
553
589
  display: inline-flex;
554
590
  font-size: var(--fs);
555
591
  line-height: 1.4;
592
+ max-width: var(--max-w);
593
+ /* A capped bubble has to break what it cannot wrap: the hint a tooltip
594
+ most often carries is a file path or a URL, and a token longer than
595
+ the cap would otherwise run straight out of the painted bubble.
596
+ `anywhere`, not `break-word`: the text sits in an anonymous flex item
597
+ whose automatic minimum size is its min-content width, and only
598
+ `anywhere` lets a break point count toward min-content — under
599
+ `break-word` the item stays as wide as the longest token and
600
+ overflows the cap anyway. Max-content is untouched either way, so
601
+ ordinary prose still shrink-wraps to its natural width. */
602
+ overflow-wrap: anywhere;
556
603
  padding: var(--py) var(--px);
557
604
  }
558
605
  }
@@ -34,7 +34,7 @@
34
34
  widget role the input owns or controls, and the only fitting one,
35
35
  `role="option"`, has children-presentational semantics — it would erase each
36
36
  chip's ✕ button, a real tab stop, from the accessibility tree. -->
37
- <span class="sr-only" role="status">{{ highlightAnnouncement }}</span>
37
+ <span class="bb-sr-only" role="status">{{ highlightAnnouncement }}</span>
38
38
  </template>
39
39
 
40
40
  <script setup lang="ts">
@@ -22,7 +22,7 @@
22
22
  the only signal a highlight moved. This polite status node — mounted empty,
23
23
  because a live region added together with its content does not announce —
24
24
  says which item the cursor is on. -->
25
- <span class="sr-only" role="status">{{ highlightAnnouncement }}</span>
25
+ <span class="bb-sr-only" role="status">{{ highlightAnnouncement }}</span>
26
26
  </template>
27
27
 
28
28
  <script setup lang="ts">
@@ -250,6 +250,13 @@ watch(selectedExamplePath, (examplePath) => {
250
250
  </script>
251
251
 
252
252
  <style scoped lang="postcss">
253
+ /*
254
+ * Tailwind v4: an SFC \3c style> block is its own PostCSS pass, so `@apply`
255
+ * cannot see the theme or the class-based `dark` variant without this.
256
+ * Emits nothing itself — it only makes the utilities resolvable.
257
+ */
258
+ @reference '../../assets/css/index.css';
259
+
253
260
  .guides-component {
254
261
  @apply overflow-hidden border text-[color:var(--bb-text)];
255
262
  background: var(--bb-panel);
@@ -278,7 +285,7 @@ watch(selectedExamplePath, (examplePath) => {
278
285
  }
279
286
 
280
287
  .guides-component__code {
281
- @apply rounded px-1 py-0.5 font-mono text-[13px];
288
+ @apply rounded-sm px-1 py-0.5 font-mono text-[13px];
282
289
  background: var(--bb-muted);
283
290
  color: var(--bb-text);
284
291
  }
@@ -288,12 +295,12 @@ watch(selectedExamplePath, (examplePath) => {
288
295
  }
289
296
 
290
297
  .guides-component__select-label {
291
- @apply text-[11px] font-semibold uppercase tracking-[0.06em];
298
+ @apply text-[11px] font-semibold tracking-[0.06em] uppercase;
292
299
  color: var(--bb-text-muted);
293
300
  }
294
301
 
295
302
  .guides-component__select {
296
- @apply w-full px-3 py-2 text-sm outline-none;
303
+ @apply w-full px-3 py-2 text-sm outline-hidden;
297
304
  background: var(--bb-panel);
298
305
  border: var(--bb-border-w) solid var(--bb-border);
299
306
  border-radius: var(--bb-radius);
@@ -311,7 +318,7 @@ watch(selectedExamplePath, (examplePath) => {
311
318
  }
312
319
 
313
320
  .guides-component__tab {
314
- @apply whitespace-nowrap px-2.5 py-1.5 text-[13px] transition-colors;
321
+ @apply px-2.5 py-1.5 text-[13px] whitespace-nowrap transition-colors;
315
322
  border-radius: var(--bb-radius);
316
323
  color: var(--bb-text-muted);
317
324
  }
@@ -343,7 +350,7 @@ watch(selectedExamplePath, (examplePath) => {
343
350
  }
344
351
 
345
352
  .guides-component__preview-card {
346
- @apply border-b lg:border-b-0 lg:border-r;
353
+ @apply border-b lg:border-r lg:border-b-0;
347
354
  border-color: var(--bb-border);
348
355
  }
349
356
 
@@ -352,7 +359,7 @@ watch(selectedExamplePath, (examplePath) => {
352
359
  }
353
360
 
354
361
  .guides-component__preview-title {
355
- @apply text-[11px] font-semibold uppercase tracking-[0.06em];
362
+ @apply text-[11px] font-semibold tracking-[0.06em] uppercase;
356
363
  color: var(--bb-text-muted);
357
364
  }
358
365
 
@@ -369,7 +376,7 @@ watch(selectedExamplePath, (examplePath) => {
369
376
  }
370
377
 
371
378
  .guides-component__source {
372
- @apply m-0 max-h-[28rem] overflow-auto p-3 font-mono text-[13px] leading-relaxed;
379
+ @apply m-0 max-h-112 overflow-auto p-3 font-mono text-[13px] leading-relaxed;
373
380
  background: color-mix(in oklab, var(--bb-text) 90%, transparent);
374
381
  border-radius: var(--bb-radius);
375
382
  color: color-mix(in oklab, var(--bb-panel) 90%, white);
@@ -50,7 +50,7 @@
50
50
  <span
51
51
  :class="{
52
52
  [`${classPrefix}-option__text`]: true,
53
- 'sr-only': hideLabel,
53
+ 'bb-sr-only': hideLabel,
54
54
  }"
55
55
  ><slot
56
56
  :checked="!!option.selected"
@@ -4,7 +4,7 @@ import n from "../BbSpinner/BbSpinner.vue.js";
4
4
  import { useBadgeHost as r } from "./badgeButtonContext.js";
5
5
  import { Fragment as i, createBlock as a, createCommentVNode as o, createElementBlock as s, createElementVNode as c, defineComponent as l, openBlock as u, renderSlot as d, unref as f } from "vue";
6
6
  //#region src/components/BbBadge/BadgeBodyContent.vue?vue&type=script&setup=true&lang.ts
7
- var p = { class: "bb-badge__content sr-only" }, m = { class: "bb-badge__content" }, h = /*@__PURE__*/ l({
7
+ var p = { class: "bb-badge__content bb-sr-only" }, m = { class: "bb-badge__content" }, h = /*@__PURE__*/ l({
8
8
  __name: "BadgeBodyContent",
9
9
  setup(l) {
10
10
  let h = r() ?? e().throw("[BbBadge] internal body renderer mounted outside a badge — this is a bug.");
@@ -20,7 +20,7 @@ var O = ["id"], k = {
20
20
  "aria-labelledby"
21
21
  ], j = {
22
22
  key: 3,
23
- class: "sr-only"
23
+ class: "bb-sr-only"
24
24
  }, M = /*@__PURE__*/ v({
25
25
  __name: "BbBadge",
26
26
  props: {
@@ -78,7 +78,7 @@ var g = /*@__PURE__*/ s({
78
78
  autofocus: _.autofocus,
79
79
  "aria-readonly": _.readonly ? !0 : void 0,
80
80
  checked: S.value,
81
- class: "sr-only",
81
+ class: "bb-sr-only",
82
82
  disabled: _.disabled,
83
83
  id: _.id,
84
84
  indeterminate: _.indeterminate,
@@ -27,14 +27,14 @@ import Te from "dayjs/plugin/weekOfYear";
27
27
  //#region src/components/BbBaseDatePicker/BbBaseDatePicker.vue?vue&type=script&setup=true&lang.ts
28
28
  var Ee = {
29
29
  "aria-live": "polite",
30
- class: "sr-only"
30
+ class: "bb-sr-only"
31
31
  }, De = {
32
32
  "aria-live": "polite",
33
- class: "sr-only"
33
+ class: "bb-sr-only"
34
34
  }, Oe = { class: "bb-base-date-picker__body" }, ke = {
35
35
  key: 0,
36
36
  class: "bb-base-date-picker__controls"
37
- }, Ae = { class: "sr-only" }, je = { class: "bb-base-date-picker__nav-center" }, Me = { class: "sr-only" }, Ne = { class: "bb-base-date-picker__content" }, Pe = ["id"], w = /*@__PURE__*/ ge({
37
+ }, Ae = { class: "bb-sr-only" }, je = { class: "bb-base-date-picker__nav-center" }, Me = { class: "bb-sr-only" }, Ne = { class: "bb-base-date-picker__content" }, Pe = ["id"], w = /*@__PURE__*/ ge({
38
38
  __name: "BbBaseDatePicker",
39
39
  props: {
40
40
  disabled: { type: Boolean },
@@ -397,7 +397,7 @@ var Ee = {
397
397
  id: `${b(Le)}-status`,
398
398
  "aria-atomic": "true",
399
399
  "aria-live": "polite",
400
- class: "sr-only",
400
+ class: "bb-sr-only",
401
401
  role: "status"
402
402
  }, y(We.value), 9, Pe)
403
403
  ], 6));