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
@@ -73,7 +73,7 @@ The slot scope carries five things:
73
73
  {{ dragging ? 'Drop to upload' : 'Drag a file here' }}
74
74
  </p>
75
75
  <BbButton variant="ghost" @click="open">or browse</BbButton>
76
- <p v-if="file" class="text-xs text-[color:var(--bb-text-muted)]">
76
+ <p v-if="file" class="text-xs text-(--bb-text-muted)">
77
77
  Selected: {{ file.name }}
78
78
  </p>
79
79
  </div>
@@ -151,7 +151,7 @@ from the model (revoke the stale one when it changes) handed to
151
151
  </BbAvatar>
152
152
  <div class="min-w-0 flex-1">
153
153
  <p class="text-sm font-medium">Ada Moreau</p>
154
- <p class="truncate text-xs text-[color:var(--bb-text-muted)]">
154
+ <p class="truncate text-xs text-(--bb-text-muted)">
155
155
  ada.moreau@acme.dev
156
156
  </p>
157
157
  </div>
@@ -334,7 +334,7 @@ there is no separate "selected files" API. Removal is a plain
334
334
  <p :id="labelId" class="text-sm font-medium">
335
335
  {{ dragging ? 'Drop to attach' : 'Drag receipts here' }}
336
336
  </p>
337
- <p class="text-xs text-[color:var(--bb-text-muted)]">
337
+ <p class="text-xs text-(--bb-text-muted)">
338
338
  PNG, JPG or PDF · up to 5 files · 5 MB each
339
339
  </p>
340
340
  <BbButton variant="outline" @click="open">Browse files</BbButton>
@@ -356,7 +356,7 @@ there is no separate "selected files" API. Removal is a plain
356
356
  <span class="min-w-0 flex-1 truncate text-sm font-medium">{{
357
357
  file.name
358
358
  }}</span>
359
- <span class="shrink-0 text-xs text-[color:var(--bb-text-muted)]">{{
359
+ <span class="shrink-0 text-xs text-(--bb-text-muted)">{{
360
360
  formatSize(file.size)
361
361
  }}</span>
362
362
  <BbButton
@@ -526,7 +526,7 @@ from the validated model:
526
526
  <div class="csv-import">
527
527
  <div class="csv-import__header">
528
528
  <p class="text-sm font-medium">Import contacts</p>
529
- <p class="text-xs text-[color:var(--bb-text-muted)]">
529
+ <p class="text-xs text-(--bb-text-muted)">
530
530
  CSV export from your previous CRM · a header row is required
531
531
  </p>
532
532
  </div>
@@ -554,9 +554,7 @@ from the validated model:
554
554
  <p :id="labelId" class="text-sm font-medium">
555
555
  {{ dragging ? 'Drop your CSV' : 'Drag a CSV export here' }}
556
556
  </p>
557
- <p class="text-xs text-[color:var(--bb-text-muted)]">
558
- .csv only · up to 1 MB
559
- </p>
557
+ <p class="text-xs text-(--bb-text-muted)">.csv only · up to 1 MB</p>
560
558
  <BbButton variant="outline" @click="open">Choose file</BbButton>
561
559
  </div>
562
560
  </template>
@@ -570,7 +568,7 @@ from the validated model:
570
568
  }}</span>
571
569
  <span
572
570
  v-if="columns.length"
573
- class="shrink-0 text-xs text-[color:var(--bb-text-muted)]"
571
+ class="shrink-0 text-xs text-(--bb-text-muted)"
574
572
  >
575
573
  {{ columns.length }} columns detected
576
574
  </span>
@@ -830,7 +828,7 @@ transfer is in flight.
830
828
  <span class="min-w-0 truncate text-sm font-medium">{{
831
829
  file.name
832
830
  }}</span>
