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
@@ -50,7 +50,8 @@ pinned to the prerelease channel rather than following it.
50
50
  `locale` explicitly if the app is not Italian** — the plugin defaults to
51
51
  `'it'` and there is no build-time warning for leaving it unset.
52
52
  3. **Choose intent:**
53
- - Whole page / flow → hop to **Recipes** below (one platform file only).
53
+ - Whole page / flow → hop to **Recipes** below (one platform folder: the
54
+ layout scaffold plus your page recipe).
54
55
  - Single control → [Component picker](./component-picker.md), then open
55
56
  **one** `../<ComponentName>.md` contract (Usage & Guidelines first).
56
57
  4. **Never invent props.** Confirm names in that contract or in
@@ -64,16 +65,27 @@ Folders: `../recipes/vue/`, `../recipes/nuxt/`, `../recipes/inertia/`.
64
65
 
65
66
  Same page shapes on each platform; only the data/mutation layer differs.
66
67
 
67
- | Intent | Recipe file (kebab) |
68
- | ------------------------------------ | ----------------------------------------- |
69
- | Filterable listing + bulk/peek | `records-workspace.md` |
70
- | Create/edit form | `record-form.md` |
71
- | Inline-edit table / backlog | `inline-edit-workspace.md` |
72
- | Hierarchy drill-down (nested tables) | `ownership-atlas.md` (Inertia only today) |
73
- | Approvals / triage inbox | `approvals-inbox.md` |
74
- | Uploads + editable queue | `upload-center.md` |
75
- | Multi-step wizard | `wizard-form.md` |
76
- | Onboarding checklist | `onboarding.md` |
68
+ **`layout-scaffold.md` is a prerequisite, not an option.** It is the three
69
+ app-owned chrome files the peek-stack context, the `PageShell` frame and the
70
+ `FloatingBottomPanel` selection dock that every other recipe's
71
+ `@/components/layout/*` imports resolve to. Open it **with** your page recipe;
72
+ the pair counts as one hop.
73
+
74
+ | Intent | Recipe file (kebab) |
75
+ | ----------------------------------------- | ----------------------------------------- |
76
+ | App chrome every recipe imports (**1st**) | `layout-scaffold.md` |
77
+ | Filterable listing + bulk/peek | `records-workspace.md` |
78
+ | Create/edit form | `record-form.md` |
79
+ | Inline-edit table / backlog | `inline-edit-workspace.md` |
80
+ | Hierarchy drill-down (nested tables) | `ownership-atlas.md` (Inertia only today) |
81
+ | Approvals / triage inbox | `approvals-inbox.md` |
82
+ | Uploads + editable queue | `upload-center.md` |
83
+ | Multi-step wizard | `wizard-form.md` |
84
+ | Onboarding checklist | `onboarding.md` |
85
+ | ⌘K / Ctrl+K global command palette | `command-palette.md` |
86
+
87
+ That table is the complete set — every file in each platform folder appears in
88
+ it, so a directory listing (or the MCP `list_recipes`) can add nothing.
77
89
 
78
90
  No recipe for detail-with-tabs, settings, dashboards, or profile pages —
79
91
  compose those from the closest listing/form recipe plus the guides
@@ -83,17 +95,20 @@ compose those from the closest listing/form recipe plus the guides
83
95
 
84
96
  ## Playbooks (only when the pattern applies)
85
97
 
86
- | Need | Guide |
87
- | -------------------------------------- | ----------------------------------------------------- |
88
- | Tied selects / stale model cleanup | [coherence-playbook](./coherence-playbook.md) |
89
- | Async `items` providers / prefill | [fetch-items-playbook](./fetch-items-playbook.md) |
90
- | `item-text` / `item-value` / disabled | [options-items-playbook](./options-items-playbook.md) |
91
- | Table cell edit ownership | [inline-edit-playbook](./inline-edit-playbook.md) |
92
- | Table row → detail (identity cells) | [agent-contract](./agent-contract.md) § Inline Links |
93
- | Peek that drills into related records | [agent-contract](./agent-contract.md) § Consult-Stack |
94
- | Icons (`lucide:*`, never invent names) | [icons-policy](./icons-policy.md) |
95
- | Tokens / styling around components | [design-language](./design-language.md) |
96
- | Upgrading from bitboss-ui 2.x | [migration/v2-to-v3](./migration/v2-to-v3.md) |
98
+ | Need | Guide |
99
+ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | Tied selects / stale model cleanup | [coherence-playbook](./coherence-playbook.md) |
101
+ | Async `items` providers / prefill | [fetch-items-playbook](./fetch-items-playbook.md) |
102
+ | `item-text` / `item-value` / disabled | [options-items-playbook](./options-items-playbook.md) |
103
+ | Table cell edit ownership | [inline-edit-playbook](./inline-edit-playbook.md) |
104
+ | Table row → detail (identity cells) | [agent-contract](./agent-contract.md) § Inline Links |
105
+ | Peek that drills into related records | [agent-contract](./agent-contract.md) § Consult-Stack |
106
+ | Icons (`lucide:*`, never invent names) | [icons-policy](./icons-policy.md) |
107
+ | Tokens / styling around components | [design-language](./design-language.md) |
108
+ | Theming a whole app / retuning a component | [design-tokens](./design-tokens.md) — the two tiers, and why most restyling is a component token, not a `--bb-*` one |
109
+ | A Tailwind utility on a `Bb*` component does nothing, or half-applies | [installation](./installation-and-plugin-setup.md) § Cascade layers — v4 is layered, we are not by default. It is per PROPERTY: a utility loses only where we declare the same one, and a shorthand like `p-4` can half-apply. `cssLayers: TAILWIND_V4_LAYERS` in the build plugin |
110
+ | Upgrading from bitboss-ui 2.x | [migration/v2-to-v3](./migration/v2-to-v3.md) |
111
+ | Bumping between two v3 prereleases | `npx bitboss-ui changelog --since <your version>` — what BREAKS in the bump; then `check --manifest` for the files it breaks |
97
112
 
98
113
  ---
99
114
 
@@ -149,6 +164,8 @@ Use this contract before generating any UI code with `bitboss-ui`.
149
164
  1. Prefer `Bb*` components in consumer-facing code.
150
165
  2. Treat low-level primitives (`Base*`, `BbBase*`, `Common*`, listbox internals) as reference-only — **except `BbBaseButton`**, which is a first-class consumer component: the unstyled navigable primitive for any clickable surface (cards, rows, custom links) that isn't a variant-first `BbButton`.
151
166
  3. Use the other reference-only components only when the user explicitly asks for internal composition details.
167
+ 4. **Not every `Bb*` export is a component.** `BbTableColumn`, `BbDropdownItem`, `BbTabsItem` and `BbBreadcrumbsItem` are TYPES: they describe the objects you pass to a prop (`columns`, `items`), and they never render. Import one with `import type { … } from 'bitboss-ui'`; writing `<BbTableColumn>` is always wrong. The `types` array in `components.json` lists every one with the prop it belongs to, and `npx bitboss-ui check` says so by name if you slip.
168
+ 5. **Two dozen of those types are the ones to reach for.** Each carries a `usage` tier: `api` means it appears in a component's own props or events, so it is what you annotate the value you build — `get_component` returns the list for the component you asked about. `wrapper` (`BbButtonProps` / `BbButtonEvents` / `BbButtonSlots` and siblings) is only for wrapping one of our components in one of yours, and `advanced` is everything left over — contexts, directive values, config shapes, the `…VariantType` unions. The tier is decided by that one rule and nothing else, so do not sort by what a name sounds like: `AlertVariantRegistry` reads like a registry but is `api`, because `BbAlert`'s `variant` prop is typed by it. Only `api` entries carry `usedBy`; if a type has one, it is `api`. Types the library does NOT export are that way on purpose: a size or a trigger is a string literal you write directly (`size="lg"`), so there is no name to import and none is missing.
152
169
 
153
170
  ## Setup-First Rule
154
171
 
@@ -250,7 +267,7 @@ For components accepting `items`:
250
267
 
251
268
  ## Icons Rule
252
269
 
