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
@@ -42,11 +42,11 @@ for buttons inside the body. Footer buttons simply set the model back to
42
42
  ```vue
43
43
  <template>
44
44
  <div
45
- class="flex w-full max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-4"
45
+ class="flex w-full max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-4"
46
46
  >
47
47
  <div>
48
48
  <p class="m-0 text-sm font-medium">Issue labels</p>
49
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
49
+ <p class="m-0 text-xs text-(--bb-text-muted)">
50
50
  12 labels · used across Northwind issues
51
51
  </p>
52
52
  </div>
@@ -68,7 +68,7 @@ for buttons inside the body. Footer buttons simply set the model back to
68
68
  label="Label name"
69
69
  placeholder="e.g. Priority"
70
70
  />
71
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
71
+ <p class="m-0 text-xs text-(--bb-text-muted)">
72
72
  Names are unique per workspace and show up in filters.
73
73
  </p>
74
74
  </div>
@@ -108,35 +108,56 @@ A `title` prop renders the standard header (title text + close button) and wires
108
108
  `aria-labelledby` for you. Put form fields in the `default` slot and actions in
109
109
  the `footer` slot — keep exactly one primary action there and align the group to
110
110
  the end. The body area scrolls internally when content overflows; header and
111
- footer stay pinned. This edit-record dialog re-seeds a typed draft from the
112
- record on `@show` so a canceled edit never leaks into the next session and
113
- copies it back on save.
111
+ footer stay pinned. This edit-record dialog builds a fresh draft from the
112
+ selected record on `@show`, drops it on `@hidden`, and copies it back on save
113
+ so a canceled edit never leaks into the next session, and neither does a field
114
+ the next record happens not to have.
114
115
 
115
116
  **Edit-record dialog with a footer action**
116
117
 
117
118
  ```vue
118
119
  <template>
119
- <div
120
- class="flex w-full max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-4"
121
- >
122
- <div class="min-w-0">
123
- <p class="m-0 truncate text-sm font-medium">{{ contact.name }}</p>
124
- <p class="m-0 truncate text-xs text-[color:var(--bb-text-muted)]">
125
- {{ contact.email }}
126
- </p>
127
- </div>
128
- <BbButton
129
- prepend:icon="lucide:pencil"
130
- variant="outline"
131
- @click="open = true"
120
+ <ul class="m-0 flex w-full max-w-sm list-none flex-col gap-2 p-0">
121
+ <li
122
+ v-for="contact in contacts"
123
+ :key="contact.id"
124
+ class="flex items-center justify-between gap-3 rounded-(--bb-radius) border p-3"
132
125
  >
133
- Edit contact
134
- </BbButton>
135
- </div>
136
- <BbDialog v-model="open" size="md" title="Edit contact" @show="seedDraft">
137
- <div class="grid gap-3">
126
+ <div class="min-w-0">
127
+ <p class="m-0 truncate text-sm font-medium">{{ contact.name }}</p>
128
+ <p class="m-0 truncate text-xs text-(--bb-text-muted)">
129
+ {{ contact.email
130
+ }}<template v-if="contact.phone"> · {{ contact.phone }}</template>
131
+ </p>
132
+ </div>
133
+ <BbButton
134
+ :aria-label="`Edit ${contact.name}`"
135
+ prepend:icon="lucide:pencil"
136
+ size="sm"
137
+ variant="outline"
138
+ @click="edit(contact)"
139
+ >
140
+ Edit
141
+ </BbButton>
142
+ </li>
143
+ </ul>
144
+ <BbDialog
145
+ v-model="open"
146
+ size="md"
147
+ title="Edit contact"
148
+ @hidden="clear"
149
+ @show="seed"
150
+ >
151
+ <div v-if="draft" class="grid gap-3">
138
152
  <BbTextInput v-model="draft.name" compact label="Full name" />
139
153
  <BbTextInput v-model="draft.email" compact label="Email" type="email" />
154
+ <BbTextInput
155
+ v-model="draft.phone"
156
+ compact
157
+ label="Phone"
158
+ placeholder="Optional"
159
+ type="tel"
160
+ />
140
161
  <BbSwitch v-model="draft.subscribed" label="Subscribed to updates" />