833
- <span class="shrink-0 text-xs text-[color:var(--bb-text-muted)]">{{
831
+ <span class="shrink-0 text-xs text-(--bb-text-muted)">{{
834
832
  statusLabel
835
833
  }}</span>
836
834
  </div>
@@ -1035,7 +1033,7 @@ form library can mark the field touched and run blur-time validation.
1035
1033
  <p :id="labelId" class="text-sm font-medium">
1036
1034
  {{ dragging ? 'Drop to attach' : 'Attach supporting PDFs' }}
1037
1035
  </p>
1038
- <p class="text-xs text-[color:var(--bb-text-muted)]">
1036
+ <p class="text-xs text-(--bb-text-muted)">
1039
1037
  PDF only · up to 4 files · 10 MB each
1040
1038
  </p>
1041
1039
  <BbButton variant="outline" @click="open">Browse</BbButton>
package/dist/ai/BbIcon.md CHANGED
@@ -36,14 +36,10 @@ The full catalog of every icon-accepting surface lives in the **Icons Policy**
36
36
 
37
37
  ```vue
38
38
  <template>
39
- <div
40
- class="flex max-w-sm flex-col gap-2 rounded-[var(--bb-radius)] border p-3"
41
- >
39
+ <div class="flex max-w-sm flex-col gap-2 rounded-(--bb-radius) border p-3">
42
40
  <div class="flex flex-col gap-0.5">
43
41
  <p class="text-sm font-medium">API status</p>
44
- <p class="text-xs text-[color:var(--bb-text-muted)]">
45
- Last checked 2 minutes ago
46
- </p>
42
+ <p class="text-xs text-(--bb-text-muted)">Last checked 2 minutes ago</p>
47
43
  </div>
48
44
  <ul class="flex flex-col gap-1.5 text-sm">
49
45
  <li
@@ -142,10 +138,13 @@ The `icon` prop takes a name in one of three forms:
142
138
  the same shape as `mdi:home` / `lucide:check`, so the source of every icon is
143
139
  legible without inferring it from a missing prefix.
144
140
 
145
- Discovery-first: **only use names you have verified exist** — a missing icon
146
- **throws at mount** (dev shows an actionable Vite overlay with the exact
147
- `npm install` fix). Never guess a name. See the Icons Policy for the discovery
148
- workflow and the heroicons `-20-`/`-16-` size-variant caveat.
141
+ Discovery-first: **only use names you have verified exist** — a name that does
142
+ not resolve renders the **built-in placeholder** (a question mark in a circle,
143
+ tagged `data-bb-icon-missing="<name>"`) and logs one `console.error` with the
144
+ exact fix; dev also shows it as a Vite overlay. It never throws, so the tree
145
+ around it stays mounted and the gap is visible where the icon should be. Never
146
+ guess a name. See the Icons Policy for the discovery workflow and the heroicons
147
+ `-20-`/`-16-` size-variant caveat.
149
148
 
150
149
  **The same glyph across collections, plus a local icon**
151
150
 
@@ -153,19 +152,19 @@ workflow and the heroicons `-20-`/`-16-` size-variant caveat.
153
152
  <div class="flex items-center gap-3">
154
153
  <div class="flex flex-col items-center gap-1">
155
154
  <BbIcon icon="lucide:heart" size="lg" />
156
- <span class="text-xs text-[color:var(--bb-text-muted)]">lucide</span>
155
+ <span class="text-xs text-(--bb-text-muted)">lucide</span>
157
156
  </div>
158
157
  <div class="flex flex-col items-center gap-1">
159
158
  <BbIcon icon="heroicons:heart" size="lg" />
160
- <span class="text-xs text-[color:var(--bb-text-muted)]">heroicons</span>
159
+ <span class="text-xs text-(--bb-text-muted)">heroicons</span>
161
160
  </div>
162
161
  <div class="flex flex-col items-center gap-1">
163
162
  <BbIcon icon="mdi:heart" size="lg" />
164
- <span class="text-xs text-[color:var(--bb-text-muted)]">mdi</span>
163
+ <span class="text-xs text-(--bb-text-muted)">mdi</span>
165
164
  </div>
166
165
  <div class="flex flex-col items-center gap-1">
167
166
  <BbIcon icon="local:sparkles" size="lg" />
168
- <span class="text-xs text-[color:var(--bb-text-muted)]">local</span>
167
+ <span class="text-xs text-(--bb-text-muted)">local</span>
169
168
  </div>
170
169
  </div>
171
170
  ```
@@ -176,18 +175,18 @@ with full `local:` literals from that folder — the canonical brand-mark demo.
176
175
  **Every `local:` name in this guide is one of _this repository's_ fixture SVGs**
177
176
  (`clock`, `loader`, `check_circle`, `alert_circle`, …). They are illustrative,
178
177
  not a shipped set: copy the pattern, not the names. In your project a
179
- `local:` name resolves only if `<name>.svg` exists in your own `iconDir`, and a
180
- missing one throws at mount.
178
+ `local:` name resolves only if `<name>.svg` exists in your own `iconDir`; a
179
+ missing one renders the placeholder and logs which file is absent.
181
180
 
182
181
  **Brand marks via local: — deploy stages**
183
182
 
184
183
  ```vue
185
184
  <template>
186
- <div class="max-w-sm rounded-[var(--bb-radius)] border p-3">
185
+ <div class="max-w-sm rounded-(--bb-radius) border p-3">
187
186
  <div class="flex items-center justify-between gap-3">
188
187
  <div class="min-w-0">
189
188
  <p class="truncate text-sm font-medium">acme/storefront</p>
190
- <p class="text-xs text-[color:var(--bb-text-muted)]">
189
+ <p class="text-xs text-(--bb-text-muted)">
191
190
  Deploy #482 · production · {{ index + 1 }}/{{ stages.length }}
192
191
  </p>
193
192
  </div>
@@ -216,6 +215,10 @@ type Stage = {
216
215
  // literal so the production scanner bundles it — never `local:${name}`. The
217
216
  // glyph is decorative beside the stage label, so no `label` prop.
218
217
  const stages: Stage[] = [
218
+ // `local:*` names resolve against YOUR `iconDir` — these four are this
219
+ // repository's fixture SVGs. Copied into another project, each one throws
220
+ // at mount until a `<name>.svg` of your own exists there (or swap them for
221
+ // `lucide:*` names, which need no files).
219
222
  { label: 'Queued', icon: 'local:clock', tone: 'stage--muted' },
220
223
  { label: 'Building', icon: 'local:loader', tone: 'stage--active' },
221
224
  { label: 'Deployed', icon: 'local:check_circle', tone: 'stage--active' },
@@ -245,16 +248,21 @@ const onAdvance = () => {
245
248
  </style>
246
249
  ```
247
250
 
248
- #### The static-literal production constraint
249
-
250
- Icon names **must be static string literals**. Production builds scan source for
251
- `provider:name` literals and bundle only those a name assembled at runtime
252
- (`` `lucide:${kind}` ``) is invisible to the scanner and **missing in
253
- production**. Map dynamic state to full literals instead a lookup object of
254
- complete names, exactly as the deploy-stages companion does (`stages[i].icon`
255
- holds `'local:clock'`, not a concatenation). An icon used only in a
256
- `.stories.ts` / `.test.ts` must also appear as a literal in a scanned source
257
- file, since those are excluded from the scan.
251
+ #### The literal-or-whitelist constraint
252
+
253
+ Provider icons ship only when the plugin can see them. It reads the **whole
254
+ project** as bytes every file under the Vite root, PHP and JSON and SQL
255
+ included, minus gitignored, hidden, binary, `node_modules`/`vendor` and
256
+ excluded paths for `provider:name` literals, and bundles exactly those (plus
257
+ `iconWhitelist`). `local:` ships everything. **Dev registers the same set**, so
258
+ a name assembled at runtime (`` `lucide:${kind}` ``) renders the placeholder
259
+ on first render in dev, not after deploy. Map dynamic state to full literals —
260
+ a lookup object of complete names, exactly as the deploy-stages companion does
261
+ (`stages[i].icon` holds `'local:clock'`, not a concatenation) — or, when the
262
+ name really is composed at runtime or comes from a database, list it in the
263
+ plugin's `iconWhitelist` (`'lucide:circle-check'`, or a glob such as
264
+ `'lucide:calendar-*'`). An icon used only in a `.stories.ts` / `.test.ts` file
265
+ is scanned like any other file.
258
266
 
259
267
  ```vue
260
268
  <script setup lang="ts">
@@ -264,7 +272,8 @@ const STATUS_ICON = {
264
272
  pending: 'lucide:clock',
265
273
  failed: 'lucide:circle-x',
266
274
  } as const;
267
- // Wrong: `lucide:${status}` — never bundled, broken in production.
275
+ // Wrong: `lucide:${status}` — never scanned; placeholder in dev AND prod
276
+ // (unless every value is in the plugin's `iconWhitelist`).
268
277
  </script>
269
278
 
270
279
  <template>
@@ -289,13 +298,13 @@ behave exactly as with registry icons.
289
298
  ```vue
290
299
  <template>
291
300
  <div
292
- class="flex max-w-sm flex-col items-center gap-2 rounded-[var(--bb-radius)] border p-4 text-center"
301
+ class="flex max-w-sm flex-col items-center gap-2 rounded-(--bb-radius) border p-4 text-center"
293
302
  >
294
- <span class="text-[color:var(--bb-text-muted)]">
303
+ <span class="text-(--bb-text-muted)">
295
304
  <BbIcon :icon="memberMark" size="2xl" />
296
305
  </span>
297
306
  <p class="text-sm font-medium">No members yet</p>
298
- <p class="text-xs text-[color:var(--bb-text-muted)]">
307
+ <p class="text-xs text-(--bb-text-muted)">
299
308
  Invite your team to start assigning issues on Northwind.
300
309
  </p>
301
310
  <BbButton variant="primary">Invite member</BbButton>
@@ -349,7 +358,9 @@ deploy-stages companions both color the row wrapper; the icon tracks it.
349
358
  scale library-wide; the defaults above apply when unset.
350
359
  - **Plugin required** — `BbIcon` resolves names through the
351
360
  `virtual:bitboss-ui/icons` module the platform plugin provides; without the
352
- Vue/Nuxt/Inertia plugin installed it throws a "Failed to load icons" error.
361
+ Vue/Nuxt/Inertia plugin installed it throws a "Failed to load icons" error
362
+ (a setup fault — a merely missing NAME never throws, it renders the
363
+ placeholder).
353
364
  Provider sets (`@iconify-json/*`) are auto-discovered from `node_modules`;
354
365
  `local:*` comes from `iconDir` (default `./assets/icons` on Nuxt).
355
366
  - **SSR / Nuxt** — icons resolve during `onServerPrefetch`, so server-rendered
@@ -189,11 +189,11 @@ when the inbox clears.
189
189
  ```vue
190
190
  <template>
191
191
  <div
192
- class="flex max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-3"
192
+ class="flex max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
193
193
  >
194
194
  <div class="min-w-0">
195
195
  <p class="truncate text-sm font-medium">Northwind</p>
196
- <p class="text-xs text-[color:var(--bb-text-muted)]">{{ statusLine }}</p>
196
+ <p class="text-xs text-(--bb-text-muted)">{{ statusLine }}</p>
197
197
  </div>
198
198
  <div class="flex items-center gap-1.5">
199
199
  <BbIndicator :text="count || undefined" variant="destructive">
@@ -58,10 +58,10 @@ project can go all-floating without touching call sites.
58
58
 
59
59
  ```vue
60
60
  <template>
61
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-4">
61
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-4">
62
62
  <div>
63
63
  <p class="text-sm font-medium">Inventory counts</p>
64
- <p class="text-xs text-[color:var(--bb-text-muted)]">
64
+ <p class="text-xs text-(--bb-text-muted)">
65
65
  Same field, three label placements — pick one per product.
66
66
  </p>
67
67
  </div>
@@ -131,10 +131,10 @@ The same messaging surface as the rest of the family:
131
131
 
132
132
  ```vue
133
133
  <template>
134
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-4">
134
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-4">
135
135
  <div>
136
136
  <p class="text-sm font-medium">Team seats · Pro plan</p>
137
- <p class="text-xs text-[color:var(--bb-text-muted)]">
137
+ <p class="text-xs text-(--bb-text-muted)">
138
138
  Hint states the range; business caps surface as errors.
139
139
  </p>
140
140
  </div>
@@ -183,10 +183,10 @@ keyboards work without configuration.
183
183
 
184
184
  ```vue
185
185
  <template>
186
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-4">
186
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-4">
187
187
  <div>
188
188
  <p class="text-sm font-medium">Office climate · Floor 3</p>
189
- <p class="text-xs text-[color:var(--bb-text-muted)]">
189
+ <p class="text-xs text-(--bb-text-muted)">
190
190
  Clamped 16–28 °C · whole degrees only.
191
191
  </p>
192
192
  </div>
@@ -235,12 +235,10 @@ stays free while the buttons move in fixed increments.
235
235
 
236
236
  ```vue
237
237
  <template>
238
- <div class="grid max-w-sm gap-2 rounded-[var(--bb-radius)] border p-3">
238
+ <div class="grid max-w-sm gap-2 rounded-(--bb-radius) border p-3">
239
239
  <div>
240
240
  <p class="text-sm font-medium">USB-C Cable (2 m)</p>
241
- <p class="text-xs text-[color:var(--bb-text-muted)]">
242
- $12.50 each · 10 in stock
243
- </p>
241
+ <p class="text-xs text-(--bb-text-muted)">$12.50 each · 10 in stock</p>
244
242
  </div>
245
243
  <BbNumberInput
246
244
  v-model="quantity"
@@ -299,10 +297,10 @@ model stays a clean number.
299
297
 
300
298
  ```vue
301
299
  <template>
302
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-4">
300
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-4">
303
301
  <div>
304
302
  <p class="text-sm font-medium">Line item · INV-2048</p>
305
- <p class="text-xs text-[color:var(--bb-text-muted)]">
303
+ <p class="text-xs text-(--bb-text-muted)">
306
304
  Currency stays in the chrome; the model is a clean number.
307
305
  </p>
308
306
  </div>
@@ -354,10 +352,10 @@ yourself if an async check must settle first.
354
352
 
355
353
  ```vue
356
354
  <template>
357
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-4">
355
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-4">
358
356
  <div>
359
357
  <p class="text-sm font-medium">Invoice credit</p>
360
- <p class="text-xs text-[color:var(--bb-text-muted)]">
358
+ <p class="text-xs text-(--bb-text-muted)">
361
359
  Clear the budget anytime; coupon check is visual-only loading.
362
360
  </p>
363
361
  </div>
@@ -433,10 +431,10 @@ watch(coupon, (value) => {
433
431
 
434
432
  ```vue
435
433
  <template>
436
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-4">
434
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-4">
437
435
  <div>
438
436
  <p class="text-sm font-medium">Seat allocation</p>
439
- <p class="text-xs text-[color:var(--bb-text-muted)]">
437
+ <p class="text-xs text-(--bb-text-muted)">
440
438
  Error, disabled, and readonly — same chrome, different affordances.
441
439
  </p>
442
440
  </div>
@@ -490,10 +488,10 @@ the label is embedded in the field, so the layout is forced vertical and
490
488
 
491
489
  ```vue
492
490
  <template>
493
- <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-3">
491
+ <div class="grid max-w-sm gap-3 rounded-(--bb-radius) border p-3">
494
492
  <div>
495
493
  <p class="text-sm font-medium">Package · BOX-4412</p>
496
- <p class="text-xs text-[color:var(--bb-text-muted)]">
494
+ <p class="text-xs text-(--bb-text-muted)">
497
495
  Shipping dimensions · compact density throughout.
498
496
  </p>
499
497
  </div>
@@ -534,10 +532,10 @@ quota screens do it.
534
532
 
535
533
  ```vue
536
534
  <template>
537
- <div class="grid max-w-md gap-3 rounded-[var(--bb-radius)] border p-4">
535
+ <div class="grid max-w-md gap-3 rounded-(--bb-radius) border p-4">
538
536
  <div>
539
537
  <p class="text-sm font-medium">API rate limits</p>
540
- <p class="text-xs text-[color:var(--bb-text-muted)]">
538
+ <p class="text-xs text-(--bb-text-muted)">
541
539
  Applied per key. Changes take effect immediately.
542
540
  </p>
543
541
  </div>
@@ -44,11 +44,11 @@ for buttons inside the body, and footer buttons simply set the model back to
44
44
  ```vue
45
45
  <template>
46
46
  <div
47
- class="flex max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-3"
47
+ class="flex max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
48
48
  >
49
49
  <div class="min-w-0">
50
50
  <p class="m-0 text-sm font-medium">Workspace</p>
51
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
51
+ <p class="m-0 text-xs text-(--bb-text-muted)">
52
52
  3 updates since yesterday
53
53
  </p>
54
54
  </div>
@@ -59,7 +59,7 @@ for buttons inside the body, and footer buttons simply set the model back to
59
59
  <div class="grid gap-3 text-sm">
60
60
  <div v-for="entry in activity" :key="entry.id" class="grid gap-0.5">
61
61
  <p class="m-0">{{ entry.text }}</p>
62
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
62
+ <p class="m-0 text-xs text-(--bb-text-muted)">
63
63
  {{ entry.when }}
64
64
  </p>
65
65
  </div>
@@ -114,9 +114,7 @@ dimension `size` controls.
114
114
 
115
115
  ```vue
116
116
  <template>
117
- <div
118
- class="flex max-w-md flex-wrap gap-2 rounded-[var(--bb-radius)] border p-3"
119
- >
117
+ <div class="flex max-w-md flex-wrap gap-2 rounded-(--bb-radius) border p-3">
120
118
  <BbButton
121
119
  v-for="edge in edges"
122
120
  :key="edge"
@@ -234,11 +232,11 @@ This companion seeds typed filter state and resets it in place.
234
232
  ```vue
235
233
  <template>
236
234
  <div
237
- class="flex max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-3"
235
+ class="flex max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
238
236
  >
239
237
  <div class="min-w-0">
240
238
  <p class="m-0 text-sm font-medium">Invoices</p>
241
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
239
+ <p class="m-0 text-xs text-(--bb-text-muted)">
242
240
  {{ matchingLabel }}
243
241
  </p>
244
242
  </div>
@@ -321,10 +319,8 @@ record" action to `BbButton`'s `href` rather than a click handler.
321
319
 
322
320
  ```vue
323
321
  <template>
324
- <div class="grid max-w-sm gap-1 rounded-[var(--bb-radius)] border p-1">
325
- <p
326
- class="m-0 px-2 pt-1.5 text-xs font-medium text-[color:var(--bb-text-muted)]"
327
- >
322
+ <div class="grid max-w-sm gap-1 rounded-(--bb-radius) border p-1">
323
+ <p class="m-0 px-2 pt-1.5 text-xs font-medium text-(--bb-text-muted)">
328
324
  Members
329
325
  </p>
330
326
  <BbButton
@@ -340,7 +336,7 @@ record" action to `BbButton`'s `href` rather than a click handler.
340
336
  :alt="member.name"
341
337
  size="32"
342
338
  :src="member.src"
343
- :timeout="demoAvatarTimeout"
339
+ :timeout="avatarTimeout"
344
340
  >
345
341
  {{ member.initials }}
346
342
  </BbAvatar>
@@ -355,7 +351,7 @@ record" action to `BbButton`'s `href` rather than a click handler.
355
351
  :alt="selected.name"
356
352
  size="56"
357
353
  :src="selected.src"
358
- :timeout="demoAvatarTimeout"
354
+ :timeout="avatarTimeout"
359
355
  >
360
356
  {{ selected.initials }}
361
357
  </BbAvatar>
@@ -372,7 +368,7 @@ record" action to `BbButton`'s `href` rather than a click handler.
372
368
  :key="row.label"
373
369
  class="flex items-baseline justify-between gap-3"
374
370
  >
375
- <dt class="shrink-0 text-xs text-[color:var(--bb-text-muted)]">
371
+ <dt class="shrink-0 text-xs text-(--bb-text-muted)">
376
372
  {{ row.label }}
377
373
  </dt>
378
374
  <dd class="m-0 truncate text-right">{{ row.value }}</dd>
@@ -396,10 +392,19 @@ record" action to `BbButton`'s `href` rather than a click handler.
396
392
  <script setup lang="ts">
397
393
  import { computed, ref } from 'vue';
398
394
  import { BbAvatar, BbBadge, BbButton, BbOffCanvas } from 'bitboss-ui';
399
- import {
400
- demoAvatarTimeout,
401
- demoFaces,
402
- } from '@/components/BbAvatar/examples/demoAvatars';
395
+
396
+ // Self-contained demo data. Any image URL works here — these are stock
397
+ // portraits; swap in your own. `timeout` is BbAvatar's deadline before the
398
+ // fallback shows (default 400ms, meant for SSR/slow networks): a remote demo
399
+ // origin needs headroom or the initials flash in first. See the guide §
400
+ // "Slow networks, SSR and the timeout prop".
401
+ const avatarTimeout = 4000;
402
+ const face = (photoId: string) =>
403
+ `https://images.unsplash.com/${photoId}?w=256&h=256&fit=crop&crop=faces&auto=format`;
404
+ const faces = {
405
+ ada: face('photo-1494790108377-be9c29b29330'),
406
+ grace: face('photo-1438761681033-6461ffad8d80'),
407
+ };
403
408
 
404
409
  type MemberStatus = 'Active' | 'Away';
405
410
 
@@ -427,7 +432,7 @@ const members: Member[] = [
427
432
  team: 'Platform',
428
433
  status: 'Active',
429
434
  statusVariant: 'soft-green',
430
- src: demoFaces.ada,
435
+ src: faces.ada,
431
436
  },
432
437
  {
433
438
  id: 2,
@@ -439,7 +444,7 @@ const members: Member[] = [
439
444
  team: 'Billing',
440
445
  status: 'Away',
441
446
  statusVariant: 'soft-amber',
442
- src: demoFaces.grace,
447
+ src: faces.grace,
443
448
  },
444
449
  ];
445
450
 
@@ -497,11 +502,11 @@ treatment in the list, and account/sign-out sit in the footer.
497
502
  ```vue
498
503
  <template>
499
504
  <div
500
- class="flex max-w-sm items-center justify-between gap-2 rounded-[var(--bb-radius)] border p-2"
505
+ class="flex max-w-sm items-center justify-between gap-2 rounded-(--bb-radius) border p-2"
501
506
  >
502
507
  <div class="min-w-0">
503
508
  <p class="m-0 truncate text-sm font-medium">Acme Billing</p>
504
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">Workspace</p>
509
+ <p class="m-0 text-xs text-(--bb-text-muted)">Workspace</p>
505
510
  </div>
506
511
  <BbButton
507
512
  aria-label="Open navigation"
@@ -514,9 +519,7 @@ treatment in the list, and account/sign-out sit in the footer.
514
519
  </div>
515
520
  <BbOffCanvas v-model="open" side="left" size="xs" title="Acme Billing">
516
521
  <nav aria-label="Primary" class="grid gap-1">
517
- <p
518
- class="m-0 px-1 pb-0.5 text-xs font-medium text-[color:var(--bb-text-muted)]"
519
- >
522
+ <p class="m-0 px-1 pb-0.5 text-xs font-medium text-(--bb-text-muted)">
520
523
  Navigate
521
524
  </p>
522
525
  <BbButton
@@ -668,12 +671,13 @@ dismiss with the same motion:
668
671
 
669
672
  Give panels the same `stack` name and they step back behind one another as you
670
673
  drill in: opening a child shifts the parent inward by `stack-gap` (default
671
- `60`px), leaving a peeking band of it visible so the hierarchy reads clearly.
674
+ `60`px), leaving a peeking band of it visible so the hierarchy reads clearly. When the child closes, the parent steps forward again in the same motion, in lockstep with the child sliding out.
672
675
  Closing the child brings the parent forward. Panels with **different names (or
673
676
  none) are independent** and simply layer by open order.
674
677
 
675
- Only the top two open panels keep their dimmed/blurred backdrop; deeper ones go
676
- transparent, so a three-deep stack does not stack three backdrops. Use a stack
678
+ Only the bottom-most open panel paints the dimmed/blurred backdrop; every panel
679
+ above it renders a transparent one, so a three-deep stack does not stack three
680
+ backdrops and a child closing never brightens or re-blurs what is behind. Use a stack
677
681
  for master → detail flows where the trail should stay visible; if the parent
678
682
  should be fully replaced instead, that is what `BbDialog`'s `stack` (which swaps
679
683
  rather than peeks) is for.
@@ -683,13 +687,11 @@ rather than peeks) is for.
683
687
  ```vue
684
688
  <template>
685
689
  <div
686
- class="flex max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-3"
690
+ class="flex max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
687
691
  >
688
692
  <div class="min-w-0">
689
693
  <p class="m-0 text-sm font-medium">Team</p>
690
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
691
- Drill into a member
692
- </p>
694
+ <p class="m-0 text-xs text-(--bb-text-muted)">Drill into a member</p>
693
695
  </div>
694
696
  <BbButton variant="outline" @click="listOpen = true">
695
697
  Team members
@@ -724,13 +726,11 @@ rather than peeks) is for.
724
726
  >