253
- External-provider-first: prefer iconify provider icons — default set `lucide:*` — sourced from `@iconify-json/*` packages the consumer installs separately (`npm i -D @iconify-json/lucide`). Use `local:*` SVGs only for brand-specific art placed in the configured `iconDir`. Only reference icons from installed providers or existing local files — **never invent icon names**. Full workflow: [Icons Policy](./icons-policy.md).
270
+ External-provider-first: prefer iconify provider icons — default set `lucide:*` — sourced from `@iconify-json/*` packages the consumer installs separately (`npm i -D @iconify-json/lucide`). Use `local:*` SVGs only for brand-specific art placed in the configured `iconDir`. Only reference icons from installed providers or existing local files — **never invent icon names**. Provider icons ship only when written as a `prefix:name` literal somewhere in the project (any file type, PHP included) or listed in the plugin's `iconWhitelist`; the same rule applies in dev, and a name that does not resolve renders a placeholder and logs an error instead of throwing. Full workflow: [Icons Policy](./icons-policy.md).
254
271
 
255
272
  ## Variants Rule
256
273
 
@@ -291,8 +308,9 @@ These overlays also return focus to their trigger element on close, by documente
291
308
 
292
309
  **Mount exactly one `<BbToast />` and one `<BbConfirm />` for the whole application, at the app root** — `App.vue` under Vue, `app.vue` under Nuxt, the `createInertiaApp` render root under Inertia. Never one per page, per route, per layout branch or per component; a page that calls `toast()` or `confirm()` mounts nothing.
293
310
 
294
- - **Zero hosts** — `toast(...)` enqueues silently and the message never expires (the dismiss timer lives inside the host); `await confirm(...)` never settles, so the caller hangs. Neither throws.
295
- - **Two hosts** — every toast and every dialog renders once per host: duplicated DOM, duplicated `role="status"` live regions, duplicated focus traps. `BbToast` warns in DEV; `BbConfirm` does not warn at all.
311
+ - **Zero hosts** — `toast(...)` enqueues and the message never expires (the dismiss timer lives inside the host); `await confirm(...)` never settles, so the caller hangs. Neither throws. Both warn once per process in DEV.
312
+ - **Two hosts** — every toast and every dialog renders once per host: duplicated DOM, duplicated `role="status"` live regions, duplicated focus traps. Both warn in DEV, naming the host count.
313
+ - **A host that mounts and then unmounts also warns** (since 2026-09-09). This is the failure mode of mounting a host anywhere but the app root: one in a layout works right up until a route renders outside that layout, and the promise then hangs forever. The warning is **deferred by one second**, not immediate, because a late-mounting host renders the pending dialog and settles the promise normally — so a host that unmounts and remounts across a route change is not a failure and stays quiet. What the warning means is "one second later, still nothing to render this".
296
314
 
297
315
  Standalone examples in this repo (`src/components/*/examples/*.vue`, stories, dev showcases) mount a host inline **because each one is a self-contained root with no application around it**. That is a property of the example harness, not a pattern to copy — in a real app the host is already mounted at the root, and the page must not mount a second one.
298
316
 
@@ -305,7 +323,7 @@ Boolean props default to `false` so the bare `<Comp foo>` shorthand turns the fe
305
323
 
306
324
  These are the only intentional default-`true` booleans; anywhere else, `true` default is a bug (`check-api` flags it).
307
325
 
308
- **Arrows are tooltip-only in v3.** `hideArrow` / `showArrow` / `arrowPadding` were removed from `BbPopover`, `BbSelectPopover`, `BbColorPalette`, and `BbDropdown` (leftover bindings land silently in `$attrs`). Only `BbTooltip` keeps `hideArrow` (default `false` arrow shown). See [bb-popover.md](./migration/components/bb-popover.md).
326
+ **Arrows are tooltip-only in v3.** `hideArrow` / `showArrow` / `arrowPadding` were removed from `BbPopover`, `BbSelectPopover`, `BbColorPalette`, and `BbDropdown` (leftover bindings land silently in `$attrs`). `BbTooltip` keeps **both** — `hideArrow` (default `false`, so the arrow shows) and `arrowPadding` (default `10`). See [bb-popover.md](./migration/components/bb-popover.md).
309
327
 
310
328
  ## Testing Against bitboss-ui
311
329
 
@@ -616,7 +634,7 @@ Two runtime options control CSS:
616
634
 
617
635
  Guaranteed order when injected: **reset → library styles → all other stylesheets**, so app-local CSS can always override the library.
618
636
 
619
- `resetCss` stays opt-in because Tailwind projects already ship Preflight — a second reset is usually unnecessary.
637
+ `resetCss` stays opt-in because Tailwind projects already ship Preflight — a second reset is usually unnecessary. A reset of SOME kind is a prerequisite of the components, not a nicety: every size in the component sheets is written for `border-box`, and native controls and fieldsets are expected to arrive without their browser chrome. Preflight provides all of that; on a page with neither Preflight nor this file, a selectable table's fieldset grows a groove border and a 36px cell renders 49px. `reset.css` is optional only in the sense that you may bring your own — the component test suite runs against this file, with no Tailwind on the page.
620
638
 
621
639
  **Tailwind v4 cascade layers vs. scoped `<style>`:** Tailwind v4's `@import
