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
@@ -17,8 +17,11 @@ Per the design language, every `destructive` action must pass through a confirm.
17
17
  **Requires exactly one `<BbConfirm />` host, mounted at the app root** (`App.vue`
18
18
  under Vue, `app.vue` under Nuxt, the `createInertiaApp` render root under
19
19
  Inertia) — never one per page or per component. With no host, `await confirm(...)`
20
- never settles and the caller hangs silently. With two hosts, every dialog renders
21
- twice. See [Agent Contract § Global Host Rule](../guides/agent-contract.md).
20
+ never settles and the caller hangs; with two, every dialog renders twice. Both
21
+ warn in DEV, including when the only host has unmounted. The no-host warning is
22
+ deferred by a second, because a late-mounting host renders the pending dialog
23
+ and settles the promise. See
24
+ [Agent Contract § Global Host Rule](../guides/agent-contract.md).
22
25
 
23
26
  ## Import
24
27
 
@@ -16,8 +16,10 @@ per viewport corner and disappear on their own (default **4000 ms**).
16
16
  under Vue, `app.vue` under Nuxt, the `createInertiaApp` render root under
17
17
  Inertia) — never one per page or per component. With no host, messages are
18
18
  enqueued but never rendered _and never expire_, because the dismiss timer lives
19
- inside the host. With two hosts, every message renders once per host. See
20
- [Agent Contract § Global Host Rule](../guides/agent-contract.md).
19
+ inside the host. With two hosts, every message renders once per host. Both warn
20
+ in DEV, including when the only host has unmounted. The no-host warning is
21
+ deferred by a second, because a late-mounting host renders the queued message.
22
+ See [Agent Contract § Global Host Rule](../guides/agent-contract.md).
21
23
 
22
24
  Rule of thumb: `useToast` for _what just happened_ ("Saved", "Upload complete"),
23
25
  `BbAlert` for the _current state of the page_ (a form error summary, a banner).
@@ -12,6 +12,8 @@ Use this contract before generating any UI code with `bitboss-ui`.
12
12
  1. Prefer `Bb*` components in consumer-facing code.