141
162
  </div>
142
163
  <template #footer>
@@ -149,33 +170,100 @@ copies it back on save.
149
170
  </template>
150
171
 
151
172
  <script setup lang="ts">
152
- import { reactive, ref } from 'vue';
173
+ import { ref } from 'vue';
153
174
  import { BbButton, BbDialog, BbSwitch, BbTextInput } from 'bitboss-ui';
154
175
 
176
+ // `phone` is optional on the record: some contacts simply do not have one.
155
177
  type Contact = {
178
+ id: number;
156
179
  name: string;
157
180
  email: string;
181
+ phone?: string;
158
182
  subscribed: boolean;
159
183
  };
160
184
 
161
- // The record being edited — in a real app this comes from your store/API.
162
- const contact = reactive<Contact>({
163
- name: 'Ada Lovelace',
164
- email: 'ada@analytical.co',
165
- subscribed: true,
185
+ // The records being edited — in a real app these come from your store/API.
186
+ // Two of them on purpose, one WITHOUT the optional field: that is the case a
187
+ // single-record demo can never show going wrong.
188
+ const contacts = ref<Contact[]>([
189
+ {
190
+ id: 1,
191
+ name: 'Ada Lovelace',
192
+ email: 'ada@analytical.co',
193
+ phone: '+44 20 7946 0958',
194
+ subscribed: true,
195
+ },
196
+ {
197
+ id: 2,
198
+ name: 'Grace Hopper',
199
+ email: 'grace@navy.mil',
200
+ subscribed: false,
201
+ },
202
+ ]);
203
+
204
+ // The form binds every field, so the draft must always carry every field.
205
+ // A blank factory guarantees that; the record only overrides what it has.
206
+ type Draft = {
207
+ name: string;
208
+ email: string;
209
+ phone: string;
210
+ subscribed: boolean;
211
+ };
212
+ const blank = (): Draft => ({
213
+ name: '',
214
+ email: '',
215
+ phone: '',
216
+ subscribed: false,
166
217
  });
167
218
 
168
219
  const open = ref(false);
169
- const draft = reactive<Contact>({ ...contact });
220
+ const editing = ref<Contact | null>(null);
221
+ const draft = ref<Draft | null>(null);
170
222
 
171
- // Re-seed on every open: a canceled edit must not leak into the next session.
172
- const seedDraft = () => {
173
- Object.assign(draft, contact);
223
+ const edit = (contact: Contact) => {
224
+ editing.value = contact;
225
+ open.value = true;
226
+ };
227
+
228
+ // Seed on `@show`, and REPLACE the draft rather than merging into it.
229
+ // `Object.assign(draft, record)` copies only the keys the record has: open Grace
230
+ // (no phone) right after Ada (phone set) and Ada's phone is still in the input.
231
+ const seed = () => {
232
+ const record = editing.value;
233
+ if (!record) return;
234
+ draft.value = {
235
+ ...blank(),
236
+ name: record.name,
237
+ email: record.email,
238
+ phone: record.phone ?? '',
239
+ subscribed: record.subscribed,
240
+ };
241
+ };
242
+
243
+ // Drop the draft once the close transition ends, so nothing waits around for
244
+ // the next record — `@hidden` is the teardown hook, not `@hide`.
245
+ const clear = () => {
246
+ draft.value = null;
247
+ editing.value = null;
174
248
  };
175
249
 
176
250
  const save = () => {
177
- // Persist here, then dismiss.
178
- Object.assign(contact, draft);
251
+ const record = editing.value;
252
+ const next = draft.value;
253
+ if (!record || !next) return;
254
+ // Persist here, then dismiss. The optional field goes back to "absent"
255
+ // when it was cleared, so the record never grows an empty string.
256
+ contacts.value = contacts.value.map((contact) =>
257
+ contact.id === record.id
258
+ ? {
259
+ ...contact,
260
+ name: next.name,
261
+ email: next.email,
262
+ phone: next.phone.trim() || undefined,
263
+ subscribed: next.subscribed,
264
+ }
265
+ : contact
266
+ );
179
267
  open.value = false;
180
268
  };
181
269
  </script>
@@ -194,11 +282,11 @@ and the only thing `aria-describedby` points at:
194
282
  ```vue
195
283
  <template>
196
284
  <div
197
- class="flex w-full max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-4"
285
+ class="flex w-full max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-4"
198
286
  >
199
287
  <div>
200
288
  <p class="m-0 text-sm font-medium">INV-2044</p>
201
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
289
+ <p class="m-0 text-xs text-(--bb-text-muted)">
202
290
  Northwind Traders · €1,280.00 · paid
203
291
  </p>
204
292
  </div>
@@ -263,11 +351,11 @@ dismissed invite doesn't resurface half-typed:
263
351
  ```vue
264
352
  <template>
265
353
  <div
266
- class="flex w-full max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-4"
354
+ class="flex w-full max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-4"
267
355
  >
268
356
  <div>
269
357
  <p class="m-0 text-sm font-medium">Team · Northwind Labs</p>
270
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
358
+ <p class="m-0 text-xs text-(--bb-text-muted)">
271
359
  4 members · 1 pending invite
272
360
  </p>
273
361
  </div>
@@ -298,7 +386,7 @@ dismissed invite doesn't resurface half-typed:
298
386
  :items="roles"
299
387
  label="Role"
300
388
  />
301
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
389
+ <p class="m-0 text-xs text-(--bb-text-muted)">
302
390
  They'll get an email with a link that expires in 7 days.
303
391
  </p>
304
392
  </div>
@@ -349,10 +437,18 @@ const send = () => {
349
437
  </script>
350
438
  ```
351
439
 
352
- Two rules keep dialog forms honest:
353
-
354
- - Reset or re-seed the draft when the dialog opens (`@show` is the hook), not
355
- when it closes a dismissed dialog may be reopened with stale state.
440
+ Three rules keep dialog forms honest:
441
+
442
+ - Seed the draft when the dialog opens (`@show` is the hook), and **replace
443
+ it, never merge into it**. `Object.assign(draft, record)` copies only the
444
+ keys the record has, so opening a record whose optional field is absent
445
+ right after one where it was set leaves the previous value in the input —
446
+ nothing warns. Build the draft from a blank factory so every field the form
447
+ binds is always present, then overlay the record
448
+ (`{ ...blank(), phone: record.phone ?? '' }`).
449
+ - Clear the draft on `@hidden` as well. A close must leave nothing behind for
450
+ the next caller; `@hidden` fires after the close transition, so the form
451
+ does not blank mid-fade.
356
452
  - On success, do the work first, then set the model to `false`; the close
357
453
  transition runs while your toast/refresh happens.
358
454
 
@@ -361,7 +457,8 @@ Two rules keep dialog forms honest:
361
457
  Four events bracket the transitions — `show` fires as the model turns `true`,
362
458
  `shown` after the native `showModal()` call and initial focus, `hide` as the
363
459
  model turns `false`, and `hidden` after the close transition completes. Use
364
- `@show` to (re)seed form state, `@hidden` to tear down expensive content.
460
+ `@show` to (re)seed form state, `@hidden` to drop a draft and tear down
461
+ expensive content.
365
462
 
366
463
  Body content is **lazy**: nothing inside the dialog renders until the first
367
464
  open. Set `eager` to render it immediately (e.g. so a chart can pre-measure, or
@@ -435,11 +532,11 @@ element.
435
532
  ```vue
436
533
  <template>
437
534
  <div
438
- class="flex w-full max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-4"
535
+ class="flex w-full max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-4"
439
536
  >
440
537
  <div>
441
538
  <p class="m-0 text-sm font-medium">Finish setting up</p>
442
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
539
+ <p class="m-0 text-xs text-(--bb-text-muted)">
443
540
  Add your company so invoices and invites look right.
444
541
  </p>
445
542
  </div>
@@ -513,13 +610,11 @@ automatically; close only once the work resolves:
513
610
  ```vue
514
611
  <template>
515
612
  <div
516
- class="flex w-full max-w-sm items-center justify-between gap-3 rounded-[var(--bb-radius)] border p-4"
613
+ class="flex w-full max-w-sm items-center justify-between gap-3 rounded-(--bb-radius) border p-4"
517
614
  >
518
615
  <div class="min-w-0">
519
616
  <p class="m-0 truncate text-sm font-medium">{{ projectName }}</p>
520
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
521
- Project · private
522
- </p>
617
+ <p class="m-0 text-xs text-(--bb-text-muted)">Project · private</p>
523
618
  </div>
524
619
  <BbButton
525
620
  prepend:icon="lucide:pencil"
@@ -595,8 +690,10 @@ const save = async () => {
595
690
 
596
691
  Dialogs sharing a `stack` name **replace** one another: only the most recently
597
692
  opened member is visible, the ones beneath are hidden until it closes — the
598
- stack reads as a single surface whose content swaps as you drill in. Dialogs
599
- with different names (or none) are independent and simply layer by open order.
693
+ stack reads as a single surface whose content swaps as you drill in. When a
694
+ member closes, the one beneath crossfades in as it fades out, on the stack's
695
+ single backdrop — the page behind never blinks between them. Dialogs with
696
+ different names (or none) are independent and simply layer by open order.
600
697
  Use a stack for a two-step flow that should not read as two boxes piling up.
601
698
 
602
699
  **Drill into advanced options**
@@ -604,11 +701,11 @@ Use a stack for a two-step flow that should not read as two boxes piling up.
604
701
  ```vue
605
702
  <template>
606
703
  <div
607
- class="flex w-full max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-4"
704
+ class="flex w-full max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-4"
608
705
  >
609
706
  <div>
610
707
  <p class="m-0 text-sm font-medium">Workspace settings</p>
611
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
708
+ <p class="m-0 text-xs text-(--bb-text-muted)">
612
709
  Storage, backups, retention
613
710
  </p>
614
711
  </div>
@@ -621,7 +718,7 @@ Use a stack for a two-step flow that should not read as two boxes piling up.
621
718
  <BbDialog v-model="first" stack="settings" title="Storage">
622
719
  <div class="grid gap-3">
623
720
  <BbSwitch v-model="autoBackup" label="Automatic backups" />
624
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
721
+ <p class="m-0 text-xs text-(--bb-text-muted)">
625
722
  Nightly snapshots keep the last 30 days of changes.
626
723
  </p>
627
724
  <BbButton
@@ -636,7 +733,7 @@ Use a stack for a two-step flow that should not read as two boxes piling up.
636
733
  <BbDialog v-model="second" stack="settings" title="Advanced retention">
637
734
  <div class="grid gap-3">
638
735
  <BbSwitch v-model="keepForever" label="Keep monthly snapshots" />
639
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
736
+ <p class="m-0 text-xs text-(--bb-text-muted)">
640
737
  Monthly snapshots stay available for 12 months after creation.
641
738
  </p>
642
739
  </div>
@@ -694,11 +791,11 @@ link.
694
791
  ```vue
695
792
  <template>
696
793
  <div
697
- class="flex w-full max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-4"
794
+ class="flex w-full max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-4"
698
795
  >
699
796
  <div>
700
797
  <p class="m-0 text-sm font-medium">Projects</p>
701
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
798
+ <p class="m-0 text-xs text-(--bb-text-muted)">
702
799
  Free plan · {{ used }} of {{ limit }} used
703
800
  </p>
704
801
  </div>
@@ -719,7 +816,7 @@ link.
719
816
  using all of them. Upgrade to <strong>Pro</strong> for unlimited
720
817
  projects and priority support.
721
818
  </p>
722
- <p class="m-0 text-xs text-[color:var(--bb-text-muted)]">
819
+ <p class="m-0 text-xs text-(--bb-text-muted)">
723
820
  Billing starts when you confirm — you can cancel anytime.
724
821
  </p>
725
822
  </div>
@@ -846,7 +943,7 @@ default falls back to `config.adaptive`.
846
943
  | `modelValue` | `boolean \| undefined` | `false` | | Used by v-model to open / close the dialog. A dialog with no `v-model` starts closed. |
847
944
  | `offCanvasProps` | `Partial<BbOffCanvasProps> \| undefined` | | | Overrides merged over the offcanvas (mobile) surface's props. The drawer defaults to a bottom, draggable, content-sized sheet (`side: 'bottom'`, `draggable: true`, `size: 'auto'`); use this to override those, set other drawer-only options (… |
848
945
  | `persistent` | `boolean \| undefined` | `false` | | Prevent closing via outside clicks or Escape key. The header close (×) button STILL closes a persistent dialog — that is the documented escape hatch; combine with `hideClose` to remove it, or use `disabled` to block every dismissal. Program… |
849
- | `size` | `Responsive<NonNullable<number \| ({ size?: any; } & string) \| keyof Sizes \| undefined>> \| undefined` | `"sm"` | | Controls the maximum width of the dialog. Accepts named presets from `'xs'` to `'2xl'`, a custom CSS width string, or a numeric pixel value — or a per-breakpoint map (e.g. `{ default: 'sm', lg: 'lg' }`) to switch width responsively. |
946
+ | `size` | `Responsive<NonNullable<number \| ({ size?: any; } & string) \| keyof BbDialogSizes \| undefined>> \| undefined` | `"sm"` | | Controls the maximum width of the dialog. Accepts named presets from `'xs'` to `'2xl'`, a custom CSS width string, or a numeric pixel value — or a per-breakpoint map (e.g. `{ default: 'sm', lg: 'lg' }`) to switch width responsively. |
850
947
  | `stack` | `string \| undefined` | | | Name of the stack this dialog belongs to. Dialogs sharing the same `stack` value replace one another: only the most recently opened stays visible while the others in the stack are hidden beneath it. Dialogs in different stacks — or with no … |
851
948
  | `title` | `string \| undefined` | | | Title text announced to assistive technologies. If omitted, no header is rendered (unless a `header` slot is supplied) and accessibility must be handled manually. |
852
949
  | `transitionDuration` | `number \| undefined` | `250` | | Controls the duration of the open and close animations. |
@@ -71,17 +71,17 @@ yes (the `<BbConfirm>` host mounts once per app, e.g. in the root layout).
71
71
  ```vue
72
72
  <template>
73
73
  <div
74
- class="flex max-w-sm items-center gap-2 rounded-[var(--bb-radius)] border p-2"
74
+ class="flex max-w-sm items-center gap-2 rounded-(--bb-radius) border p-2"
75
75
  >
76
76
  <div
77
77
  aria-hidden="true"
78
- class="flex size-8 shrink-0 items-center justify-center rounded-[var(--bb-radius-sm)] bg-[color:var(--bb-muted)] text-[color:var(--bb-text-muted)]"
78
+ class="flex size-8 shrink-0 items-center justify-center rounded-(--bb-radius-sm) bg-(--bb-muted) text-(--bb-text-muted)"
79
79
  >
80
80
  <span class="text-xs font-medium">PDF</span>
81
81
  </div>
82
82
  <div class="min-w-0 flex-1">
83
83
  <p class="truncate text-sm font-medium">Q3-report.pdf</p>
84
- <p class="text-xs text-[color:var(--bb-text-muted)]">
84
+ <p class="text-xs text-(--bb-text-muted)">
85
85
  248 KB · <span data-last>{{ last }}</span>
86
86
  </p>
87
87
  </div>
@@ -194,11 +194,11 @@ selectable group the menu stays open instead, and the row itself carries
194
194
  ```vue
195
195
  <template>
196
196
  <div
197
- class="flex max-w-sm items-center gap-2 rounded-[var(--bb-radius)] border p-2"
197
+ class="flex max-w-sm items-center gap-2 rounded-(--bb-radius) border p-2"
198
198
  >
199
199
  <div class="min-w-0 flex-1">
200
200
  <p class="truncate text-sm font-medium">bitboss/storefront</p>
201
- <p class="text-xs text-[color:var(--bb-text-muted)]" data-sync>
201
+ <p class="text-xs text-(--bb-text-muted)" data-sync>
202
202
  {{ status }}
203
203
  </p>
204
204
  </div>
@@ -268,7 +268,7 @@ nesting shallow (two levels is plenty).
268
268
  ```vue
269
269
  <template>
270
270
  <div
271
- class="flex max-w-xs items-center justify-between gap-2 rounded-[var(--bb-radius)] border p-2"
271
+ class="flex max-w-xs items-center justify-between gap-2 rounded-(--bb-radius) border p-2"
272
272
  >
273
273
  <span class="text-sm font-medium">Acme Studio</span>
274
274
  <BbDropdown :items="items" placement="bottom-end">
@@ -357,9 +357,9 @@ must point at a _group_ (not a plain item, not a missing key).
357
357
  ```vue
358
358
  <template>
359
359
  <div
360
- class="flex max-w-md flex-wrap items-center gap-2 rounded-[var(--bb-radius)] border p-2"
360
+ class="flex max-w-md flex-wrap items-center gap-2 rounded-(--bb-radius) border p-2"
361
361
  >
362
- <span class="pr-1 text-xs font-medium text-[color:var(--bb-text-muted)]"
362
+ <span class="pr-1 text-xs font-medium text-(--bb-text-muted)"
363
363
  >Invoices</span
364
364
  >
365
365
  <BbDropdown :adaptive="false" :items="items" :width="200">
@@ -378,7 +378,7 @@ must point at a _group_ (not a plain item, not a missing key).
378
378
  <BbDropdownGroup id="sort" v-model="sortBy" />
379
379
  <BbDropdownGroup id="tags" v-model="tags" />
380
380
  </BbDropdown>
381
- <span class="text-xs text-[color:var(--bb-text-muted)]">
381
+ <span class="text-xs text-(--bb-text-muted)">
382
382
  Sort: <span data-sort>{{ sortBy }}</span> · Tags:
383
383
  <span data-tags>{{ tags.join(', ') || 'none' }}</span>
384
384
  </span>
@@ -456,11 +456,11 @@ The full grammar:
456
456
  ```vue
457
457
  <template>
458
458
  <div
459
- class="flex max-w-sm flex-wrap items-center gap-2 rounded-[var(--bb-radius)] border p-2"
459
+ class="flex max-w-sm flex-wrap items-center gap-2 rounded-(--bb-radius) border p-2"
460
460
  >
461
461
  <div class="min-w-0 flex-1">
462
462
  <p class="truncate text-sm font-medium">INV-2048 · Northwind</p>
463
- <p class="text-xs text-[color:var(--bb-text-muted)]">
463
+ <p class="text-xs text-(--bb-text-muted)">
464
464
  Assignees ·
465
465
  <span data-count>{{ assignees.length }} selected</span>
466
466
  </p>
@@ -535,11 +535,9 @@ a valid selection.
535
535
  ```vue
536
536
  <template>
537
537
  <div
538
- class="flex max-w-md flex-wrap items-center gap-2 rounded-[var(--bb-radius)] border p-2"
538
+ class="flex max-w-md flex-wrap items-center gap-2 rounded-(--bb-radius) border p-2"
539
539
  >
540
- <span class="pr-1 text-xs font-medium text-[color:var(--bb-text-muted)]"
541
- >Board</span
542
- >
540
+ <span class="pr-1 text-xs font-medium text-(--bb-text-muted)">Board</span>
543
541
  <BbDropdown
544
542
  :adaptive="false"
545
543
  :items="menu"
@@ -560,7 +558,7 @@ a valid selection.
560
558
  <BbDropdownGroup id="board" v-model="board" />
561
559
  <BbDropdownGroup id="assignees" v-model="assignees" />
562
560
  </BbDropdown>
563
- <span class="text-xs text-[color:var(--bb-text-muted)]">
561
+ <span class="text-xs text-(--bb-text-muted)">
564
562
  <span data-board>{{ board ?? 'all boards' }}</span> ·
565
563
  <span data-assignees>{{ assigneeNames || 'anyone' }}</span>
566
564
  </span>
@@ -771,11 +769,9 @@ the edge slots, to replace one row's content use its key.
771
769
  ```vue
772
770
  <template>
773
771
  <div
774
- class="flex max-w-xs items-center gap-2 rounded-[var(--bb-radius)] border p-2"
772
+ class="flex max-w-xs items-center gap-2 rounded-(--bb-radius) border p-2"
775
773
  >
776
- <span class="text-xs font-medium text-[color:var(--bb-text-muted)]"
777
- >Workspace</span
778
- >
774
+ <span class="text-xs font-medium text-(--bb-text-muted)">Workspace</span>
779
775
  <!-- `adaptive` is pinned off so the demo behaves identically on any
780
776
  viewport; leave it at the project default in real apps. -->
781
777
  <BbDropdown :adaptive="false" :items="items">
@@ -794,7 +790,7 @@ the edge slots, to replace one row's content use its key.
794
790
  (and inside group headers). Payload is read-only. -->
795
791
  <template #item:prepend="{ item }">
796
792
  <BbIcon
797
- class="text-[color:var(--bb-text-muted)]"
793
+ class="text-(--bb-text-muted)"
798
794
  data-env-dot
799
795
  :icon="envIcon(item.key)"
800
796
  size="xs"
@@ -874,7 +870,7 @@ the sheet's pinned header and footer.
874
870
  </BbButton>
875
871
  </template>
876
872
  <template #header>
877
- <div class="px-3 py-2 text-xs font-semibold text-[color:var(--bb-text-muted)]">
873
+ <div class="px-3 py-2 text-xs font-semibold text-(--bb-text-muted)">
878
874
  Jump to month
879
875
  </div>
880
876
  </template>
@@ -903,11 +899,11 @@ behaves exactly as with a click menu.
903
899
  ```vue
904
900
  <template>
905
901
  <div
906
- class="flex max-w-sm items-center gap-2 rounded-[var(--bb-radius)] border p-2"
902
+ class="flex max-w-sm items-center gap-2 rounded-(--bb-radius) border p-2"
907
903
  >
908
904
  <div class="min-w-0 flex-1">
909
905
  <p class="truncate text-sm font-medium">INV-2048</p>
910
- <p class="text-xs text-[color:var(--bb-text-muted)]">
906
+ <p class="text-xs text-(--bb-text-muted)">
911
907
  Northwind · €1,240.00 ·
912
908
  <span data-last>{{ last }}</span>
913
909
  </p>
@@ -1221,7 +1217,7 @@ mid-session never swaps a live menu between flyout and sheet.
1221
1217
  | `eager` | `boolean \| undefined` | `false` | | Prevents rendering content before it's made visible. |
1222
1218
  | `enforceCoherence` | `boolean \| undefined` | `false` | | Default for pipeline groups: drop selections that are no longer present in a group's current items (e.g. after a `dependencies` re-fetch) from the selection and `v-model`. A group's own `enforceCoherence` overrides this. |
1223
1219
  | `id` | `string \| undefined` | | | The identifier of the component. |
1224
- | `items` | `Item[]` | `[]` | yes | Array used to render each dropdown button. They can act as `button`, as `a`, or as `router-link` as they are based on the functionality provided by `BbBaseButton`. Pass a flat array for a single group, nest arrays for divider-separated grou… |
1220
+ | `items` | `BbDropdownItem[]` | `[]` | yes | Array used to render each dropdown button. They can act as `button`, as `a`, or as `router-link` as they are based on the functionality provided by `BbBaseButton`. Pass a flat array for a single group, nest arrays for divider-separated grou… |
1225
1221
  | `loadingText` | `string \| undefined` | | | Default placeholder text shown while a pipeline group's async items load. A group's own `loadingText` overrides this; both fall back to the localized `common.loadingText` (`'Loading...'`). |
1226
1222
  | `noDataText` | `string \| undefined` | | | Default placeholder text shown when a pipeline group resolves to no items. A group's own `noDataText` overrides this; both fall back to the localized `common.noDataText` (`'No data to display'`). |
1227
1223
  | `offCanvasProps` | `Partial<BbOffCanvasProps> \| undefined` | | | Overrides forwarded to the underlying `BbOffCanvas` when `adaptive` renders the sheet on mobile. Merged over the adaptive defaults (`direction: 'bottom'`, `size: 'auto'`, `draggable: true`). Ignored when `adaptive` is `false`. |
@@ -47,20 +47,16 @@ entry's remaining rows are dropped from the menu.
47
47
 
48
48
  ```vue
49
49
  <template>
50
- <div
51
- class="flex max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
52
- >
50
+ <div class="flex max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-3">
53
51
  <div class="flex flex-col gap-0.5">
54
52
  <span class="text-sm font-medium">Invoice INV-2048</span>
55
- <span class="text-xs text-[color:var(--bb-text-muted)]">
53
+ <span class="text-xs text-(--bb-text-muted)">
56
54
  Acme Studio · due Jul 31, 2026
57
55
  </span>
58
56
  </div>
59
57
  <div class="flex items-center gap-2">
60
58
  <BbDropdownButton :items="items" prepend:icon="lucide:check" />
61
- <span class="text-xs text-[color:var(--bb-text-muted)]" data-last>{{
62
- last
63
- }}</span>
59
+ <span class="text-xs text-(--bb-text-muted)" data-last>{{ last }}</span>
64
60
  </div>
65
61
  </div>
66
62
  </template>
@@ -100,12 +96,10 @@ so each export is a real navigation, not an `@click` that fakes a download.
100
96
 
101
97
  ```vue
102
98
  <template>
103
- <div
104
- class="flex max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
105
- >
99
+ <div class="flex max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-3">
106
100
  <div class="flex flex-col gap-0.5">
107
101
  <span class="text-sm font-medium">Q2 revenue report</span>
108
- <span class="text-xs text-[color:var(--bb-text-muted)]">
102
+ <span class="text-xs text-(--bb-text-muted)">
109
103
  Acme Studio · Jul 1 – Sep 30, 2026
110
104
  </span>
111
105
  </div>
@@ -115,9 +109,7 @@ so each export is a real navigation, not an `@click` that fakes a download.
115
109
  prepend:icon="lucide:download"
116
110
  variant="outline"
117
111
  />
118
- <span class="text-xs text-[color:var(--bb-text-muted)]" data-export>{{
119
- last
120
- }}</span>
112
+ <span class="text-xs text-(--bb-text-muted)" data-export>{{ last }}</span>
121
113
  </div>
122
114
  </div>
123
115
  </template>
@@ -205,18 +197,16 @@ manually.
205
197
 
206
198
  ```vue
207
199
  <template>
208
- <div
209
- class="flex max-w-sm flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
210
- >
200
+ <div class="flex max-w-sm flex-col gap-3 rounded-(--bb-radius) border p-3">
211
201
  <div class="flex flex-col gap-0.5">
212
202
  <span class="text-sm font-medium">July product changelog</span>
213
- <span class="text-xs text-[color:var(--bb-text-muted)]">
203
+ <span class="text-xs text-(--bb-text-muted)">
214
204
  bitboss-ui · ready to publish
215
205
  </span>
216
206
  </div>
217
207
  <div class="flex items-center gap-2">
218
208
  <BbDropdownButton :items="items" prepend:icon="lucide:rocket" />
219
- <span class="text-xs text-[color:var(--bb-text-muted)]" data-status>{{
209
+ <span class="text-xs text-(--bb-text-muted)" data-status>{{
220
210
  status
221
211
  }}</span>
222
212
  </div>
@@ -272,16 +262,11 @@ not the one that needs a second thought.
272
262
 
273
263
  ```vue
274
264
  <template>
275
- <div
276
- class="flex max-w-md flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
277
- >
265
+ <div class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border p-3">
278
266
  <div class="flex items-center justify-between gap-3">
279
267
  <div class="flex min-w-0 flex-col gap-0.5">
280
268
  <span class="text-sm font-medium">acme-web · production</span>
281
- <span
282
- class="text-xs text-[color:var(--bb-text-muted)]"
283
- data-deploy-status
284
- >
269
+ <span class="text-xs text-(--bb-text-muted)" data-deploy-status>
285
270
  {{ status }}
286
271
  </span>
287
272
  </div>
@@ -376,12 +361,10 @@ main button itself — so the control always announces exactly what it will do.
376
361
 
377
362
  ```vue
378
363
  <template>
379
- <div
380
- class="flex max-w-md flex-col gap-3 rounded-[var(--bb-radius)] border p-3"
381
- >
364
+ <div class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border p-3">
382
365
  <div class="flex flex-col gap-0.5">
383
366
  <span class="text-sm font-medium">Add usage-based billing #482</span>
384
- <span class="text-xs text-[color:var(--bb-text-muted)]" data-merge-status>
367
+ <span class="text-xs text-(--bb-text-muted)" data-merge-status>
385
368
  {{ status }}
386
369
  </span>
387
370
  </div>