622
640
  'tailwindcss'` puts every utility class in `@layer utilities`. A Vue SFC's
@@ -685,23 +703,193 @@ Leave `resetCss` at `false` here — Preflight already reset everything.
685
703
  Nothing else about the plugin changes; `injectStyles` controls only whether the
686
704
  library injects its own `<style>`.
687
705
 
688
- **Tailwind v4 apps do not need this** — source order does not decide their ties
689
- at all. `@import 'tailwindcss'` puts Preflight in a real `@layer base` and
690
- utilities in `@layer utilities`, and layered rules lose to unlayered rules of
691
- equal specificity **regardless of source order**. `dist/styles.css` declares no
692
- `@layer`, so the library wins those ties wherever it is injected: leave
693
- auto-injection on. The same mechanic bites the other way, and silently — a
694
- utility cannot re-size a Bb component either. `<BbButton class="h-12 px-6">`
695
- loses to the library's own `.bb-button { height: var(--h); padding-left:
696
- var(--px); padding-right: var(--px) }` (equal specificity, unlayered), with no
697
- warning from anything. Size Bb components through their props and tokens, never
698
- utilities. (Same trap as the scoped-`<style>` case above, one level up.)
706
+ **Tailwind v4 apps do not need `injectStyles: false`** — source order does not
707
+ decide their ties at all. `@import 'tailwindcss'` puts Preflight in a real
708
+ `@layer base` and utilities in `@layer utilities`, and **a layered rule loses to
709
+ an unlayered one regardless of source order**. Leave auto-injection on and use
710
+ `cssLayers` below.
711
+
712
+ ### Cascade layers the switch that decides who wins a tie
713
+
714
+ The cascade weighs **layer before specificity**. Unlayered CSS beats every
715
+ layered rule, however specific the layered one is. That one fact means there is
716
+ no single correct default, so the option is opt-in and you tell the library
717
+ where it sits:
718
+
719
+ | your CSS | `cssLayers` | who wins a tie |
720
+ | -------------------------------------------- | ----------------------------------- | ---------------------------------------------- |
721
+ | no layers (plain CSS, Tailwind v3) | unset (default) | source order — library first, so your CSS wins |
722
+ | Tailwind v4, or any layered CSS | your full order, ours in it (below) | exactly what your array says |
723
+ | no layers, and you want to override anything | `['bitboss-ui']` | you do, always |
724
+
725
+ The value is always an array: your complete layer order with `bitboss-ui` in
726
+ it, and `bitboss-ui-reset` immediately before it **when `resetCss` is on**.
727
+ With the reset off you may list `bitboss-ui-reset` anyway — an unused layer is
728
+ just empty — so the constant below works in every project, and you do not need
729
+ to know that Tailwind carries a reset of its own to use it.
730
+
731
+ **We do not ask you to add an `@layer` line to your stylesheet.** The plugin
732
+ injects the declaration ahead of its own rules, and a layer's position is fixed
733
+ by its first mention — so the order you configure is the order you get.
734
+
735
+ #### Tailwind v4
736
+
737
+ ```ts
738
+ // vite.config.ts — or the `bitboss` key in nuxt.config.ts
739
+ import { bitbossUi, TAILWIND_V4_LAYERS } from 'bitboss-ui/vite';
740
+
741
+ bitbossUi({
742
+ iconDir: './assets/icons',
743
+ cssLayers: TAILWIND_V4_LAYERS,
744
+ });
745
+ ```
746
+
747
+ `TAILWIND_V4_LAYERS` is
748
+ `['theme', 'base', 'bitboss-ui-reset', 'bitboss-ui', 'components', 'utilities']`:
749
+ Tailwind's own order with ours **after `base`**, so Preflight cannot outrank
750
+ the components, and **before `utilities`**, so `<BbButton class="px-6">`
751
+ works. Prefer the constant to the literal — a six-item array that has to be
752
+ exactly right is the kind of thing a typo turns into a silent loss of every
753
+ tie.
754
+
755
+ #### Tailwind v3, or plain CSS
756
+
757
+ Leave `cssLayers` unset. v3 emits unlayered CSS, and two unlayered stylesheets
758
+ are decided by source order — which auto-injection already puts in your favour.
759
+
760
+ #### Tailwind v3 — making utilities win anyway (opt-in)
761
+
762
+ v3 has no native layers of its own, but you can put its output into real ones
763
+ by wrapping the directives yourself. This is the shape PrimeVue documents, and
764
+ it is the only way to get v4's behaviour on v3:
765
+
766
+ ```css
767
+ /* your Tailwind entry, e.g. app.css */
768
+ @layer tailwind-base, bitboss-ui-reset, bitboss-ui, tailwind-utilities;
769
+
770
+ @layer tailwind-base {
771
+ @tailwind base;
772
+ }
773
+
774
+ @layer tailwind-utilities {
775
+ @tailwind components;
776
+ @tailwind utilities;
777
+ }
778
+ ```
779
+
780
+ ```ts
781
+ bitbossUi({
782
+ iconDir: './assets/icons',
783
+ cssLayers: [
784
+ 'tailwind-base',
785
+ 'bitboss-ui-reset',
786
+ 'bitboss-ui',
787
+ 'tailwind-utilities',
788
+ ],
789
+ });
790
+ ```
791
+
792
+ Both halves are required and they must agree: the CSS puts Tailwind's expanded
793
+ output INTO the layers, and `cssLayers` puts us between them. Verified against
794
+ a real v3 build — `.p-4` on a `Bb*` component wins, and Preflight no longer
795
+ outranks the components.
796
+
797
+ This is the one case where you write an `@layer` line yourself, because the
798
+ line has to wrap YOUR directives and we cannot reach them. Ours is emitted as
799
+ usual; the two declarations name the same order, and a layer's position is
800
+ fixed by its first mention, so they agree rather than fight.
801
+
802
+ #### "I just want to override things and not think about it"
803
+
804
+ ```ts
805
+ bitbossUi({
806
+ iconDir: './assets/icons',
807
+ cssLayers: ['bitboss-ui'], // ['bitboss-ui-reset', 'bitboss-ui'] with resetCss: true
808
+ });
809
+ ```
810
+
811
+ Declares our layer(s) and nothing else. All of your unlayered CSS then wins,
812
+ whatever its specificity.
813
+
814
+ > **Do not use this if your app loads a baseline stylesheet.** A reset is
815
+ > unlayered CSS made of element selectors, which is the exact combination this
816
+ > value inverts: `normalize.css`, your own `baseline.css`, or **Tailwind v3's
817
+ > Preflight** stop losing to our components on specificity and start outranking
818
+ > them outright. Measured on a real v3 build, a component rule of
819
+ > `margin: 5px` renders as `0px` — Preflight's `margin: 0` wins. The library
820
+ > does not look "overridable", it looks broken: controls lose their height,
821
+ > padding and borders, and nothing warns. If you want utilities to win on v3,
822
+ > use the layer hack above, which keeps the reset below us where it belongs.
823
+
824
+ Two further consequences: your global element rules (`button {}`, `input {}`)
825
+ now outrank component internals too — and this is the **wrong** value for a
826
+ layered app, because your own layers are declared later and land above ours,
827
+ Preflight included. A Tailwind v4 app uses the array above.
828
+
829
+ #### What "the utility loses" actually means
830
+
831
+ Not "utilities stop working on `Bb*` components". The cascade picks a winner
832
+ **per property**, so a layered utility loses only where we declare that exact
833
+ property on that element. Anything we leave alone is uncontested and simply
834
+ applies. Measured against the shipped sheet, v4 with `cssLayers` unset:
835
+
836
+ | on a `BbButton` | outcome | why |
837
+ | --------------- | ---------------- | ------------------------------------- |
838
+ | `w-20` | applies | we never declare `width` |
839
+ | `h-12` | applies | we declare `min-height`, not `height` |
840
+ | `m-4` | applies | we never declare `margin` |
841
+ | `p-4` | **half applies** | top/bottom land, left/right lose |
842
+ | `px-6` | no effect | we declare both padding longhands |
843
+ | `rounded-lg` | no effect | we declare `border-radius` |
844
+ | `text-lg` | no effect | we declare `font-size` |
845
+ | `gap-2` | no effect | we declare `gap` |
846
+
847
+ **The shorthand row is the one that hurts.** A clean loss is legible: you write
848
+ `px-6`, nothing moves, you go looking. `p-4` expands to four longhands, we
849
+ declare only the horizontal two, so their vertical padding applies while our
850
+ horizontal survives — a component nobody designed, with no signal that anything
851
+ was overridden.
852
+
853
+ So `cssLayers` does not turn utilities from broken into working. It turns them
854
+ from a per-property lottery into "always wins". **Predictability is the
855
+ feature, not capability** — and it is why the fix is worth taking even in an app
856
+ where the handful of utilities you tried happened to land.
857
+
858
+ #### Symptoms and what they mean
859
+
860
+ | what you see | cause | fix |
861
+ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------- |
862
+ | a Tailwind utility on a `Bb*` component does nothing, or half-applies | v4 utilities are layered, the library is not — see the per-property table above | `cssLayers: TAILWIND_V4_LAYERS` |
863
+ | every component lost its background / border | you listed only our two layers in a layered (v4) app | `cssLayers: TAILWIND_V4_LAYERS` |
864
+ | your global `button {}` reset suddenly restyles component internals | you listed only our two layers in an unlayered app | that is what it does — use the full order, or unset |
865
+ | components lost their height / padding / borders on **v3** | you listed only our layers while a reset or Preflight is unlayered | use the v3 layer hack, or leave `cssLayers` unset |
866
+ | nothing changed after setting `cssLayers` | the option lives in the **build plugin**, not in `app.use()` | set it in `vite.config.ts` / `nuxt.config.ts` |
867
+ | build fails naming a layer | your array omits `bitboss-ui` (or the reset layer with `resetCss: true`) | the error prints a working array — copy it |
868
+
869
+ A malformed value **fails the build**, never the browser: a wrong layer order is
870
+ invisible at runtime, because the CSS still parses and merely loses every tie.
871
+
872
+ #### The one thing layers do not fix
873
+
874
+ Layers decide ties between rules. They do not help when a component's own
875
+ inline `style` sets a property, because inline styles beat every layer. Where a
876
+ component needs to be positioned or sized by its parent, it exposes a CSS
877
+ variable, and that hook works in every configuration:
878
+
879
+ ```html
880
+ <!-- takes the row's slack; works on v3, v4, layered or not -->
881
+ <BbBreadcrumbs class="min-w-0" style="--flex-grow: 1" :items="crumbs" />
882
+ ```
883
+
884
+ Reach for the documented variable before reaching for a utility.
699
885
 
700
886
  ## 6) Icons
701
887
 
702
- - `iconDir` (required) is scanned recursively for `.svg` files; each file becomes `local:<basename>`.
703
- - Provider icons (`lucide:pencil`, `mdi:home`, …) require the matching `@iconify-json/<prefix>` package installed in the consumer app. In dev, every `prefix:name` the registry does not know produces a Vite error overlay with the fix: a mistyped name inside an installed set gets a "did you mean …?" hint, and an uninstalled prefix gets the list of installed sets plus the `npm install -D @iconify-json/<prefix>` command (the optional `@iconify/collections` peer only sharpens that wording, see [Install](#1-install)). A missing `local:` icon is a missing file in `iconDir` and surfaces as `BbIcon`'s runtime error naming it. Production builds report none of this the scan simply drops what does not resolve, so a wrong name fails at runtime there.
704
- - Production builds bundle **only statically referenced** provider icons write icon names as literal strings (`icon="lucide:trash-2"`), never assemble them dynamically.
888
+ - `iconDir` (required) is scanned recursively for `.svg` files; each file becomes `local:<basename>`. **`local:` ships everything.**
889
+ - Provider icons (`lucide:pencil`, `mdi:home`, …) require the matching `@iconify-json/<prefix>` package installed in the consumer app. **Providers ship only what is scanned or whitelisted**: the plugin reads the whole project as bytes every file under the Vite root, PHP and Blade and JSON and SQL included for `<prefix>:<name>` literals of the installed sets, and emits one lazy chunk per name that resolves. Skipped: gitignored paths (every `.gitignore` level), `node_modules`, `vendor`, `storage`, the build `outDir`, hidden paths, binaries, and `iconsScanner.exclude` globs; `iconsScanner.include` adds locations. The scan is cached by content hash under `node_modules/.cache/bitboss-ui/`, runs on 2–4 worker threads once a project passes ~1500 files (a cold scan is disk-bound, and the threads let a slow disk serve reads in parallel — roughly 0.2 s for 10k files on a laptop), prints one line (file count, elapsed, threads) after a cold scan, and stays silent when warm.
890
+ - **This holds in dev exactly as in prod.** Dev registers the same scanned-plus-whitelisted set, re-scans a file on every save, and hot-updates the registry — a new literal renders without a restart, and a name that is neither scanned nor whitelisted fails on first render in dev instead of after deploy.
891
+ - **Names the scanner cannot see** — composed at runtime, stored in a database, chosen in an admin UI — go in `iconWhitelist` (exact names or globs). An entry that resolves in no installed set fails the build with the actionable message.
892
+ - **A name that does not resolve renders the built-in placeholder** (a question mark in a circle, sized and coloured like the icon it replaces, with `data-bb-icon-missing="<name>"`) and logs one `console.error` per name per page load, in both environments; dev also raises the Vite error overlay. The message is the fix: a mistyped name inside an installed set gets a "did you mean …?" hint, a real icon nobody wrote as a literal is told to use `iconWhitelist`, and an uninstalled prefix gets the list of installed sets plus the `npm install -D @iconify-json/<prefix>` command (the optional `@iconify/collections` peer only sharpens that wording, see [Install](#1-install)). A missing `local:` icon names the `.svg` that is not in `iconDir`. `BbIcon` never throws for a missing name, so the surrounding tree stays mounted.
705
893
  - The plugin also writes `node_modules/.bitboss-ui/local-icons.json` and (in dev, unless `vscodeSettings: false`) wires `.vscode/settings.json` so the Iconify editor extension previews local icons.
706
894
 
707
895
  Policy for choosing icons: [Icons Policy](./icons-policy.md).
@@ -712,7 +900,37 @@ Options are the same for `bitboss-ui/vite` and the `bitboss` key in Nuxt.
712
900
 
713
901
  ### `iconDir: string` (required)
714
902
 
715
- Path to local SVG icons, absolute or relative to project root. Scanned recursively; file basename (without extension) becomes the icon name, addressable as `local:<name>`.
903
+ Path to local SVG icons, absolute or relative to project root. Scanned recursively; file basename (without extension) becomes the icon name, addressable as `local:<name>`. Every file in it ships.
904
+
905
+ ### `iconsScanner?: { include?: string[]; exclude?: string[]; maxFileSize?: number }`
906
+
907
+ Tunes the provider-icon source scan (see [Icons](#6-icons)). All three are optional and additive to the defaults; the root is always scanned.
908
+
909
+ - `include` — extra paths or globs scanned **in addition** to the Vite root: a vendor package that carries icon names, a directory outside the root (`'../shared/ui'`, `'vendor/acme/ui-kit/**/*.php'`). Relative entries resolve against the root. An entry is explicit, so the built-in skips (`vendor`, hidden segments, `.gitignore` files above it) do not apply to the entry itself — only to what lies beneath it. The Nuxt module adds the project `rootDir` here automatically, because Nuxt roots Vite at `app/`.
910
+ - `exclude` — globs removed from the scan on top of the defaults, in gitignore syntax, relative to the root (`['resources/legacy/**', '**/*.sql']`).
911
+ - `maxFileSize` — bytes. Exists only so a database dump is not read into memory; the default is 256 MiB, so a real source file is never skipped for its size.
912
+
913
+ ```ts
914
+ bitbossUi({
915
+ iconDir: './resources/icons',
916
+ iconsScanner: {
917
+ include: ['vendor/acme/ui-kit/src'],
918
+ exclude: ['database/dumps/**'],
919
+ },
920
+ });
921
+ ```
922
+
923
+ ### `iconWhitelist?: string[]`
924
+
925
+ Provider icons that **always** ship, scanned or not — exact names (`'tabler:crown'`) and globs (`'tabler:calendar-*'`, expanded against the installed set). This is the home of every name the scanner cannot see as a literal: composed at runtime, read from a database, picked by an admin. An entry that resolves in no installed set — a typo, an uninstalled prefix, a glob matching nothing — **fails the build** with the same actionable message the dev overlay shows for a missing icon. `local:` names never belong here (they all ship already).
926
+
927
+ ```ts
928
+ bitbossUi({
929
+ iconDir: './resources/icons',
930
+ // `icon` is stored per category in the database; the admin picks from these.
931
+ iconWhitelist: ['tabler:category', 'tabler:tag', 'tabler:calendar-*'],
932
+ });
933
+ ```
716
934
 
717
935
  ### `injectStyles?: boolean` (default: `true`)
718
936
 
@@ -722,6 +940,28 @@ Auto-inject the library stylesheet at runtime. If `false`, you must `import 'bit
722
940
 