13
13
  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`.
14
14
  3. Use the other reference-only components only when the user explicitly asks for internal composition details.
15
+ 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.
16
+ 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.
15
17
 
16
18
  ## Setup-First Rule
17
19
 
@@ -113,7 +115,7 @@ For components accepting `items`:
113
115
 
114
116
  ## Icons Rule
115
117
 
116
- 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).
118
+ 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).
117
119
 
118
120
  ## Variants Rule
119
121
 
@@ -154,8 +156,9 @@ These overlays also return focus to their trigger element on close, by documente
154
156
 
155
157
  **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.
156
158
 
157
- - **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.
158
- - **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.
159
+ - **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.
160
+ - **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.
161
+ - **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".
159
162
 
160
163
  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.
161
164
 
@@ -168,7 +171,7 @@ Boolean props default to `false` so the bare `<Comp foo>` shorthand turns the fe
168
171
 
169
172
  These are the only intentional default-`true` booleans; anywhere else, `true` default is a bug (`check-api` flags it).
170
173
 
171
- **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).
174
+ **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).
172
175
 
173
176
  ## Testing Against bitboss-ui
174
177
 
@@ -37,7 +37,8 @@ pinned to the prerelease channel rather than following it.
37
37
  `locale` explicitly if the app is not Italian** — the plugin defaults to
38
38
  `'it'` and there is no build-time warning for leaving it unset.
39
39
  3. **Choose intent:**
40
- - Whole page / flow → hop to **Recipes** below (one platform file only).
40
+ - Whole page / flow → hop to **Recipes** below (one platform folder: the
41
+ layout scaffold plus your page recipe).
41
42
  - Single control → [Component picker](./component-picker.md), then open
42
43
  **one** `../<ComponentName>.md` contract (Usage & Guidelines first).
43
44
  4. **Never invent props.** Confirm names in that contract or in
@@ -51,16 +52,27 @@ Folders: `../recipes/vue/`, `../recipes/nuxt/`, `../recipes/inertia/`.
51
52
 
52
53
  Same page shapes on each platform; only the data/mutation layer differs.
53
54
 
54
- | Intent | Recipe file (kebab) |
55
- | ------------------------------------ | ----------------------------------------- |
56
- | Filterable listing + bulk/peek | `records-workspace.md` |
57
- | Create/edit form | `record-form.md` |
58
- | Inline-edit table / backlog | `inline-edit-workspace.md` |
59
- | Hierarchy drill-down (nested tables) | `ownership-atlas.md` (Inertia only today) |
60
- | Approvals / triage inbox | `approvals-inbox.md` |
61
- | Uploads + editable queue | `upload-center.md` |
62
- | Multi-step wizard | `wizard-form.md` |
63
- | Onboarding checklist | `onboarding.md` |
55
+ **`layout-scaffold.md` is a prerequisite, not an option.** It is the three
56
+ app-owned chrome files — the peek-stack context, the `PageShell` frame and the
57
+ `FloatingBottomPanel` selection dock — that every other recipe's
58
+ `@/components/layout/*` imports resolve to. Open it **with** your page recipe;
59
+ the pair counts as one hop.
60
+
61
+ | Intent | Recipe file (kebab) |
62
+ | ----------------------------------------- | ----------------------------------------- |
63
+ | App chrome every recipe imports (**1st**) | `layout-scaffold.md` |
64
+ | Filterable listing + bulk/peek | `records-workspace.md` |
65
+ | Create/edit form | `record-form.md` |
66
+ | Inline-edit table / backlog | `inline-edit-workspace.md` |
67
+ | Hierarchy drill-down (nested tables) | `ownership-atlas.md` (Inertia only today) |
68
+ | Approvals / triage inbox | `approvals-inbox.md` |
69
+ | Uploads + editable queue | `upload-center.md` |
70
+ | Multi-step wizard | `wizard-form.md` |
71
+ | Onboarding checklist | `onboarding.md` |
72
+ | ⌘K / Ctrl+K global command palette | `command-palette.md` |
73
+
74
+ That table is the complete set — every file in each platform folder appears in
75
+ it, so a directory listing (or the MCP `list_recipes`) can add nothing.
64
76
 
65
77
  No recipe for detail-with-tabs, settings, dashboards, or profile pages —
66
78
  compose those from the closest listing/form recipe plus the guides
@@ -70,17 +82,20 @@ compose those from the closest listing/form recipe plus the guides
70
82
 
71
83
  ## Playbooks (only when the pattern applies)
72
84
 
73
- | Need | Guide |
74
- | -------------------------------------- | ----------------------------------------------------- |
75
- | Tied selects / stale model cleanup | [coherence-playbook](./coherence-playbook.md) |
76
- | Async `items` providers / prefill | [fetch-items-playbook](./fetch-items-playbook.md) |
77
- | `item-text` / `item-value` / disabled | [options-items-playbook](./options-items-playbook.md) |
78
- | Table cell edit ownership | [inline-edit-playbook](./inline-edit-playbook.md) |
79
- | Table row → detail (identity cells) | [agent-contract](./agent-contract.md) § Inline Links |
80
- | Peek that drills into related records | [agent-contract](./agent-contract.md) § Consult-Stack |
81
- | Icons (`lucide:*`, never invent names) | [icons-policy](./icons-policy.md) |
82
- | Tokens / styling around components | [design-language](./design-language.md) |
83
- | Upgrading from bitboss-ui 2.x | [migration/v2-to-v3](./migration/v2-to-v3.md) |
85
+ | Need | Guide |
86
+ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | Tied selects / stale model cleanup | [coherence-playbook](./coherence-playbook.md) |
88
+ | Async `items` providers / prefill | [fetch-items-playbook](./fetch-items-playbook.md) |
89
+ | `item-text` / `item-value` / disabled | [options-items-playbook](./options-items-playbook.md) |
90
+ | Table cell edit ownership | [inline-edit-playbook](./inline-edit-playbook.md) |
91
+ | Table row → detail (identity cells) | [agent-contract](./agent-contract.md) § Inline Links |
92
+ | Peek that drills into related records | [agent-contract](./agent-contract.md) § Consult-Stack |
93
+ | Icons (`lucide:*`, never invent names) | [icons-policy](./icons-policy.md) |
94
+ | Tokens / styling around components | [design-language](./design-language.md) |
95
+ | 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 |
96
+ | 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 |
97
+ | Upgrading from bitboss-ui 2.x | [migration/v2-to-v3](./migration/v2-to-v3.md) |
98
+ | 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 |
84
99
 
85
100
  ---
86
101
 
@@ -231,24 +231,29 @@ a composable you want is not in this corpus's `composables/` folder — it is no
231
231
  here.** Check this list before inventing a name; if the need is genuinely absent,
232
232
  compose it or write it yourself.
233
233
 
234
- | Assumed component or composable | Reality |
235
- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
236
- | `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`. |
237
- | `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. |
238
- | `BbModal` | It is [BbDialog](../BbDialog.md). |
239
- | `BbDrawer` / `BbSheet` | It is [BbOffCanvas](../BbOffCanvas.md). |
240
- | `BbCombobox` / `BbAutocomplete` | It is [BbSelect](../BbSelect.md) / [BbSelectPopover](../BbSelectPopover.md) — search is built in, see `disable-writing`. |
241
- | `BbToggle` / `BbToggleGroup` | It is [BbButton](../BbButton.md) with `v-model` (+ `true-value` / `false-value`), which owns `aria-pressed` and the active class. |
242
- | `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. |
243
- | `BbTab` (singular) | The consumer API is [BbTabs](../BbTabs.md) / `BbTabsRoot`. `BbTab` appears only as the **v2 "before" side** of the migration diffs. |
244
- | `BbCard` | Does not exist — a card is a `<div>` with tokens. See [design-language](./design-language.md). |
245
- | `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. |
246
- | `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. |
247
- | `BbStepper` / `BbWizard` | Does not exist as a component; the wizard-form recipe composes one. |
248
- | `BbSkeleton` (standalone) | Only `BbTable` ships skeletons, via the column `skeleton` field. |
249
- | `BbCommand` / command palette | Does not exist. Compose `BbDialog` + `BbSelectPopover`. |
250
- | `BbCalendar` (month / scheduling view) | Does not exist. [BbDatePicker](../BbDatePicker.md) is a picker, not a calendar surface. |
251
- | `BbChart` / `BbGraph`, `BbCarousel`, `BbTimeline`, `BbDivider` | Do not exist. No charting, carousel or timeline primitive ships here. |
234
+ | Assumed component or composable | Reality |
235
+ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
236
+ | `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`. |
237
+ | `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. |
238
+ | `BbModal` | It is [BbDialog](../BbDialog.md). |
239
+ | `BbDrawer` / `BbSheet` | It is [BbOffCanvas](../BbOffCanvas.md). |
240
+ | `BbCombobox` / `BbAutocomplete` | It is [BbSelect](../BbSelect.md) / [BbSelectPopover](../BbSelectPopover.md) — search is built in, see `disable-writing`. |
241
+ | `BbToggle` / `BbToggleGroup` | It is [BbButton](../BbButton.md) with `v-model` (+ `true-value` / `false-value`), which owns `aria-pressed` and the active class. |
242
+ | `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. |
243
+ | `BbTab` (singular) | The consumer API is [BbTabs](../BbTabs.md) / `BbTabsRoot`. `BbTab` appears only as the **v2 "before" side** of the migration diffs. |
244
+ | `BbCard` | Does not exist — a card is a `<div>` with tokens. See [design-language](./design-language.md). |
245
+ | `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. |
246
+ | `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. |
247
+ | `BbStepper` / `BbWizard` | Does not exist as a component; the [wizard-form recipe](../recipes/inertia/wizard-form.md) (also under `vue/`, `nuxt/`) composes one. |
248
+ | `BbSkeleton` (standalone) | Only `BbTable` ships skeletons, via the column `skeleton` field. |
249
+ | `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. |
250
+ | `BbCalendar` (month / scheduling view) | Does not exist. [BbDatePicker](../BbDatePicker.md) is a picker, not a calendar surface. |
251
+ | `BbDataTable` / `BbDataGrid` / data grid | It is [BbTable](../BbTable.md) — sorting, selection, column order/resize, virtual scrolling and inline edit all live there. |
252
+ | `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. |
253
+ | `BbSnackbar` / `BbNotification` / flash message | It is [BbToast](../BbToast.md), raised through the `useToast` composable. |
254
+ | `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. |
255
+ | `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. |
256
+ | `BbChart` / `BbGraph`, `BbCarousel`, `BbTimeline`, `BbDivider` | Do not exist. No charting, carousel or timeline primitive ships here. |
252
257
 
253
258
  App chrome — an app shell, sidebar, page header, selection dock — is
254
259
  deliberately **not** a component: it is yours to own. The per-platform
@@ -14,8 +14,13 @@ components or choose component variants.
14
14
  1. **Never hardcode colors** in custom CSS around components. Use the `--bb-*`
15
15
  custom properties below so your surfaces follow theme and dark mode.
16
16
  2. **Never override component internals** (`.bb-*__*` element classes) from
17
- consumer code. Restyle via tokens, variants, or documented props — the only
18
- exceptions are extension points a component's own doc sanctions.
17
+ consumer code, and never target the reference-only families at all
18
+ (`.bb-base-*`, `.bb-common-*`, listbox internals) — they are the CSS twin of
19
+ the `BbBase*` / `Common*` import boundary and they get restructured without
20
+ a rename table. Restyle via tokens, variants, or documented props. A
21
+ component's **root** class (`.bb-table`, `.bb-badge`) is a legitimate target
22
+ for setting that component's own tokens — see "How to style around the
23
+ library" below.
19
24
  3. **One primary action per view region.** Everything else is `secondary`,
20
25
  `outline`, or `ghost`.
21
26
  4. **`destructive` is only for irreversible actions**, and destructive flows
@@ -25,23 +30,67 @@ components or choose component variants.
25
30
 
26
31
  ## How to style around the library
27
32
 
28
- Three lanes, in order of preference:
33
+ Four lanes, in order of preference. Do not skip to a later lane because an
34
+ earlier one has no obvious knob — check the lane first.
29
35
 
30
36
  1. **Component look → props.** Pick a variant, `size`, `compact`. Never write
31
37
  CSS to make a component look different.
32
38
  2. **Page layout & spacing → Tailwind utilities** (or your project's
33
39
  equivalent) on your own wrapper elements — `flex items-end gap-3`,
34
40
  `grid gap-6`, `max-w-3xl`. Components size themselves; you only place them.
35
- 3. **Genuinely custom visuals → `--bb-*` tokens.** The few CSS rules that
41
+ 3. **Retuning one component → its own component tokens.** Every component
42
+ declares unprefixed `--*` tokens: cell height, padding, badge fill, tab
43
+ strip height. **Set the token on the element that declares it** — that is
44
+ usually the component's root class, and an inline style on the component
45
+ then works too:
46
+
47
+ ```css
48
+ .bb-table {
49
+ --cell-h: 32px;
50
+ --padding-x: 8px;
51
+ }
52
+ ```
53
+
54
+ ```vue
55
+ <BbTree style="--indent: 14px" />
56
+ ```
57
+
58
+ ⚠ **silent** — a component made of several elements declares some tokens on
59
+ an inner one, and a value you set further out is _masked_, not inherited:
60
+ the inner element's own declaration wins. `BbTabs` keeps `--list-h` on
61
+ `.bb-tabs-list`, so `<BbTabs style="--list-h: 40px">` does nothing and
62
+ `.bb-tabs-list { --list-h: 40px }` is the working form. Check which selector
63
+ declares the token before you set it.
64
+
65
+ 4. **Genuinely custom visuals → `--bb-*` tokens.** The few CSS rules that
36
66
  remain (a custom panel, a status dot) use theme tokens so they follow
37
67
  theming and dark mode automatically.
38
68
 
39
- Treat everything inside a component as private API. Where a component is
40
- designed to be extended visually, its own doc names the extension point —
41
- [BbTree](../BbTree.md) has you draw connector lines in consumer CSS anchored
42
- to its `--indent`/`--gap` tokens; [BbTabs](../BbTabs.md) exposes `--list-h`.
43
- Read the per-component doc before writing any CSS that mentions a `.bb-`
44
- class.
69
+ **Finding a component's tokens.** They are not in the `--bb-*` inventory below
70
+ — that tier is the brand knobs only, and it is deliberately small. Component
71
+ tokens live in the component's own CSS, on whichever element consumes them.
72
+ Read `dist/ai/source/<Name>.md`, which carries that CSS verbatim, and grep it
73
+ for the declaration rather than assuming the root. Some components also call
74
+ theirs out in the contract, as [BbTree](../BbTree.md) does for the
75
+ `--indent`/`--gap` pair you anchor connector lines to.
76
+
77
+ **A missing `--bb-*` token is not a gap in the library.** If you want a shorter
78
+ table row and there is no `--bb-table-row-h`, that is lane 3 telling you to set
79
+ `--cell-h` on `.bb-table`, not an invitation to write element-class overrides.
80
+ The full rules, including how per-size lookup tables interact with specificity,
81
+ are in [design-tokens.md](./design-tokens.md) § "Overriding one from a consumer
82
+ app".
83
+
84
+ **A utility class on a component that does nothing is a layer problem, not a
85
+ token problem.** Tailwind v4 puts its utilities in `@layer utilities`, and the
86
+ cascade ranks any unlayered rule above any layered one regardless of
87
+ specificity — so `<BbButton class="px-6">` loses to the library's own padding
88
+ until the library is layered too. That is a build-plugin setting, not CSS:
89
+ `cssLayers: TAILWIND_V4_LAYERS` (exported from `bitboss-ui/vite`) places the
90
+ library after Preflight and before the utilities, and the utility wins. Opt-in,
91
+ and the whole story is in
92
+ [installation-and-plugin-setup](./installation-and-plugin-setup.md) § Cascade
93
+ layers.
45
94
 
46
95
  ## Token Vocabulary
47
96
 
@@ -51,6 +100,13 @@ that change and everything derived follows. The full architecture (tiers,
51
100
  naming rules, the emphasis ladder) lives in
52
101
  [design-tokens.md](./design-tokens.md); this is the consumer summary.
53
102
 
103
+ **This list is closed.** There is no Tailwind-style numeric ramp — no
104
+ `--bb-primary-800`, no `--bb-gray-100`. Emphasis comes from the derived tokens
105
+ below (`--bb-muted`, `--bb-text-muted`, `--bb-surface-hover`) and from
106
+ `color-mix()` against a knob, not from a shade number. A `--bb-*` name that is
107
+ not in these tables resolves to nothing and fails silently, so a rule built on
108
+ one renders with no value at all rather than erroring.
109
+
54
110
  ### Surfaces & structure
55
111
 
56
112
  | Token | Meaning |
@@ -106,9 +162,11 @@ so themes with loud brand colors keep gray chrome.
106
162
 
107
163
  Input-family tokens also exist (`--bb-input-h`, `--bb-input-px`,
108
164
  `--bb-label-fs`…) — see design-tokens.md. Component-specific tokens are
109
- **unprefixed** and defined on each component's root class (e.g. BbTabs'
110
- `--list-h`); override them per instance or at a scope to retune a whole area
111
- coherently instead of styling individual components.
165
+ **unprefixed** and defined on the element that consumes them, usually the
166
+ component's root class (e.g. BbTree's `--indent`). Override them per instance
167
+ or at a scope to retune a whole area coherently instead of styling individual
168
+ components, and set them on the declaring element — see lane 3 above for the
169
+ masking trap.
112
170
 
113
171
  ## Dark Mode
114
172
 
@@ -155,6 +213,76 @@ recipe in [design-tokens.md](./design-tokens.md).
155
213
  Never invent a variant name that is not built-in or registered in the
156
214
  project's plugin config — check `vite.config.*` / `nuxt.config.*` first.
157
215
 
216
+ ### Re-skinning a variant the library already ships
217
+
218
+ Registering a variant covers a name we do **not** ship. Changing how an
219
+ existing one looks is a different job, and the first question is not "which
220
+ CSS" but **how far the change should reach**:
221
+
222
+ 1. **The colour itself is wrong for this brand** (primary is blue, not black) →
223
+ override the **theme knobs**, not any component. `--bb-primary` and its
224
+ family feed every surface, and every derived value and dark-mode variant
225
+ follows. This is the theme builder's path and the only one that stays
226
+ coherent.
227
+ 2. **One area should look different** (a toolbar, a marketing header) →
228
+ re-point the same knobs **on a scope**. Custom properties inherit, so this
229
+ reaches every component in the subtree, whatever shape its variant block
230
+ has:
231
+
232
+ ```css
233
+ .toolbar {
234
+ --bb-primary: #2563eb;
235
+ --bb-primary-hover: #1d4ed8;
236
+ --bb-primary-fg: #fff;
237
+ }
238
+ ```
239
+
240
+ Prefer this to per-component rules. It cannot desynchronise states, and it
241
+ survives dark mode.
242
+
243
+ 3. **One component's take on one variant is wrong** (badges too loud, buttons
244
+ fine) → you have to read that component's variant block first, because
245
+ **they are not built the same way**:
246
+
247
+ | Family | Its variant block sets | So a re-skin is… |
248
+ | ---------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
249
+ | `BbBadge`, `BbTooltip` | component tokens (`--bg`, `--color` / `--fg`, `--border-color`, `--ring`) | re-point those tokens |
250
+ | `BbButton` | CSS properties directly (`background-color`, `border-color`, `color`) | **no token to re-point** — scope the knobs (2), or restate _every_ state |
251
+ | `BbAlert` | both (`--main-color` / `--muted-color`, plus `background-color`) | read the block; half of it is tokens |
252
+ | `BbToast` | only `--icon-color` — the variant tints the icon, nothing else | there is no fill to re-skin |
253
+
254
+ Read the block in `dist/ai/source/<Name>.md` before writing the rule. Assuming
255
+ the badge recipe works on a button is the single most common way this goes
256
+ wrong: `.bb-button--primary { --bg: … }` is silently inert, because the button
257
+ never reads a `--bg`.
258
+
259
+ **Three traps, all of which fail quietly.**
260
+
261
+ **1. A variant is not one declaration.** `primary` on a button sets the rest
262
+ state, `:hover`, `[aria-pressed]`, the `:focus-visible` ring, _and_ an adjacent
263
+ group border. Override `background-color` alone and the old brand survives in
264
+ hover and focus — visible only on interaction, which is why this one reaches
265
+ production. Overriding the knobs (1 or 2) changes every state at once; that is
266
+ the argument for doing it there.
267
+
268
+ **2. Variant blocks are two classes deep.** They are nested under the root, so
269
+ they compile to `.bb-button.bb-button--primary` — more specific than the
270
+ single-class rule you are about to write:
271
+
272
+ ```css
273
+ .bb-button--primary {
274
+ background-color: red;
275
+ } /* 0,1,0 — loses, renders nothing */
276
+ .bb-button.bb-button--primary {
277
+ background-color: red;
278
+ } /* 0,2,0 — wins */
279
+ ```
280
+
281
+ **3. Override the knob, not the derived value.** Values built with
282
+ `color-mix()` off a knob re-derive per theme. Replacing the derived value
283
+ hardcodes it for both schemes, and the bug surfaces as "dark mode looks wrong"
284
+ months later.
285
+
158
286
  ## Using color
159
287
 
160
288
  **Reach for color more than the monochrome default suggests — but make it mean
@@ -295,12 +295,34 @@ Rules:
295
295
  SelectPopover to read) keep a distinctive name per rule 3.
296
296
 
297
297
  Component tokens are still real affordances: a consumer can retune one
298
- instance (`<BbTabs style="--list-h: 40px">`) or a whole page scope. They are
298
+ instance (`<BbTree style="--indent: 14px">`) or a whole page scope. They are
299
299
  just not global namespace.
300
300
 
301
301
  ### Overriding one from a consumer app
302
302
 
303
- Three rules, in order of how often they are broken.
303
+ Four rules, in order of how often they are broken.
304
+
305
+ **0. Set it on the element that declares it.** Sealing (rule 2 above) is what
306
+ makes an inline style on the component root work — and it is also what makes
307
+ the same style _silently do nothing_ when the token lives on an inner element.
308
+ The inner declaration is not overridden by an inherited value; it replaces it.
309
+ `BbTabs` declares `--list-h` on `.bb-tabs-list`, not on `.bb-tabs`:
310
+
311
+ ```css
312
+ /* works — the list is the element that declares it */
313
+ .bb-tabs-list {
314
+ --list-h: 40px;
315
+ }
316
+ ```
317
+
318
+ ```vue
319
+ <!-- does nothing — .bb-tabs-list re-declares --list-h and masks this -->
320
+ <BbTabs style="--list-h: 40px" />
321
+ ```
322
+
323
+ Grep the component's CSS in `dist/ai/source/<Name>.md` for the declaration
324
+ before choosing a selector. A masked token produces no warning and no visual
325
+ change.
304
326
 
305
327
  **1. Override only what you actually render.** Several components expose a
306
328
  per-size lookup table (`--icon-size-xs … --icon-size-2xl`, `--h-xs … --h-2xl`)
@@ -334,7 +356,10 @@ silently does nothing on every `size="sm"` one — the confusing half-failure th
334
356
  sends people to the shotgun in rule 1. The per-size entry has no competing
335
357
  declaration, so it always wins. (A scope selector of equal-or-greater
336
358
  specificity also works, but you have to keep winning that race; overriding the
337
- entry does not.)
359
+ entry does not. If your app is on Tailwind v4 or otherwise layered, the race
360
+ has a structural answer: `cssLayers` in the build plugin puts the library in a
361
+ layer below your utilities, and a utility then wins without any specificity at
362
+ all — see the install guide § Cascade layers.)
338
363
 
339
364
  **3. Scope it to the component, never `:root`.** Short names are deliberately
340
365
  reused across components — `--icon-size` is read by `BbButton`, `BbBadge` and
@@ -656,7 +681,7 @@ like it would replace the seal rule structurally, but it breaks the system:
656
681
 
657
682
  - `inherits: false` stops the root→descendant flow _inside_ a component
658
683
  (declare on `.bb-base-switch`, consume on `__icon` — that is inheritance),
659
- which is also what makes `style="--list-h: 40px"` overrides work. CSS
684
+ which is also what makes `style="--indent: 14px"` overrides work. CSS
660
685
  cannot express "inherit within my subtree but not into nested components".
661
686
  - A registered property always has a value, so `var(--max-h, none)`
662
687
  fallbacks never fire again — our sealing pattern dies.
@@ -16,12 +16,16 @@ Mandatory for AI-generated code.
16
16
  2. **`local:*` is for brand-specific art only** — logos, custom marks, one-off
17
17
  illustrations. They are SVG files in the configured `iconDir`.
18
18
  3. **Never invent icon names.** Use only names that exist in an installed
19
- `@iconify-json/*` set or as files in `iconDir`. A missing icon throws at
20
- mount (dev shows a Vite error overlay with the exact fix).
21
- 4. **Icon names must be static string literals.** Production builds scan source
22
- for `provider:name` literals and bundle only those — a name assembled at
23
- runtime (`` `lucide:${kind}` ``) will be missing in production. Map dynamic
24
- state to literals instead (e.g. a lookup object of full names).
19
+ `@iconify-json/*` set or as files in `iconDir`. A name that does not
20
+ resolve renders the built-in placeholder (a question mark in a circle,
21
+ tagged `data-bb-icon-missing="<name>"`) and logs one `console.error` with
22
+ the fix — in dev also as a Vite error overlay. It never throws.
23
+ 4. **Write icon names as literals, anywhere in the project.** The plugin
24
+ scans the whole project (PHP included) for `provider:name` literals and
25
+ bundles only those; `local:` ships everything. A name the scanner cannot
26
+ see as a literal — composed at runtime (`` `lucide:${kind}` ``), stored in a
27
+ database, chosen in an admin panel — goes in the plugin's `iconWhitelist`.
28
+ This holds in dev exactly as in production: dev registers the same set.
25
29
  5. **Never hand-place `BbIcon` where a component already accepts an icon** via
26
30
  a prop, item field, or dedicated slot (catalog below).
27
31
 
@@ -35,19 +39,38 @@ The `bitbossUi` Vite plugin (`iconDir` option) — or the Nuxt module (config ke
35
39
  auto-discovered. Installing `@iconify-json/lucide` makes every valid
36
40
  `lucide:<name>` available; removing it makes them all errors. Nothing to
37
41
  configure.
38
- - **Dev**: all icons of installed sets are registered. Referencing an
39
- uninstalled prefix or a non-existent name fails loudly in the Vite error
40
- overlay with an actionable message. A typo inside an installed set reads
41
- "does not exist in the lucide icon set — did you mean `lucide:…`?"; a set
42
- that is not installed reads "no installed icon set has the prefix `mdi`
43
- (installed sets: lucide) … run: `npm install -D @iconify-json/mdi`". Read
44
- the message: a typo wants a different name, a missing set wants an install,
45
- never a guess.
46
- - **Build**: source files (`.vue`/`.ts`/`.js`/`.tsx`/`.jsx`; tests and stories
47
- excluded) are scanned for static `provider:name` literals; each referenced
48
- icon is emitted as its own lazy chunk. This is why rule 4 exists — and why an
49
- icon used only in a `.stories.ts`/`.test.ts` file must also appear as a
50
- literal in a scanned source file.
42
+ - **What ships — `local:` everything, providers only what is scanned or
43
+ whitelisted.** Every SVG in `iconDir` is bundled. For provider sets the
44
+ plugin reads the **whole project** as bytes — every file under the Vite
45
+ root, PHP, Blade, JSON, SQL seeders and Markdown included — looking for
46
+ `<prefix>:<name>` where `<prefix>` is an installed set; what resolves in the
47
+ set's `icons.json` (icons and aliases) becomes a lazy chunk. Skipped:
48
+ whatever `.gitignore` ignores (every level), `node_modules`, `vendor`,
49
+ `storage`, the build `outDir`, every hidden path (`.something`), binary
50
+ files, and paths listed in `iconsScanner.exclude`. Extra locations
51
+ (`iconsScanner.include`) are additive. No language is parsed, so a Laravel
52
+ enum returning `'tabler:user-share'` counts as a literal.
53
+ - **Dev is prod.** The same scan runs on dev start (once, cached by content
54
+ hash under `node_modules/.cache/bitboss-ui/`) and again per file on every
55
+ save, so the registry in dev is exactly the set the build emits. Typing a
56
+ new literal into a template renders it without a restart. An icon that is
57
+ neither scanned nor whitelisted therefore shows the placeholder and the
58
+ error on first render in dev — not after deploy.
59
+ - **Names the scanner cannot see** — composed at runtime, persisted in a
60
+ database, picked by an admin — go in `iconWhitelist`: exact names
61
+ (`'tabler:crown'`) or globs (`'tabler:calendar-*'`). An entry that resolves
62
+ in no installed set fails the build with the same message the overlay shows.
63
+ - **A missing name is loud, in both environments.** `BbIcon` renders the
64
+ built-in placeholder glyph with `data-bb-icon-missing="<requested name>"`
65
+ and logs one `console.error` per name per page load; dev adds the Vite
66
+ error overlay. A typo inside an installed set reads "does not exist in the
67
+ lucide icon set — did you mean `lucide:…`?"; a real icon nobody wrote as a
68
+ literal reads "exists in the lucide icon set but is not bundled … add it to
69
+ `iconWhitelist`"; a set that is not installed reads "no installed icon set
70
+ has the prefix `mdi` (installed sets: lucide) … run:
71
+ `npm install -D @iconify-json/mdi`". Read the message: a typo wants a
72
+ different name, an unscanned name wants the whitelist, a missing set wants
73
+ an install, never a guess.
51
74
  - **Local icons**: `iconDir` is scanned recursively; each `.svg` becomes
52
75
  `local:<basename-without-extension>`. A bare, unprefixed name normalizes to
53
76
  `local:`. Adding/removing files is picked up live in dev, and the plugin
@@ -256,8 +279,10 @@ markup) yourself:
256
279
  (slot content or `text`; `aria-label` only to override).
257
280
  - Inventing or guessing icon names; assuming a set is installed without
258
281
  checking `package.json`.
259
- - Runtime-composed icon names (template strings / concatenation) — invisible to
260
- the build scanner, broken in production.
282
+ - Runtime-composed icon names (template strings / concatenation) without a
283
+ matching `iconWhitelist` entry — invisible to the scanner, so they render the
284
+ placeholder (in dev too). Prefer a lookup of full literals; whitelist what
285
+ must stay dynamic.
261
286
  - Setting `color`/`fill` on the icon element instead of the parent.
262
287
  - Falling back to random provider sets when a needed `local:` asset is missing —
263
288
  ask for the SVG (or approval to substitute) instead.