725
727
  <div v-if="selected" class="grid gap-2 text-sm">
726
728
  <p class="m-0">
727
- <span class="text-xs text-[color:var(--bb-text-muted)]">Role</span
728
- ><br />
729
+ <span class="text-xs text-(--bb-text-muted)">Role</span><br />
729
730
  {{ selected.role }}
730
731
  </p>
731
732
  <p class="m-0">
732
- <span class="text-xs text-[color:var(--bb-text-muted)]">Joined</span
733
- ><br />
733
+ <span class="text-xs text-(--bb-text-muted)">Joined</span><br />
734
734
  {{ selected.joined }}
735
735
  </p>
736
736
  </div>
@@ -809,11 +809,11 @@ half-typed edits never survive a close.
809
809
  ```vue
810
810
  <template>
811
811
  <div
812
- class="flex max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-3"
812
+ class="flex max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
813
813
  >
814
814
  <div class="min-w-0">
815
815
  <p class="m-0 text-sm font-medium">INV-2043</p>
816
- <p class="m-0 truncate text-xs text-[color:var(--bb-text-muted)]">
816
+ <p class="m-0 truncate text-xs text-(--bb-text-muted)">
817
817
  {{ saved }}
818
818
  </p>
819
819
  </div>
@@ -880,11 +880,11 @@ genuine click that starts and ends on the backdrop (≤4px travel) does.
880
880
  ```vue
881
881
  <template>
882
882
  <div
883
- class="flex max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-3"
883
+ class="flex max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
884
884
  >
885
885
  <div class="min-w-0">
886
886
  <p class="m-0 text-sm font-medium">PR #1842</p>
887
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
887
+ <p class="m-0 text-xs text-(--bb-text-muted)">
888
888
  Needs a reviewer before merge
889
889
  </p>
890
890
  </div>
@@ -905,7 +905,7 @@ genuine click that starts and ends on the backdrop (≤4px travel) does.
905
905
  label="Reviewer"
906
906
  placeholder="Name or email"
907
907
  />
908
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
908
+ <p class="m-0 text-xs text-(--bb-text-muted)">
909
909
  Backdrop and Escape stay locked — resolve from the footer.
910
910
  </p>
911
911
  </div>