723
941
  Inject the reset stylesheet (always ordered before the library stylesheet). Keep `false` and import `bitboss-ui/reset.css` if you prefer explicit control.
724
942
 
943
+ ### `cssLayers?: string[]` (default: unset)
944
+
945
+ Wraps the library's stylesheets in CSS cascade layers and declares where they
946
+ sit. Opt-in. Full guide with copy-paste blocks in §5 → _Cascade layers_.
947
+
948
+ The value is your app's **complete** layer order with `bitboss-ui` in it, and
949
+ `bitboss-ui-reset` before it when `resetCss` is on (listing it anyway is fine).
950
+ Unset means unlayered, today's behaviour, right for plain CSS and Tailwind v3.
951
+
952
+ ```ts
953
+ import { bitbossUi, TAILWIND_V4_LAYERS } from 'bitboss-ui/vite';
954
+
955
+ // Tailwind v4 — ['theme', 'base', 'bitboss-ui-reset', 'bitboss-ui', 'components', 'utilities']
956
+ cssLayers: TAILWIND_V4_LAYERS;
957
+ // no layers of your own, and you want to override anything
958
+ cssLayers: ['bitboss-ui']; // ['bitboss-ui-reset', 'bitboss-ui'] with resetCss: true
959
+ ```
960
+
961
+ You do not add an `@layer` line to your own CSS — the plugin emits the
962
+ declaration ahead of its rules. An invalid array, or a boolean, fails the build
963
+ with a message containing a working replacement.
964
+
725
965
  ### `injectDirectives?: boolean` (default: `true`)
726
966
 
727
967
  Registers the global `v-bb-tooltip`, `v-bb-dropdown`, `v-bb-color`, `v-bb-date` and `v-bb-time` directives when the runtime plugin installs — all five, on both the Vue and the Nuxt runtime. Each registration is skipped if the app already declared a directive under that name, so disable the option only if you want none of them.
@@ -929,6 +1169,39 @@ says this in its header.
929
1169
  | `dropdownItemVariants` | `default, destructive` | `bb-dropdown__item--variant-<name>` |
930
1170
  | `tooltipVariants` | `default, destructive` | `bb-tooltip--<name>` |
931
1171
  | `toastVariants` | `default, success, info, warning, destructive` | `bb-toast-message--<name>` |
1172
+ | `inputVariants` | `outline, secondary, ghost` | `common-input-wrapper--<name>` |
1173
+
1174
+ `inputVariants` is the `variant` prop of the eight text-like controls
1175
+ (`BbTextInput`, `BbTextarea`, `BbNumberInput`, `BbSelect`, `BbTag`,
1176
+ `BbDatePickerInput`, `BbTimePickerInput`, `BbColorInput`). Its class hook is
1177
+ the shared field box, and a custom entry is a **token block, not a rule set**:
1178
+ the box's geometry (height, padding, border width) never changes with the
1179
+ variant, so a variant only assigns the look variables the box reads —
1180
+
1181
+ ```css
1182
+ /* a registered `inputVariants: ['brand']` */
1183
+ .common-input-wrapper--brand {
1184
+ --bg: color-mix(in oklab, var(--bb-primary) 8%, var(--bb-panel));
1185
+ --bg-hover: color-mix(in oklab, var(--bb-primary) 12%, var(--bb-panel));
1186
+ --bg-disabled: var(--bb-muted);
1187
+ /* read `--state-border` to tint under errors/warnings like `outline`;
1188
+ leave it out to stay untinted like `ghost` */
1189
+ --border-color: var(
1190
+ --state-border,
1191
+ color-mix(in oklab, var(--bb-primary) 40%, var(--bb-border))
1192
+ );
1193
+ --border-focus: var(--state-border, var(--bb-primary));
1194
+ /* the focus ring: leave this out and the variant has none, like `none` */
1195
+ --ring-size: var(--bb-ring-size);
1196
+ /* what the box paints under the ring (ghost/secondary use their hover wash) */
1197
+ --bg-focus: var(--bg-hover);
1198
+ }
1199
+ ```
1200
+
1201
+ The ring's **colour** is state-owned (primary, danger under errors, warn under
1202
+ warnings) and never a variant's business; whether a ring is **drawn** is the
1203
+ variant's call through `--ring-size` — the three built-ins draw it, `none` does
1204
+ not, and a custom variant chooses.
932
1205
 
933
1206
  Variant props are locked to the registered set (`keyof <X>VariantRegistry`):
934
1207
  an unregistered name is a compile error, not a silently unstyled render.
@@ -959,10 +1232,12 @@ when the contract cannot answer the question — the usual case is rebuilding
959
1232
  chrome around a control the library does not ship, since the class names such a
960
1233
  template emits are already styled by the public `bitboss-ui/styles.css`.
961
1234
 
962
- Add `--mcp` to also register a live MCP server (`npx bitboss-ui@<installed version> mcp`, pinned so npx can never fetch a different version from the registry) the harness launches on demand — it merges the entry (never overwriting other servers) into `.mcp.json` (Claude Code), `.cursor/mcp.json` (Cursor), `.vscode/mcp.json` (VS Code / Copilot), and `~/.codeium/windsurf/mcp_config.json` (Windsurf). **Windsurf's config is global** — it has no per-project scope, so registering it affects every project on the machine; that is why only the explicit `ai-init --mcp` command writes it, while the plugin's `mcp: true` dev-server flag (§7) auto-registers the project-scoped harnesses only. Install the server's optional peers first — `npm i -D @modelcontextprotocol/sdk zod` — they are not dependencies of the library (~12 MB used only by the MCP server); the command warns if they are absent. **`--mcp` is optional and skipping it loses you nothing but the transport** — the server only reads the `dist/ai/` files listed above, so run plain `ai-init` and let the agent read that directory if you cannot add the peers (see `mcp?: boolean` in §7).
1235
+ Add `--mcp` to also register a live MCP server (`node node_modules/bitboss-ui/bin/bitboss-ui.mjs mcp` — the installed binary, not `npx`, so it can never fetch from the registry, its optional peers resolve in your project rather than in an npx cache, and it cannot drift behind an upgrade) the harness launches on demand — it merges the entry (never overwriting other servers) into `.mcp.json` (Claude Code), `.cursor/mcp.json` (Cursor), `.vscode/mcp.json` (VS Code / Copilot), and `~/.codeium/windsurf/mcp_config.json` (Windsurf). **Windsurf's config is global** — it has no per-project scope, so registering it affects every project on the machine; that is why only the explicit `ai-init --mcp` command writes it, while the plugin's `mcp: true` dev-server flag (§7) auto-registers the project-scoped harnesses only. Install the server's optional peers first — `npm i -D @modelcontextprotocol/sdk zod` — they are not dependencies of the library (~12 MB used only by the MCP server); the command warns if they are absent. **`--mcp` is optional and skipping it loses you nothing but the transport** — the server only reads the `dist/ai/` files listed above, so run plain `ai-init` and let the agent read that directory if you cannot add the peers (see `mcp?: boolean` in §7).
963
1236
 
964
1237
  For a human-in-the-loop (and agent) safety net, add the `bitboss-ui/eslint-plugin` flat-config plugin (`...bitbossUi.configs.recommended`) so unknown/removed `Bb*` props surface as ESLint errors in the editor and CI — the same manifest checks as `bitboss-ui check`, reusing your `eslint-plugin-vue` parser. Real HTML attributes fall through untouched; escape hatches are the standard `<!-- eslint-disable-next-line bitboss-ui/no-unknown-attributes -->` and the rule's `allowAttributes` / `allowComponents` options. **`eslint --fix` also auto-migrates deprecated v2 props** (renames, boolean-polarity inversions like `allowWriting`→`disableWriting`, value remaps like BbToast `placement`→`position`, and deletion of inert removed props); structural migrations that need a slot/directive/CSS are reported but left by hand. One deliberate boundary: the rule's "did you mean `<prop>`?" hints for a mistyped/unknown attribute are advisory message text, not fixes — `eslint --fix` (including an unattended run in a pre-commit hook) never applies them; only the registered v2→v3 migrations above are auto-fixed. A guess applied blind across a codebase would be a silent behavior change, so acting on a hint is always a per-site decision. **This library is TS-first (recipes ship `<script setup lang="ts">`, some with `generic="T"`), so wire `@typescript-eslint/parser` for both `.ts` and `.vue` files** — without it `vue-eslint-parser` throws a `Parsing error` on those files, which means no rule (including `no-unknown-attributes`) runs on them at all. See the README "ESLint plugin" section for the full config snippet (both parser blocks + the `npm i -D @typescript-eslint/parser` line) and the fix table. `recommended` also enables `bitboss-ui/no-active-class-on-root-link`: `active-class` on a link-capable `Bb*` component (`BbButton`/`BbBaseButton`/`BbBadgeButton`) matches by path PREFIX, so a link whose target is the literal root (`href="/"` / `to="/"`) reads as "active" on every URL — the rule flags that combination and points you at `exact-active-class` instead.
965
1238
 
1239
+ Two rules in `recommended` are `warn` rather than `error`, because each describes markup that may be deliberate. `bitboss-ui/no-unknown-event` flags an `@event` the component does not emit (it falls through to the root element, so it may be a considered native listener). `bitboss-ui/no-unknown-token` flags a bare `var(--bb-…)` naming a token the installed package does not declare, in a `<style>` block or a static `style="…"` attribute — an undefined custom property is invalid-at-computed-value-time, so CSS discards the **entire declaration** and the rule silently does nothing at runtime. Supplying a fallback (`var(--bb-canvas, #f8fafc)`) is defined behaviour and is never reported; `allowTokens` exempts names you define yourself. The token list is read from the `dist/styles.css` you installed, so it cannot claim a token is invented when your version declares it, and with no stylesheet present the rule stays silent. Before reaching for a `--bb-*` name at all, re-read the two tiers above: a name the library does not declare usually means the knob you want is a **component** token, not a gap in the global set. `bitboss-ui check` carries two further advisory hints that are deliberately not ESLint rules — the hand-rolled avatar and spinner detectors — because both are guesses, and a lint rule that fires on correct code teaches people to ignore all of them.
1240
+
966
1241
  **The manifest ships with the package** — `dist/ai/components.json` is inside
967
1242
  the tarball, and the rules resolve it relative to their own file, so a normal
968
1243
  install needs no configuration. If it ever cannot be read (a broken install, a
@@ -971,6 +1246,18 @@ nothing), the rules cannot check anything: they now **warn once** saying they
971
1246
  are inactive rather than passing silently. Pass `failClosed: true` to a rule's
972
1247
  options if you would rather CI stop than under-check.
973
1248
 
1249
+ Alongside `components` the manifest carries a `types` array: the public TYPE
1250
+ exports (`BbTableColumn`, `BbDropdownItem`, …), each with the component and prop
1251
+ it is written on. That is how `check`, the ESLint rules and the MCP server tell
1252
+ a type used as a tag from a typo and from a component your app owns — three
1253
+ cases that used to share one message and one (usually wrong) remedy.
1254
+
1255
+ **`allowComponents` is a per-tag off switch, not the suppression of one
1256
+ message.** An allowlisted name is skipped whole: no unknown-component finding,
1257
+ and no prop, event, v-model or slot check on that tag, permanently. Right for an
1258
+ app-owned `<BbRichEditor>`; wrong for everything else — never reach for it to
1259
+ quieten a `Bb*` name the library itself exports.
1260
+
974
1261
  `recommended` also enables **`bitboss-ui/no-reverse-false`**, which covers the one v2 break a rename table cannot: `reverse` on `BbCheckbox` / `BbRadio` / `BbSwitch` kept its NAME and inverted its MEANING in v3, so the v2 spelling `:reverse="false"` (control on the right) is now a no-op that renders the control on the LEFT. Nothing else catches it — it is a declared prop, correctly typed, holding its own default value — and it cannot be expressed as a rename, because a rename entry is keyed by prop name and would rewrite the legitimate v3 spellings too. The rule flags only a LITERAL `:reverse="false"` (a dynamic `:reverse="isRtl"` that evaluates false is left alone) and offers both corrections as editor suggestions; pass `{ assumeV2: true }` to promote the v2-intent rewrite to a real `--fix` for a bulk sweep, then remove it — the two readings produce opposite layouts, so it must never be on by default. See [the migration guide](../guides/migration/components/bb-checkbox.md).
975
1262
 
976
1263
  It also enables `bitboss-ui/require-partner-prop`: an opt-in prop set without its documented required partner — BbTable `keyboard-navigation` / `v-model:highlighted` without `row-class` (the shipped tint says "current row"; `row-class` says why, and is how you override it), or `loading-text` without `:loading` bound — compiles and mounts clean and just does nothing; the rule names the missing partner and quotes why.
@@ -1224,24 +1511,29 @@ a composable you want is not in this corpus's `composables/` folder — it is no
1224
1511
  here.** Check this list before inventing a name; if the need is genuinely absent,
1225
1512
  compose it or write it yourself.
1226
1513
 
1227
- | Assumed component or composable | Reality |
1228
- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1229
- | `BbColumnManager` — show/hide/reorder columns | Does not exist as a component. Order is `v-model:order` on `BbTable` (an array of keys any panel can write; `reorderable` adds the built-in header drag + keyboard handle); visibility is `hidden: true` on the column definition (the column stays declared and keeps its slot in the order; header slots get a `hideColumn()` that fires `hide:column`). Compose the panel from `BbPopover` + `BbCheckbox`. |
1230
- | `BbKanban` / drag-and-drop board | Does not exist. There is no reusable DnD primitive in the library (`BbTable`'s header drag is internal to the table) — compose the board from `BbTable` (a table/list view of the same data) or hand-roll the drag-and-drop interaction yourself. |
1231
- | `BbModal` | It is [BbDialog](../BbDialog.md). |
1232
- | `BbDrawer` / `BbSheet` | It is [BbOffCanvas](../BbOffCanvas.md). |
1233
- | `BbCombobox` / `BbAutocomplete` | It is [BbSelect](../BbSelect.md) / [BbSelectPopover](../BbSelectPopover.md) — search is built in, see `disable-writing`. |
1234
- | `BbToggle` / `BbToggleGroup` | It is [BbButton](../BbButton.md) with `v-model` (+ `true-value` / `false-value`), which owns `aria-pressed` and the active class. |
1235
- | `TagChip` / colored, read-only tag chip | It is [BbBadge](../BbBadge.md) — [BbTag](../BbTag.md) is a tags-_input_ only, not a display component. Use `BbBadge` with a registered `soft-*` variant for a fixed palette, or a one-off `--bg`/`--color` override per row for arbitrary/data-driven colors (BbBadge.md § One-off colors) — not a registered variant per distinct color. |
1236
- | `BbTab` (singular) | The consumer API is [BbTabs](../BbTabs.md) / `BbTabsRoot`. `BbTab` appears only as the **v2 "before" side** of the migration diffs. |
1237
- | `BbCard` | Does not exist — a card is a `<div>` with tokens. See [design-language](./design-language.md). |
1238
- | `BbList` / `BbListItem` / `BbDescriptionList` | Do not exist — a list row is an `<li>` with tokens, and [BbBaseButton](../BbBaseButton.md) makes the whole row navigable. See the note under Data Display. |
1239
- | `useLiveFeed` / any polling or cross-tab feed helper | Does not exist, and neither does a cross-tab helper — `useBroadcastChannelInstance` was removed (cross-tab messaging is not a UI concern). Use `useBroadcastChannel` from `@vueuse/core`; the polling and the merge are yours. |
1240
- | `BbStepper` / `BbWizard` | Does not exist as a component; the wizard-form recipe composes one. |
1241
- | `BbSkeleton` (standalone) | Only `BbTable` ships skeletons, via the column `skeleton` field. |
1242
- | `BbCommand` / command palette | Does not exist. Compose `BbDialog` + `BbSelectPopover`. |
1243
- | `BbCalendar` (month / scheduling view) | Does not exist. [BbDatePicker](../BbDatePicker.md) is a picker, not a calendar surface. |
1244
- | `BbChart` / `BbGraph`, `BbCarousel`, `BbTimeline`, `BbDivider` | Do not exist. No charting, carousel or timeline primitive ships here. |
1514
+ | Assumed component or composable | Reality |
1515
+ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1516
+ | `BbColumnManager` — show/hide/reorder columns | Does not exist as a component. Order is `v-model:order` on `BbTable` (an array of keys any panel can write; `reorderable` adds the built-in header drag + keyboard handle); visibility is `hidden: true` on the column definition (the column stays declared and keeps its slot in the order; header slots get a `hideColumn()` that fires `hide:column`). Compose the panel from `BbPopover` + `BbCheckbox`. |
1517
+ | `BbKanban` / drag-and-drop board | Does not exist. There is no reusable DnD primitive in the library (`BbTable`'s header drag is internal to the table) — compose the board from `BbTable` (a table/list view of the same data) or hand-roll the drag-and-drop interaction yourself. |
1518
+ | `BbModal` | It is [BbDialog](../BbDialog.md). |
1519
+ | `BbDrawer` / `BbSheet` | It is [BbOffCanvas](../BbOffCanvas.md). |
1520
+ | `BbCombobox` / `BbAutocomplete` | It is [BbSelect](../BbSelect.md) / [BbSelectPopover](../BbSelectPopover.md) — search is built in, see `disable-writing`. |
1521
+ | `BbToggle` / `BbToggleGroup` | It is [BbButton](../BbButton.md) with `v-model` (+ `true-value` / `false-value`), which owns `aria-pressed` and the active class. |
1522
+ | `TagChip` / colored, read-only tag chip | It is [BbBadge](../BbBadge.md) — [BbTag](../BbTag.md) is a tags-_input_ only, not a display component. Use `BbBadge` with a registered `soft-*` variant for a fixed palette, or a one-off `--bg`/`--color` override per row for arbitrary/data-driven colors (BbBadge.md § One-off colors) — not a registered variant per distinct color. |
1523
+ | `BbTab` (singular) | The consumer API is [BbTabs](../BbTabs.md) / `BbTabsRoot`. `BbTab` appears only as the **v2 "before" side** of the migration diffs. |
1524
+ | `BbCard` | Does not exist — a card is a `<div>` with tokens. See [design-language](./design-language.md). |
1525
+ | `BbList` / `BbListItem` / `BbDescriptionList` | Do not exist — a list row is an `<li>` with tokens, and [BbBaseButton](../BbBaseButton.md) makes the whole row navigable. See the note under Data Display. |
1526
+ | `useLiveFeed` / any polling or cross-tab feed helper | Does not exist, and neither does a cross-tab helper — `useBroadcastChannelInstance` was removed (cross-tab messaging is not a UI concern). Use `useBroadcastChannel` from `@vueuse/core`; the polling and the merge are yours. |
1527
+ | `BbStepper` / `BbWizard` | Does not exist as a component; the [wizard-form recipe](../recipes/inertia/wizard-form.md) (also under `vue/`, `nuxt/`) composes one. |
1528
+ | `BbSkeleton` (standalone) | Only `BbTable` ships skeletons, via the column `skeleton` field. |
1529
+ | `BbCommand` / command palette | Does not exist. Compose `BbDialog` + `BbSelectPopover` — do not hand-roll the search: the [command-palette recipe](../recipes/inertia/command-palette.md) (also under `vue/`, `nuxt/`) already wires debouncing, race protection, grouping and keyboard nav. |
1530
+ | `BbCalendar` (month / scheduling view) | Does not exist. [BbDatePicker](../BbDatePicker.md) is a picker, not a calendar surface. |
1531
+ | `BbDataTable` / `BbDataGrid` / data grid | It is [BbTable](../BbTable.md) sorting, selection, column order/resize, virtual scrolling and inline edit all live there. |
1532
+ | `filter` / `filter-method` prop on BbTable, column filters | No `filter` prop, and its absence is not a missing feature. A filter is an input to the FETCH: put your filter state in `dependencies` and the provider refetches (debounced, value-hashed, races discarded). Per-column filter UI goes in `#header:<key>:append`, which keeps the built-in sort button. A whole filtered listing is the records-workspace recipe. Client-side over a plain array, use a computed: `:items="filteredItems"`. See [BbTable](../BbTable.md) § Filtering. |
1533
+ | `BbSnackbar` / `BbNotification` / flash message | It is [BbToast](../BbToast.md), raised through the `useToast` composable. |
1534
+ | `BbRichTextEditor` / `BbEditor` / WYSIWYG | Does not exist, and no rich-text or markdown editor ships here. [BbTextarea](../BbTextarea.md) is plain text only; bring your own editor. |
1535
+ | `BbEmptyState` / `BbPlaceholder` / no-results panel | Does not exist as a component. [BbTable](../BbTable.md) has a `#no-data` slot; elsewhere an empty state is your own markup with tokens. |
1536
+ | `BbChart` / `BbGraph`, `BbCarousel`, `BbTimeline`, `BbDivider` | Do not exist. No charting, carousel or timeline primitive ships here. |
1245
1537
 
1246
1538
  App chrome — an app shell, sidebar, page header, selection dock — is
1247
1539
  deliberately **not** a component: it is yours to own. The per-platform
@@ -1282,8 +1574,13 @@ components or choose component variants.
1282
1574
  1. **Never hardcode colors** in custom CSS around components. Use the `--bb-*`
1283
1575
  custom properties below so your surfaces follow theme and dark mode.
1284
1576
  2. **Never override component internals** (`.bb-*__*` element classes) from
1285
- consumer code. Restyle via tokens, variants, or documented props — the only
1286
- exceptions are extension points a component's own doc sanctions.
1577
+ consumer code, and never target the reference-only families at all
1578
+ (`.bb-base-*`, `.bb-common-*`, listbox internals) they are the CSS twin of
1579
+ the `BbBase*` / `Common*` import boundary and they get restructured without
1580
+ a rename table. Restyle via tokens, variants, or documented props. A
1581
+ component's **root** class (`.bb-table`, `.bb-badge`) is a legitimate target
1582
+ for setting that component's own tokens — see "How to style around the
1583
+ library" below.
1287
1584
  3. **One primary action per view region.** Everything else is `secondary`,
1288
1585
  `outline`, or `ghost`.
1289
1586
  4. **`destructive` is only for irreversible actions**, and destructive flows
@@ -1293,23 +1590,67 @@ components or choose component variants.
1293
1590
 
1294
1591
  ## How to style around the library
1295
1592
 
1296
- Three lanes, in order of preference:
1593
+ Four lanes, in order of preference. Do not skip to a later lane because an
1594
+ earlier one has no obvious knob — check the lane first.
1297
1595
 
1298
1596
  1. **Component look → props.** Pick a variant, `size`, `compact`. Never write
1299
1597
  CSS to make a component look different.
1300
1598
  2. **Page layout & spacing → Tailwind utilities** (or your project's
1301
1599
  equivalent) on your own wrapper elements — `flex items-end gap-3`,
1302
1600
  `grid gap-6`, `max-w-3xl`. Components size themselves; you only place them.
1303
- 3. **Genuinely custom visuals`--bb-*` tokens.** The few CSS rules that
1601
+ 3. **Retuning one componentits own component tokens.** Every component
1602
+ declares unprefixed `--*` tokens: cell height, padding, badge fill, tab
1603
+ strip height. **Set the token on the element that declares it** — that is
1604
+ usually the component's root class, and an inline style on the component
1605
+ then works too:
1606
+
1607
+ ```css
1608
+ .bb-table {
1609
+ --cell-h: 32px;
1610
+ --padding-x: 8px;
1611
+ }
1612
+ ```
1613
+
1614
+ ```vue
1615
+ <BbTree style="--indent: 14px" />
1616
+ ```
1617
+
1618
+ ⚠ **silent** — a component made of several elements declares some tokens on
1619
+ an inner one, and a value you set further out is _masked_, not inherited:
1620
+ the inner element's own declaration wins. `BbTabs` keeps `--list-h` on
1621
+ `.bb-tabs-list`, so `<BbTabs style="--list-h: 40px">` does nothing and
1622
+ `.bb-tabs-list { --list-h: 40px }` is the working form. Check which selector
1623
+ declares the token before you set it.
1624
+
1625
+ 4. **Genuinely custom visuals → `--bb-*` tokens.** The few CSS rules that
1304
1626
  remain (a custom panel, a status dot) use theme tokens so they follow
1305
1627
  theming and dark mode automatically.
1306
1628
 
1307
- Treat everything inside a component as private API. Where a component is
1308
- designed to be extended visually, its own doc names the extension point
1309
- [BbTree](../BbTree.md) has you draw connector lines in consumer CSS anchored
1310
- to its `--indent`/`--gap` tokens; [BbTabs](../BbTabs.md) exposes `--list-h`.
1311
- Read the per-component doc before writing any CSS that mentions a `.bb-`
1312
- class.
1629
+ **Finding a component's tokens.** They are not in the `--bb-*` inventory below
1630
+ that tier is the brand knobs only, and it is deliberately small. Component
1631
+ tokens live in the component's own CSS, on whichever element consumes them.
1632
+ Read `dist/ai/source/<Name>.md`, which carries that CSS verbatim, and grep it
1633
+ for the declaration rather than assuming the root. Some components also call
1634
+ theirs out in the contract, as [BbTree](../BbTree.md) does for the
1635
+ `--indent`/`--gap` pair you anchor connector lines to.
1636
+
1637
+ **A missing `--bb-*` token is not a gap in the library.** If you want a shorter
1638
+ table row and there is no `--bb-table-row-h`, that is lane 3 telling you to set
1639
+ `--cell-h` on `.bb-table`, not an invitation to write element-class overrides.
1640
+ The full rules, including how per-size lookup tables interact with specificity,
1641
+ are in [design-tokens.md](./design-tokens.md) § "Overriding one from a consumer
1642
+ app".
1643
+
1644
+ **A utility class on a component that does nothing is a layer problem, not a
1645
+ token problem.** Tailwind v4 puts its utilities in `@layer utilities`, and the
1646
+ cascade ranks any unlayered rule above any layered one regardless of
1647
+ specificity — so `<BbButton class="px-6">` loses to the library's own padding
1648
+ until the library is layered too. That is a build-plugin setting, not CSS:
1649
+ `cssLayers: TAILWIND_V4_LAYERS` (exported from `bitboss-ui/vite`) places the
1650
+ library after Preflight and before the utilities, and the utility wins. Opt-in,
1651
+ and the whole story is in
1652
+ [installation-and-plugin-setup](./installation-and-plugin-setup.md) § Cascade
1653
+ layers.
1313
1654
 
1314
1655
  ## Token Vocabulary
1315
1656
 
@@ -1319,6 +1660,13 @@ that change and everything derived follows. The full architecture (tiers,
1319
1660
  naming rules, the emphasis ladder) lives in
1320
1661
  [design-tokens.md](./design-tokens.md); this is the consumer summary.
1321
1662
 
1663
+ **This list is closed.** There is no Tailwind-style numeric ramp — no
1664
+ `--bb-primary-800`, no `--bb-gray-100`. Emphasis comes from the derived tokens
1665
+ below (`--bb-muted`, `--bb-text-muted`, `--bb-surface-hover`) and from
1666
+ `color-mix()` against a knob, not from a shade number. A `--bb-*` name that is
1667
+ not in these tables resolves to nothing and fails silently, so a rule built on
1668
+ one renders with no value at all rather than erroring.
1669
+
1322
1670
  ### Surfaces & structure
1323
1671
 
1324
1672
  | Token | Meaning |
@@ -1374,9 +1722,11 @@ so themes with loud brand colors keep gray chrome.
1374
1722
 
1375
1723
  Input-family tokens also exist (`--bb-input-h`, `--bb-input-px`,
1376
1724
  `--bb-label-fs`…) — see design-tokens.md. Component-specific tokens are
1377
- **unprefixed** and defined on each component's root class (e.g. BbTabs'
1378
- `--list-h`); override them per instance or at a scope to retune a whole area
1379
- coherently instead of styling individual components.
1725
+ **unprefixed** and defined on the element that consumes them, usually the
1726
+ component's root class (e.g. BbTree's `--indent`). Override them per instance
1727
+ or at a scope to retune a whole area coherently instead of styling individual
1728
+ components, and set them on the declaring element — see lane 3 above for the
1729
+ masking trap.
1380
1730
 
1381
1731
  ## Dark Mode
1382
1732
 
@@ -1394,13 +1744,14 @@ To crossfade instead of switching instantly, add `theme-animated` to the
1394
1744
 
1395
1745
  Variant names are typed per family. Built-in sets:
1396
1746
 
1397
- | Family | Built-in variants |
1398
- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
1399
- | `BbButton` / `BbDropdownButton` | `primary` `secondary` `outline` `ghost` `destructive` `link` (+ `none` unstyled escape hatch) |
1400
- | `BbBadge` | `primary` `secondary` `outline` `ghost` `destructive` |
1401
- | `BbAlert` | `primary` `outline` `destructive` `warning` |
1402
- | `BbToast` / `useToast` | `default` `success` `info` `warning` `destructive` |
1403
- | `BbConfirm` / `useConfirm`, dropdown items, tooltips | `default` `destructive` |
1747
+ | Family | Built-in variants |
1748
+ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1749
+ | `BbButton` / `BbDropdownButton` | `primary` `secondary` `outline` `ghost` `destructive` `link` (+ `none` unstyled escape hatch) |
1750
+ | `BbBadge` | `primary` `secondary` `outline` `ghost` `destructive` |
1751
+ | `BbAlert` | `primary` `outline` `destructive` `warning` |
1752
+ | `BbToast` / `useToast` | `default` `success` `info` `warning` `destructive` |
1753
+ | `BbConfirm` / `useConfirm`, dropdown items, tooltips | `default` `destructive` |
1754
+ | Text-like form controls (`BbTextInput`, `BbTextarea`, `BbNumberInput`, `BbSelect`, `BbTag`, date/time/color inputs) | `outline` (default) `secondary` `ghost` (+ `none` structure-only) — same names, same tokens as the button; `ghost` draws no border in any state, errors show through icon + message |
1404
1755
 
1405
1756
  What the shared names mean (shadcn-style vocabulary):
1406
1757
 
@@ -1416,13 +1767,84 @@ What the shared names mean (shadcn-style vocabulary):
1416
1767
 
1417
1768
  Projects can register extra variants through the Vite/Nuxt plugin
1418
1769
  (`buttonVariants`, `badgeVariants`, `alertVariants`, `confirmVariants`,
1419
- `dropdownItemVariants`, `tooltipVariants`, `toastVariants` options).
1770
+ `dropdownItemVariants`, `tooltipVariants`, `toastVariants`, `inputVariants`
1771
+ options).
1420
1772
  Registered names become typed values, but the library ships no CSS for them —
1421
1773
  the project styles `.bb-button--<name>` (etc.) itself, following the state
1422
1774
  recipe in [design-tokens.md](./design-tokens.md).
1423
1775
  Never invent a variant name that is not built-in or registered in the
1424
1776
  project's plugin config — check `vite.config.*` / `nuxt.config.*` first.
1425
1777
 
1778
+ ### Re-skinning a variant the library already ships
1779
+
1780
+ Registering a variant covers a name we do **not** ship. Changing how an
1781
+ existing one looks is a different job, and the first question is not "which
1782
+ CSS" but **how far the change should reach**:
1783
+
1784
+ 1. **The colour itself is wrong for this brand** (primary is blue, not black) →
1785
+ override the **theme knobs**, not any component. `--bb-primary` and its
1786
+ family feed every surface, and every derived value and dark-mode variant
1787
+ follows. This is the theme builder's path and the only one that stays
1788
+ coherent.
1789
+ 2. **One area should look different** (a toolbar, a marketing header) →
1790
+ re-point the same knobs **on a scope**. Custom properties inherit, so this
1791
+ reaches every component in the subtree, whatever shape its variant block
1792
+ has:
1793
+
1794
+ ```css
1795
+ .toolbar {
1796
+ --bb-primary: #2563eb;
1797
+ --bb-primary-hover: #1d4ed8;
1798
+ --bb-primary-fg: #fff;
1799
+ }
1800
+ ```
1801
+
1802
+ Prefer this to per-component rules. It cannot desynchronise states, and it
1803
+ survives dark mode.
1804
+
1805
+ 3. **One component's take on one variant is wrong** (badges too loud, buttons
1806
+ fine) → you have to read that component's variant block first, because
1807
+ **they are not built the same way**:
1808
+
1809
+ | Family | Its variant block sets | So a re-skin is… |
1810
+ | ---------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
1811
+ | `BbBadge`, `BbTooltip` | component tokens (`--bg`, `--color` / `--fg`, `--border-color`, `--ring`) | re-point those tokens |
1812
+ | `BbButton` | CSS properties directly (`background-color`, `border-color`, `color`) | **no token to re-point** — scope the knobs (2), or restate _every_ state |
1813
+ | `BbAlert` | both (`--main-color` / `--muted-color`, plus `background-color`) | read the block; half of it is tokens |
1814
+ | `BbToast` | only `--icon-color` — the variant tints the icon, nothing else | there is no fill to re-skin |
1815
+
1816
+ Read the block in `dist/ai/source/<Name>.md` before writing the rule. Assuming
1817
+ the badge recipe works on a button is the single most common way this goes
1818
+ wrong: `.bb-button--primary { --bg: … }` is silently inert, because the button
1819
+ never reads a `--bg`.
1820
+
1821
+ **Three traps, all of which fail quietly.**
1822
+
1823
+ **1. A variant is not one declaration.** `primary` on a button sets the rest
1824
+ state, `:hover`, `[aria-pressed]`, the `:focus-visible` ring, _and_ an adjacent
1825
+ group border. Override `background-color` alone and the old brand survives in
1826
+ hover and focus — visible only on interaction, which is why this one reaches
1827
+ production. Overriding the knobs (1 or 2) changes every state at once; that is
1828
+ the argument for doing it there.
1829
+
1830
+ **2. Variant blocks are two classes deep.** They are nested under the root, so
1831
+ they compile to `.bb-button.bb-button--primary` — more specific than the
1832
+ single-class rule you are about to write:
1833
+
1834
+ ```css
1835
+ .bb-button--primary {
1836
+ background-color: red;
1837
+ } /* 0,1,0 — loses, renders nothing */
1838
+ .bb-button.bb-button--primary {
1839
+ background-color: red;
1840
+ } /* 0,2,0 — wins */
1841
+ ```
1842
+
1843
+ **3. Override the knob, not the derived value.** Values built with
1844
+ `color-mix()` off a knob re-derive per theme. Replacing the derived value
1845
+ hardcodes it for both schemes, and the bug surfaces as "dark mode looks wrong"
1846
+ months later.
1847
+
1426
1848
  ## Using color
1427
1849
 
1428
1850
  **Reach for color more than the monochrome default suggests — but make it mean