bitboss-ui 3.0.0-beta.57 → 3.0.0-beta.58

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 (146) hide show
  1. package/bin/bitboss-ui-mcp.mjs +113 -26
  2. package/bin/bitboss-ui.mjs +43 -9
  3. package/dist/ai/BbDialog.md +153 -21
  4. package/dist/ai/BbDropdown.md +2 -2
  5. package/dist/ai/BbDropdownButton.md +148 -98
  6. package/dist/ai/BbForm.md +78 -7
  7. package/dist/ai/BbOffCanvas.md +25 -11
  8. package/dist/ai/BbTable.md +9 -1
  9. package/dist/ai/changelog.json +2015 -5
  10. package/dist/ai/components.json +87 -85
  11. package/dist/ai/composables/useBbDialogContext.md +26 -19
  12. package/dist/ai/composables/useBbFormContext.md +8 -10
  13. package/dist/ai/composables/useBbOffCanvasContext.md +4 -2
  14. package/dist/ai/guides/agent-contract.md +1 -1
  15. package/dist/ai/guides/ai-router.md +2 -1
  16. package/dist/ai/guides/eslint-rules.md +2 -2
  17. package/dist/ai/guides/icons-policy.md +6 -7
  18. package/dist/ai/guides/migration/components/bb-dropdown-button.md +32 -9
  19. package/dist/ai/guides/migration/v2-to-v3.md +50 -2
  20. package/dist/ai/guides/migration/v3-beta-tooling.md +36 -9
  21. package/dist/ai/helpers/inertia/manifest.json +1 -1
  22. package/dist/ai/index.md +2 -2
  23. package/dist/ai/manifest/common.json +119 -0
  24. package/dist/ai/manifest/components/BbAccordion.json +9 -10
  25. package/dist/ai/manifest/components/BbAlert.json +14 -15
  26. package/dist/ai/manifest/components/BbAsterisk.json +1 -2
  27. package/dist/ai/manifest/components/BbAvatar.json +8 -9
  28. package/dist/ai/manifest/components/BbBadge.json +10 -11
  29. package/dist/ai/manifest/components/BbBadgeButton.json +41 -42
  30. package/dist/ai/manifest/components/BbBaseButton.json +43 -44
  31. package/dist/ai/manifest/components/BbBaseCheckboxIcon.json +9 -10
  32. package/dist/ai/manifest/components/BbBaseRadioIcon.json +8 -9
  33. package/dist/ai/manifest/components/BbBaseSwitchIcon.json +9 -10
  34. package/dist/ai/manifest/components/BbBreadcrumbs.json +10 -11
  35. package/dist/ai/manifest/components/BbButton.json +52 -53
  36. package/dist/ai/manifest/components/BbCalendar.json +29 -30
  37. package/dist/ai/manifest/components/BbCheckbox.json +42 -43
  38. package/dist/ai/manifest/components/BbCheckboxGroup.json +61 -62
  39. package/dist/ai/manifest/components/BbCollapsible.json +6 -7
  40. package/dist/ai/manifest/components/BbColorInput.json +67 -68
  41. package/dist/ai/manifest/components/BbColorPalette.json +22 -23
  42. package/dist/ai/manifest/components/BbConfirm.json +1 -2
  43. package/dist/ai/manifest/components/BbDatePicker.json +42 -43
  44. package/dist/ai/manifest/components/BbDatePickerInput.json +74 -75
  45. package/dist/ai/manifest/components/BbDialog.json +32 -32
  46. package/dist/ai/manifest/components/BbDropdown.json +33 -34
  47. package/dist/ai/manifest/components/BbDropdownButton.json +72 -33
  48. package/dist/ai/manifest/components/BbDropdownGroup.json +4 -4
  49. package/dist/ai/manifest/components/BbDropzone.json +21 -22
  50. package/dist/ai/manifest/components/BbForm.json +10 -9
  51. package/dist/ai/manifest/components/BbHoverCard.json +16 -17
  52. package/dist/ai/manifest/components/BbIcon.json +4 -5
  53. package/dist/ai/manifest/components/BbIndicator.json +8 -9
  54. package/dist/ai/manifest/components/BbNumberInput.json +66 -67
  55. package/dist/ai/manifest/components/BbOffCanvas.json +32 -32
  56. package/dist/ai/manifest/components/BbPagination.json +14 -15
  57. package/dist/ai/manifest/components/BbPopover.json +21 -22
  58. package/dist/ai/manifest/components/BbProgress.json +6 -7
  59. package/dist/ai/manifest/components/BbPtScope.json +1 -1
  60. package/dist/ai/manifest/components/BbRadio.json +37 -38
  61. package/dist/ai/manifest/components/BbRadioGroup.json +62 -63
  62. package/dist/ai/manifest/components/BbRating.json +40 -41
  63. package/dist/ai/manifest/components/BbSelect.json +77 -78
  64. package/dist/ai/manifest/components/BbSelectPopover.json +51 -52
  65. package/dist/ai/manifest/components/BbSlider.json +40 -41
  66. package/dist/ai/manifest/components/BbSmoothHeight.json +7 -8
  67. package/dist/ai/manifest/components/BbSpinner.json +3 -4
  68. package/dist/ai/manifest/components/BbSwitch.json +42 -43
  69. package/dist/ai/manifest/components/BbSwitchGroup.json +62 -63
  70. package/dist/ai/manifest/components/BbTable.json +68 -69
  71. package/dist/ai/manifest/components/BbTabs.json +27 -28
  72. package/dist/ai/manifest/components/BbTabsList.json +2 -3
  73. package/dist/ai/manifest/components/BbTabsPanels.json +1 -2
  74. package/dist/ai/manifest/components/BbTabsRoot.json +26 -26
  75. package/dist/ai/manifest/components/BbTag.json +48 -49
  76. package/dist/ai/manifest/components/BbTextInput.json +64 -65
  77. package/dist/ai/manifest/components/BbTextarea.json +59 -60
  78. package/dist/ai/manifest/components/BbTimePicker.json +26 -27
  79. package/dist/ai/manifest/components/BbTimePickerInput.json +56 -57
  80. package/dist/ai/manifest/components/BbToast.json +1 -2
  81. package/dist/ai/manifest/components/BbTooltip.json +16 -17
  82. package/dist/ai/manifest/components/BbTree.json +4 -5
  83. package/dist/ai/manifest/components/ChipsBox.json +3 -3
  84. package/dist/ai/manifest/components/ClearableButton.json +1 -1
  85. package/dist/ai/manifest/components/CommaBox.json +1 -1
  86. package/dist/ai/manifest/components/CommonField.json +32 -32
  87. package/dist/ai/manifest/components/CommonFieldInput.json +15 -15
  88. package/dist/ai/manifest/components/CommonPopover.json +17 -17
  89. package/dist/ai/manifest/components/CommonTimeSelector.json +9 -9
  90. package/dist/ai/manifest/components/FlatListBox.json +14 -14
  91. package/dist/ai/manifest/components/GroupedListBox.json +15 -15
  92. package/dist/ai/manifest/components/ListBox.json +17 -17
  93. package/dist/ai/manifest/components/OptionsContainer.json +16 -16
  94. package/dist/ai/manifest/index.json +89 -85
  95. package/dist/ai/manifest/meta.json +42 -2
  96. package/dist/ai/manifest/types.advanced.json +10 -10
  97. package/dist/ai/manifest/types.api.json +51 -49
  98. package/dist/ai/recipes/inertia/approvals-inbox.md +6 -0
  99. package/dist/ai/recipes/inertia/inline-edit-workspace.md +6 -0
  100. package/dist/ai/recipes/inertia/onboarding.md +6 -0
  101. package/dist/ai/recipes/inertia/ownership-atlas.md +6 -0
  102. package/dist/ai/recipes/inertia/records-workspace.md +6 -0
  103. package/dist/ai/recipes/inertia/settings-workspace.md +6 -0
  104. package/dist/ai/recipes/inertia/upload-center.md +6 -0
  105. package/dist/ai/source/BbButton.md +11 -1
  106. package/dist/ai/source/BbDialog.md +55 -18
  107. package/dist/ai/source/BbDropdownButton.md +103 -112
  108. package/dist/ai/source/BbForm.md +63 -13
  109. package/dist/ai/source/BbOffCanvas.md +51 -16
  110. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +1 -1
  111. package/dist/components/BbButton/types.d.ts +6 -0
  112. package/dist/components/BbDialog/BbDialog.vue.d.ts +2 -2
  113. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +191 -181
  114. package/dist/components/BbDialog/types.d.ts +21 -13
  115. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +143 -100
  116. package/dist/components/BbDropdownButton/types.d.ts +25 -15
  117. package/dist/components/BbForm/BbForm.vue.d.ts +3 -3
  118. package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +195 -188
  119. package/dist/components/BbForm/formSchema.d.ts +9 -0
  120. package/dist/components/BbHost/BbHostOpening.js +1 -1
  121. package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +2 -2
  122. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +242 -234
  123. package/dist/components/BbOffCanvas/types.d.ts +21 -13
  124. package/dist/composables/formSubmitLock.d.ts +6 -1
  125. package/dist/composables/hostOpenings.d.ts +10 -4
  126. package/dist/composables/hostOpenings.js +1 -1
  127. package/dist/composables/overlayRegistry.d.ts +22 -9
  128. package/dist/composables/overlayRegistry.js +175 -132
  129. package/dist/composables/useBbDialogContext.d.ts +29 -11
  130. package/dist/composables/useBbDialogContext.js +17 -8
  131. package/dist/composables/useBbFormContext.d.ts +12 -7
  132. package/dist/composables/useBbFormContext.js +7 -6
  133. package/dist/composables/useOnDemandOpener.d.ts +6 -13
  134. package/dist/composables/useOnDemandOpener.js +47 -22
  135. package/dist/directives/overlayTrigger.d.ts +4 -2
  136. package/dist/directives/overlayTrigger.js +1 -0
  137. package/dist/index.d.ts +2 -2
  138. package/dist/llms-full.txt +625 -203
  139. package/dist/llms-medium.txt +3 -2
  140. package/dist/llms.txt +2 -2
  141. package/dist/styles.css +1 -1
  142. package/dist/utils/versionCheck.js +1 -1
  143. package/llms.txt +2 -2
  144. package/package.json +1 -1
  145. package/scripts/lib/agent-manifest.mjs +231 -18
  146. package/scripts/lib/validate-bb-markup.mjs +53 -9
@@ -1161,6 +1161,19 @@ function getComponent({
1161
1161
  const type = (manifest.types ?? []).find((t) => t.name === name);
1162
1162
  if (type) {
1163
1163
  const use = type.usedBy?.[0];
1164
+ // Q74.1: a component's own Props type is that component's props.
1165
+ const propsOf =
1166
+ type.component && name === `${type.component}Props`
1167
+ ? type.component
1168
+ : null;
1169
+ // Q74.2: `fields` stand in for a declaration that adds nothing.
1170
+ const shape = type.definition
1171
+ ? '`definition` above is the type itself'
1172
+ : type.fields
1173
+ ? '`fields` above are its shape'
1174
+ : propsOf
1175
+ ? `Its fields are ${possessive(propsOf)} props: get_component('${propsOf}')`
1176
+ : 'It is described above';
1164
1177
  return {
1165
1178
  found: true,
1166
1179
  kind: 'type',
@@ -1179,9 +1192,10 @@ function getComponent({
1179
1192
  ...(type.fields ? { fields: type.fields } : {}),
1180
1193
  // A union of private members: each member's fields (Q51.21).
1181
1194
  ...(type.members ? { members: type.members } : {}),
1195
+ ...(propsOf ? { propsOf } : {}),
1182
1196
  import: `import type { ${type.replacedBy ?? name} } from 'bitboss-ui';`,
1183
1197
  hint: use
1184
- ? `A type, not a component: it types ${possessive(use.component)} \`${use.name}\` ${use.surface}. \`definition\` above is the type itself; call get_component('${use.component}') to see it in use.`
1198
+ ? `A type, not a component: it types ${possessive(use.component)} \`${use.name}\` ${use.surface}. ${shape}; call get_component('${use.component}') to see it in use.`
1185
1199
  : 'A type, not a component — import it for the value you pass, never write it as a tag.',
1186
1200
  };
1187
1201
  }
@@ -2124,7 +2138,11 @@ function docIndex() {
2124
2138
  ? `accepted values ${type.values.join(' | ')}`
2125
2139
  : type.fields
2126
2140
  ? `object shape, fields ${type.fields.join('; ')}`
2127
- : String(type.definition ?? '')) +
2141
+ : type.definition
2142
+ ? String(type.definition)
2143
+ : type.component && type.name === `${type.component}Props`
2144
+ ? `the props of ${type.component}`
2145
+ : '') +
2128
2146
  `. import type { ${type.name} } from 'bitboss-ui'.`,
2129
2147
  read: { tool: 'get_component', args: { name: type.name } },
2130
2148
  });
@@ -2556,6 +2574,66 @@ function changeLine(row, focus) {
2556
2574
  return `${row.id} ${row.summary ?? firstSentence(row.description, 160)}`;
2557
2575
  }
2558
2576
 
2577
+ /**
2578
+ * One page of `[release, line]` pairs, grouped by release: it ends at
2579
+ * CHANGES_PAGE lines or CHANGES_PAGE_CHARS of them, so its size holds however
2580
+ * long the lines grow; the rest is a cursor away.
2581
+ */
2582
+ function pageLines(lines, cursor) {
2583
+ const start = Math.max(0, Number(cursor ?? 0) || 0);
2584
+ let end = start;
2585
+ for (let used = 0; end < lines.length && end - start < CHANGES_PAGE; end++) {
2586
+ used += lines[end][1].length + 3;
2587
+ if (used > CHANGES_PAGE_CHARS && end > start) break;
2588
+ }
2589
+ const byRelease = {};
2590
+ for (const [release, line] of lines.slice(start, end))
2591
+ (byRelease[release] ??= []).push(line);
2592
+ return {
2593
+ byRelease,
2594
+ more:
2595
+ end < lines.length
2596
+ ? { nextCursor: String(end), remaining: lines.length - end }
2597
+ : {},
2598
+ };
2599
+ }
2600
+
2601
+ /**
2602
+ * Q74.4: what the releases in range ADDED, as `[release, "area · what"]`,
2603
+ * oldest first; `unreleased` only when `until` is open. A component keeps
2604
+ * the lines of its own area (BbDialog → `dialog`) and those whose commits
2605
+ * touched its folder (a family scope: `inputs`).
2606
+ */
2607
+ function addedPairs(doc, { since, until, component }) {
2608
+ const area = component
2609
+ ? String(component).replace(/^Bb/, '').toLowerCase()
2610
+ : null;
2611
+ return Object.entries(doc.added ?? {})
2612
+ .filter(([at]) => {
2613
+ if (at === 'unreleased') return until == null;
2614
+ if (since != null && compareVersions(at, String(since)) <= 0)
2615
+ return false;
2616
+ if (until != null && compareVersions(at, String(until)) > 0) return false;
2617
+ return true;
2618
+ })
2619
+ .flatMap(([at, entries]) =>
2620
+ entries
2621
+ .map((entry) =>
2622
+ typeof entry === 'string' ? { text: entry, components: [] } : entry
2623
+ )
2624
+ .filter(
2625
+ ({ text, components }) =>
2626
+ !area ||
2627
+ text.split(' · ')[0] === area ||
2628
+ components.includes(String(component))
2629
+ )
2630
+ .map(({ text }) => [at, text])
2631
+ );
2632
+ }
2633
+
2634
+ const ADDED_HINT =
2635
+ 'Fixes are not tracked. What the range added, one line per feature: list_changes({ since, kind: "added" }).';
2636
+
2559
2637
  function changeLines(rows, cursor, withClassMap = false, focus) {
2560
2638
  const lines = [];
2561
2639
  const classRenames = {};
@@ -2576,25 +2654,12 @@ function changeLines(rows, cursor, withClassMap = false, focus) {
2576
2654
  ? -1
2577
2655
  : compareVersions(a, b)
2578
2656
  );
2657
+ const page = pageLines(lines, cursor);
2658
+ // The class map rides on the first page only.
2579
2659
  const start = Math.max(0, Number(cursor ?? 0) || 0);
2580
- // A page ends at CHANGES_PAGE lines or CHANGES_PAGE_CHARS of them, so its
2581
- // size holds however long the summaries grow; the rest is a cursor away.
2582
- let end = start;
2583
- for (let used = 0; end < lines.length && end - start < CHANGES_PAGE; end++) {
2584
- used += lines[end][1].length + 3;
2585
- if (used > CHANGES_PAGE_CHARS && end > start) break;
2586
- }
2587
- const byRelease = {};
2588
- for (const [release, line] of lines.slice(start, end))
2589
- (byRelease[release] ??= []).push(line);
2590
2660
  return {
2591
- changes: byRelease,
2592
- ...(end < lines.length
2593
- ? {
2594
- nextCursor: String(end),
2595
- remaining: lines.length - end,
2596
- }
2597
- : {}),
2661
+ changes: page.byRelease,
2662
+ ...page.more,
2598
2663
  ...(Object.keys(classRenames).length && start === 0 && !withClassMap
2599
2664
  ? {
2600
2665
  classRenames: `${Object.keys(classRenames).length} .bb-* class renames (the beta.36 sweep): the map with surface: 'class'`,
@@ -2692,6 +2757,24 @@ function listChanges({
2692
2757
  }
2693
2758
  }
2694
2759
 
2760
+ if (kind === 'added') {
2761
+ const pairs = addedPairs(doc, { since, until, component });
2762
+ const page = pageLines(pairs, cursor);
2763
+ return {
2764
+ version: doc.version,
2765
+ since: since ?? null,
2766
+ until: until ?? doc.version,
2767
+ ...(component ? { filteredBy: { component } } : {}),
2768
+ count: pairs.length,
2769
+ added: page.byRelease,
2770
+ ...page.more,
2771
+ hint:
2772
+ pairs.length === 0
2773
+ ? 'Nothing added in that range (fixes are not tracked).'
2774
+ : 'One "area · what" line per feature, grouped by release, from its feat commit. Breaking changes: the same call without kind. Next page: cursor: nextCursor.',
2775
+ };
2776
+ }
2777
+
2695
2778
  const rows = detail === 'rows';
2696
2779
  const filtered = (doc.breaking ?? []).filter(
2697
2780
  (row) =>
@@ -2751,7 +2834,8 @@ function listChanges({
2751
2834
  Object.fromEntries(Object.entries(bucket).filter(([, n]) => n > 0)),
2752
2835
  ])
2753
2836
  ),
2754
- hint: 'Counts only (a missing count is 0). What a bump breaks: list_changes({ since: "<your version>" }), one line per change; from v2: since: "2". One component: { component: "BbSelect" }. Full rows: detail: "rows".',
2837
+ added: addedPairs(doc, {}).length,
2838
+ hint: 'Counts only (a missing count is 0). What a bump breaks: list_changes({ since: "<your version>" }), one line per change; from v2: since: "2". One component: { component: "BbSelect" }. Full rows: detail: "rows". `added`: features, kind: "added".',
2755
2839
  };
2756
2840
  }
2757
2841
 
@@ -2808,10 +2892,11 @@ function listChanges({
2808
2892
  count: inRange.length,
2809
2893
  ...changeLines(inRange, cursor, surface === 'class', component),
2810
2894
  guide: doc.guide,
2895
+ added: addedPairs(doc, { since, until, component }).length,
2811
2896
  hint:
2812
2897
  inRange.length === 0
2813
- ? 'No breaking changes recorded in that range — features and fixes are deliberately not tracked here, so an empty result means nothing BREAKS, not that nothing changed.'
2814
- : 'One line per change, grouped by the release that shipped it (id, components, old → new, or the break in one sentence). Whole rows: detail: "rows". The `guide` page explains each. Next page: cursor: nextCursor.',
2898
+ ? `No breaking changes recorded in that range: nothing BREAKS. ${ADDED_HINT}`
2899
+ : `One line per change, grouped by the release that shipped it (id, components, old → new, or the break in one sentence). Whole rows: detail: "rows". The \`guide\` page explains each. Next page: cursor: nextCursor. \`added\`: features, kind: "added".`,
2815
2900
  };
2816
2901
  }
2817
2902
 
@@ -2824,7 +2909,7 @@ function listChanges({
2824
2909
  guide: doc.guide,
2825
2910
  hint:
2826
2911
  inRange.length === 0
2827
- ? 'No breaking changes recorded in that range — features and fixes are deliberately not tracked here, so an empty result means nothing BREAKS, not that nothing changed.'
2912
+ ? `No breaking changes recorded in that range: nothing BREAKS. ${ADDED_HINT}`
2828
2913
  : `Each entry names the component and the new spelling; \`guide\` is the migration page with the full explanation. ${ROW_FIELDS}`,
2829
2914
  };
2830
2915
  }
@@ -3018,7 +3103,7 @@ function buildServer() {
3018
3103
  'list_changes',
3019
3104
  {
3020
3105
  description:
3021
- 'Breaking changes between bitboss-ui releases — what breaks if you bump. Call with since=<the version you are on>: one line per change, grouped by release. No arguments: counts per release. Narrow with component / surface / kind; detail: "rows" for the full records. Only BREAKING changes are tracked; features and fixes are not.',
3106
+ 'Breaking changes between bitboss-ui releases — what breaks if you bump. Call with since=<the version you are on>: one line per change, grouped by release. No arguments: counts per release. Narrow with component / surface / kind; detail: "rows" for the full records. kind: "added" lists what the range added (features); fixes are not tracked.',
3022
3107
  inputSchema: {
3023
3108
  since: z
3024
3109
  .string()
@@ -3053,9 +3138,11 @@ function buildServer() {
3053
3138
  'Only renames of this surface (`item` = a field of an `items` entry, e.g. a dropdown row `key` → `value`).'
3054
3139
  ),
3055
3140
  kind: z
3056
- .enum(['rename', 'behaviour'])
3141
+ .enum(['rename', 'behaviour', 'added'])
3057
3142
  .optional()
3058
- .describe('Only renames, or only behaviour changes.'),
3143
+ .describe(
3144
+ "Only renames, or only behaviour changes; 'added': what the range added (features), one line each."
3145
+ ),
3059
3146
  detail: z
3060
3147
  .enum(['lines', 'rows'])
3061
3148
  .optional()
@@ -40,8 +40,8 @@
40
40
  * add Copy Inertia helpers into the app as files it owns
41
41
  * (scripts/lib/helpers-add.mjs); `helpers list|diff|update`
42
42
  * reports on them and refreshes the ones the app has not edited.
43
- * changelog Breaking changes between two releases — `--since <version>`
44
- * answers "what breaks if I bump", without MCP. Reads the
43
+ * changelog Breaking changes between two releases, then what they added —
44
+ * `--since <version>` answers "what breaks if I bump", without MCP. Reads the
45
45
  * installed `dist/ai/changelog.json`.
46
46
  * mcp Start a stdio MCP server exposing the dist/ai knowledge base
47
47
  * (search_docs, search_components, get_component, get_composable,
@@ -167,7 +167,7 @@ function coreRules(version) {
167
167
  1. **Start at the AI Router**: \`${ROUTER}\` — which file to open next (do not dump the full catalogue into context).
168
168
  2. **Obey the agent contract**: \`${CONTRACT}\`.
169
169
  3. **Prefer \`Bb*\` consumer components**; never use entries marked "Reference Only - Internal" unless the user explicitly asks for internals. (\`BbBaseButton\` and the \`BbBase*Icon\` glyphs are consumer API; the \`Common*\` building blocks are not.)
170
- 4. **Never invent prop/event/slot names** — open \`${KNOWLEDGE_BASE}/<ComponentName>.md\` (Usage & Guidelines first) or its API file \`${KNOWLEDGE_BASE}/manifest/components/<ComponentName>.json\` (one member per line; the list is \`${KNOWLEDGE_BASE}/manifest/index.json\`). Never read \`components.json\` whole: it is the 1.2 MB file the tools use.
170
+ 4. **Never invent prop/event/slot names** — open \`${KNOWLEDGE_BASE}/<ComponentName>.md\` (Usage & Guidelines first) or its API file \`${KNOWLEDGE_BASE}/manifest/components/<ComponentName>.json\` (one member per line; a row marked \`common: true\` is whole in \`${KNOWLEDGE_BASE}/manifest/common.json\`; the list is \`${KNOWLEDGE_BASE}/manifest/index.json\`). Never read \`components.json\` whole: it is the 1.2 MB file the tools use.
171
171
  5. **Icons**: discover names first; never invent them (\`${KNOWLEDGE_BASE}/guides/icons-policy.md\`).
172
172
  6. **Item-driven components**: declarative \`items\` (an object row reads its own \`label\` and \`value\`; \`item-text\` / \`item-value\` only for other field names, never to restate those two), \`dependencies\`, \`enforce-coherence\` — no manual watchers for coherence.
173
173
  7. **Whole pages**: one recipe under \`${KNOWLEDGE_BASE}/recipes/{vue|nuxt|inertia}/\` (see the router table).
@@ -780,7 +780,7 @@ function printFindingLegend(findings) {
780
780
  */
781
781
  /**
782
782
  * `changelog [--since <v>] [--until <v>] [--json]` — what breaks between two
783
- * releases, for anyone not driving the MCP server (a human at a terminal, CI,
783
+ * releases, then what they added (Q74.4: one line per feature), for anyone not driving the MCP server (a human at a terminal, CI,
784
784
  * or an agent whose harness has no MCP). Same data and same range rules as the
785
785
  * `list_changes` tool; this is the transport-free path to it.
786
786
  */
@@ -823,6 +823,22 @@ function changelogCommand({ since, until, json: jsonMode }) {
823
823
  if (until != null && compareReleaseVersions(at, until) > 0) return false;
824
824
  return true;
825
825
  });
826
+ // Q74.4: what the range ADDED (feat commits, one line each), oldest first.
827
+ const inRange = (at) => {
828
+ if (!ranged) return true;
829
+ if (at === 'unreleased') return until == null;
830
+ if (since != null && compareReleaseVersions(at, since) <= 0) return false;
831
+ if (until != null && compareReleaseVersions(at, until) > 0) return false;
832
+ return true;
833
+ };
834
+ const added = Object.entries(doc.added ?? {})
835
+ .filter(([at]) => inRange(at))
836
+ .flatMap(([at, entries]) =>
837
+ entries.map((entry) => ({
838
+ at,
839
+ line: typeof entry === 'string' ? entry : entry.text,
840
+ }))
841
+ );
826
842
 
827
843
  if (jsonMode) {
828
844
  console.log(
@@ -833,6 +849,7 @@ function changelogCommand({ since, until, json: jsonMode }) {
833
849
  until: until ?? doc.version,
834
850
  count: rows.length,
835
851
  breaking: rows,
852
+ added: added.map(({ at, line }) => `[${at}] ${line}`),
836
853
  guide: doc.guide,
837
854
  },
838
855
  null,
@@ -845,13 +862,26 @@ function changelogCommand({ since, until, json: jsonMode }) {
845
862
  const range = ranged
846
863
  ? `${since ?? 'the start of v3'} → ${until ?? doc.version}`
847
864
  : `the whole v2 → v3 upgrade`;
865
+ const printAdded = () => {
866
+ if (!added.length) return;
867
+ // Compact by default: the whole v3 line is hundreds of lines.
868
+ if (!ranged) {
869
+ console.log(
870
+ `\n${added.length} addition(s) over ${range}: list one hop with --since <your version>.`
871
+ );
872
+ return;
873
+ }
874
+ console.log(`\n${added.length} addition(s), ${range}:\n`);
875
+ for (const { at, line } of added) console.log(` [${at}] ${line}`);
876
+ };
848
877
  if (rows.length === 0) {
849
878
  // "Nothing breaks" and "I could not tell you" must not look alike.
850
879
  console.log(
851
880
  `[bitboss-ui] changelog: no BREAKING changes in ${range}.\n` +
852
- ' Features and fixes are deliberately not tracked here, so this\n' +
853
- ' means nothing breaks — not that nothing changed.'
881
+ ' Fixes are not tracked here, so this means nothing\n' +
882
+ ' breaks — not that nothing changed.'
854
883
  );
884
+ printAdded();
855
885
  return;
856
886
  }
857
887
  console.log(
@@ -874,6 +904,7 @@ function changelogCommand({ since, until, json: jsonMode }) {
874
904
  }
875
905
  if (entry.guide) console.log(` ${entry.guide}`);
876
906
  }
907
+ printAdded();
877
908
  console.log(`\nFull migration guide: ${doc.guide}`);
878
909
  }
879
910
 
@@ -1651,7 +1682,8 @@ Commands:
1651
1682
  \`bitboss-ui/validated\`, retired: everything is in
1652
1683
  \`bitboss-ui\`; \`--fix\` rewrites it in .vue files),
1653
1684
  \`unregistered-rule\` (a string or object rule no
1654
- \`defineRule\` in the project registers; a
1685
+ \`defineRule\` in the project registers (in a .md
1686
+ page, also one the page's own fences show); a
1655
1687
  registration it cannot read is named, never skipped:
1656
1688
  list such names under \`registeredRules\` in
1657
1689
  bitboss-ui.check.json),
@@ -1721,8 +1753,10 @@ Commands:
1721
1753
  Breaking changes between two releases — "what breaks if I
1722
1754
  bump". \`--since\` is the version you are ON, and is
1723
1755
  exclusive. With no range, every break in the v2 → v3
1724
- upgrade. Only BREAKING changes are tracked, so an empty
1725
- result means nothing breaks, not that nothing changed.
1756
+ upgrade. Then what the range added, one line per feature
1757
+ (with no range, only their count). Fixes are not tracked,
1758
+ so no breaking changes means nothing breaks, not that
1759
+ nothing changed.
1726
1760
  npx bitboss-ui changelog --since 3.0.0-beta.20
1727
1761
  Pair it with \`check --manifest\` to turn the list into the
1728
1762
  files in YOUR app that break.
@@ -203,8 +203,9 @@ reads `data` in two places:
203
203
  `data` (typed `any`, so `data.name` reads with no cast);
204
204
  - in code, `useBbDialogContext<T>(id)` returns it typed, along with `open`,
205
205
  `show(data?)` and `hide()`. A component inside the dialog can call it too.
206
- On an open dialog, `show(newData)` replaces the data and a bare `show()`
207
- (or a trigger without `data`) keeps it.
206
+ A `show()` on an open dialog changes nothing: it joins the opening (its
207
+ promise, its data); the new data, trigger and `onComplete` are ignored. A
208
+ show during the close transition opens it again.
208
209
 
209
210
  `data` stays set until the close transition has run, so the content never
210
211
  flashes empty while the dialog fades out. The edit-record example in
@@ -241,7 +242,8 @@ The rules:
241
242
  - **An open before the dialog mounts waits for it.** A `show()`, or a
242
243
  `v-bb-dialog` click whose own handler renders the dialog through a `v-if`,
243
244
  is held until the dialog registers, so one click opens it (up to 1s; then
244
- it settles `'missing'` with a dev warning, and `onComplete` never runs).
245
+ it settles `'missing'` with a dev warning, and `onComplete` never runs). A
246
+ `hide()` meanwhile cancels a held `show()`, silently: it settles `'hide'`.
245
247
  The warning suggests the nearest registered id for a typo, and says when a
246
248
  dialog with that id exists but is out of reach (inside an on-demand dialog,
247
249
  which is private, or parked by KeepAlive) or is a `BbOffCanvas`.
@@ -323,7 +325,6 @@ const replace = (saved: Member) => {
323
325
  | `'close'` | the ✕ or the slots' `close()` |
324
326
  | `'swipe'` | the phone sheet swiped away |
325
327
  | `'hide'` | `hide()`, or the model set to `false` (also a bound `v-model` whose parent refused the open) |
326
- | `'replaced'` | a newer `show()` on the open dialog took over |
327
328
  | `'missing'` | no dialog with that id mounted within 1s |
328
329
  | `'unmounted'` | the dialog unmounted (or KeepAlive parked it), or a waiting `show()`'s caller went away |
329
330
  | `'failed'` | an on-demand dialog could not render |
@@ -340,6 +341,101 @@ const replace = (saved: Member) => {
340
341
  flags the unawaited promise of a bare `edit.show(row)`, write
341
342
  `void edit.show(row)`.
342
343
 
344
+ #### The page saves: a promise from `onComplete`
345
+
346
+ When the page owns the save (it knows the endpoint, the list to update),
347
+ `onComplete` returns the request's promise. `complete(value)` calls it first,
348
+ and the dialog stays open and **locked** until it settles: the ✕, Cancel,
349
+ Escape and the backdrop are refused, and a footer `BbButton` whose click
350
+ returns `complete(value)` spins. Resolved, the dialog closes and the opening
351
+ settles `{ completed: true, value }`; rejected, it stays open with its data
352
+ and `complete()` rejects with the same error, so the dialog can show it.
353
+
354
+ ```vue
355
+ <template>
356
+ <BbButton v-bb-dialog="{ id: 'rename', data: row, onComplete: save }">
357
+ Rename
358
+ </BbButton>
359
+ <BbDialog id="rename" v-slot="{ data, complete }" title="Rename">
360
+ <BbForm :model="data" @submit="() => complete(data)">
361
+ <BbTextInput label="Name" name="name" />
362
+ <BbButton type="submit">Save</BbButton>
363
+ </BbForm>
364
+ </BbDialog>
365
+ </template>
366
+ ```
367
+
368
+ From code: `edit.show(row, { onComplete: save })` (both the id and the
369
+ component handle). A `save` function passed in `data` is no longer needed.
370
+ A sync `onComplete` (or none) closes at once, as before.
371
+
372
+ `onComplete` is just a function. A trigger inside a child component gets the
373
+ page's save the way your app already shares things: a function prop
374
+ (`:save="saveMember"`), a page `provide` / `inject`, or a store action. Not an
375
+ emit: an emit cannot return the promise.
376
+
377
+ To show a rejected save's field errors, keep them in a `ref` bound on the
378
+ form, `<BbForm :field-errors="fieldErrors">` (BbForm § The page's errors
379
+ object): the dialog stays open, and each message lands under its field until
380
+ the user edits it.
381
+
382
+ ```vue
383
+ <script setup lang="ts">
384
+ import { ref } from 'vue';
385
+
386
+ const fieldErrors = ref<Record<string, string[]>>({});
387
+ </script>
388
+
389
+ <template>
390
+ <BbDialog id="rename" v-slot="{ data, complete }" title="Rename">
391
+ <BbForm
392
+ :field-errors="fieldErrors"
393
+ :model="data"
394
+ @submit="
395
+ () =>
396
+ complete(data).catch((error) => {
397
+ // Your save's rejection: the 422 body's `errors` by field path.
398
+ fieldErrors = error.errors ?? {};
399
+ })
400
+ "
401
+ >
402
+ <BbTextInput label="Name" name="name" />
403
+ <BbButton type="submit">Save</BbButton>
404
+ </BbForm>
405
+ </BbDialog>
406
+ </template>
407
+ ```
408
+
409
+ With Inertia, the page's `useForm().errors` is that object already; the
410
+ record-form recipe (`ai/recipes/inertia/record-form.md`) binds
411
+ `<BbForm :field-errors="form.errors">` the same way.
412
+
413
+ An Inertia visit (`router.put`, `form.put`) returns no promise and never
414
+ rejects on its own, so `onComplete` wraps it: a validation error rejects
415
+ with the errors (the example above reads `error.errors`), success resolves,
416
+ and a visit cut short by another visit resolves too, closing the dialog.
417
+
418
+ ```ts
419
+ import { router } from '@inertiajs/vue3';
420
+
421
+ type User = { id: number; name: string };
422
+
423
+ const save = (user: User) =>
424
+ new Promise<void>((resolve, reject) => {
425
+ router.put(`/users/${user.id}`, user, {
426
+ onSuccess: () => resolve(),
427
+ onError: (errors) => reject({ errors }),
428
+ // Cancelled or interrupted: close. Anything else that ends here
429
+ // unsettled (a 500, a network error) keeps the dialog open.
430
+ // Both are no-ops after onSuccess / onError.
431
+ onFinish: (visit) =>
432
+ visit.cancelled || visit.interrupted
433
+ ? resolve()
434
+ : reject(new Error('The save failed')),
435
+ });
436
+ });
437
+ ```
438
+
343
439
  #### The data is a copy
344
440
 
345
441
  Every opening holds its own **reactive copy** of the `data` it was given
@@ -421,7 +517,7 @@ default.
421
517
  </div>
422
518
  </template>
423
519
  <template #footer="{ data, close, complete, reset }">
424
- <!-- `reset()` restores the default, on an edit too: it clears. -->
520
+ <!-- `reset()` clears the default's keys; an edit keeps its `id`. -->
425
521
  <BbButton variant="ghost" @click="reset">Clear</BbButton>
426
522
  <BbButton variant="ghost" @click="close">Cancel</BbButton>
427
523
  <BbButton
@@ -462,9 +558,11 @@ const save = (saved: Member) => {
462
558
  </script>
463
559
  ```
464
560
 
465
- - **`reset()`** (the slots', or `useBbDialogContext(id).reset()`) restores a
466
- fresh copy of `default-data`, on an edit too: it clears the form, it doesn't
467
- go back to the row. Cancel is what discards an edit.
561
+ - **`reset()`** (the slots', or `useBbDialogContext(id).reset()`) restores
562
+ the keys `default-data` declares from a fresh copy, on an edit too, and keeps
563
+ every key it doesn't declare: the row's `id` stays, so a cleared edit still
564
+ saves as an edit. It doesn't go back to the row's values; Cancel is what
565
+ discards an edit.
468
566
  - **Every close resets it** as well: nothing to do on `@show` or `@hidden`.
469
567
  - **An object, not a function.** The dialog copies it every time and never
470
568
  writes it, so a module constant is safe.
@@ -577,10 +675,16 @@ const createForDesign = async () => {
577
675
  KeepAlive park. A flow that changes the URL inside one dialog (a wizard
578
676
  whose steps are routes) opens it from the parent route or the persistent
579
677
  layout, which stays mounted. A `show()` from a store lives until closed.
580
- - **A second `show()` stacks** a second instance on top, with its own promise.
581
- Give a create flow one trigger: several buttons each opening the same
582
- component stack copies. Opened by id, the single mounted dialog dedupes
583
- instead.
678
+ - **From a store.** Create the handle in the setup store's body (or under
679
+ `app.runWithContext()`), then `show()` / `hide()` from its actions or a
680
+ router hook. Created in a plain module, outside any injection context,
681
+ `show()` settles `'failed'` with a dev warning.
682
+ - **One handle, one dialog.** `useBbDialogContext(Component)` is the same
683
+ handle as the id form (`isReady`, `open`, `data`, `show`, `hide`,
684
+ `complete`, `reset`), about its own dialog: a `show()` while it is open
685
+ joins it. Another handle (another `useBbDialogContext(Component)` call, or
686
+ another trigger element) opens its own instance on top, with its own
687
+ promise. `isReady` says a `<BbHost />` is mounted to render it.
584
688
  - **Each opening gets its own DOM id**, so two stacked instances stay valid
585
689
  HTML; `aria-controls` on the trigger follows it. A template dialog keeps the
586
690
  `id` you gave it. To find one in an e2e test, use its role and title,
@@ -968,6 +1072,18 @@ const form = useBbFormContext('edit-member-form');
968
1072
  field uses, `form.setErrors({ ...errors, _request: [message] })`, and shows in
969
1073
  the form's `unmatchedErrors` (see BbForm § Server errors).
970
1074
 
1075
+ #### Steps in one dialog
1076
+
1077
+ - **Each step's first field gets `autofocus`**: it fires when the field
1078
+ mounts, so focus follows the step with no watcher (`focus-target` picks only
1079
+ where the opening starts).
1080
+ - **A final success step with no field is a status region**
1081
+ (`role="status"`), announced as it appears, not a focus target.
1082
+ - **A footer button that changes type in place** (Next, `type="button"`,
1083
+ becoming Accept, `type="submit"`, during its own click): give each its own
1084
+ `key`. Unkeyed, Vue reuses the element, its type flips mid-click, and the
1085
+ click on Next submits the form.
1086
+
971
1087
  ### Lifecycle events and lazy content
972
1088
 
973
1089
  Four events bracket the transitions — `show` fires as the model turns `true`,
@@ -987,11 +1103,16 @@ Every way to close, side by side:
987
1103
  | Escape, Back on a URL dialog, a backdrop click, a swipe | the user | yes: `persistent` too | `reason: 'escape'` / `'backdrop'` / `'swipe'` | on the key, Back, click or swipe |
988
1104
  | `hide()` (context), the model set to `false` | your code | no | `{ completed: false, reason: 'hide' }` | at once |
989
1105
  | `complete(value)` (slots, context) | your code | no | `{ completed: true, value }` | at once |
990
- | `@hide` | the dialog | it reports a close, it can't stop one | settled together with it | as any close above **starts** |
1106
+ | `@hide` | the dialog | it reports a close, it can't stop one | emits it (the same object) | as any close above **starts** |
991
1107
  | `@hidden` | the dialog | it reports a close, it can't stop one | already settled | after the close transition; focus returns, `data` clears and the body unmounts then |
992
1108
 
993
1109
  A refused close fires nothing and settles nothing. Use `@hide` for what
994
- follows the decision (a toast, a refresh), `@hidden` for teardown.
1110
+ follows the decision (a toast, a refresh), `@hidden` for teardown. `@hide`
1111
+ carries the opening's result, so a live preview reverts on a dismissal with
1112
+ no flag of its own: `@hide="(r) => { if (!r.completed) restore() }"`. To
1113
+ tell a save from a cancel, close a save with `complete()` (slots or context),
1114
+ not by writing the model: on a `v-model` dialog too, it closes through the
1115
+ model and reports `{ completed: true }`.
995
1116
 
996
1117
  The body lives **per opening**: nothing inside the dialog renders until it
997
1118
  opens, and the body unmounts after the close transition (right after
@@ -1003,7 +1124,9 @@ tests that query content before opening): `data` still clears while closed (to
1003
1124
  `undefined`, or the `default-data` copy), so an eager body guards its own
1004
1125
  reads. `transition-duration` (ms, default
1005
1126
  `250`) times both the open/close animation and when `hidden` fires — reopening
1006
- during the close transition keeps the dialog open instead of snapping shut.
1127
+ during the close transition keeps the dialog open instead of snapping shut, with
1128
+ a fresh body: every new opening gets one, a reopen mid-close included (an
1129
+ `eager` body is kept).
1007
1130
 
1008
1131
  ### Size
1009
1132
 
@@ -1056,6 +1179,11 @@ Every dismissal path is governed by two props, and by a form inside:
1056
1179
  - `true`: always locked.
1057
1180
  - `false`: never locked, not even mid-save. Closing mid-save is then on you.
1058
1181
 
1182
+ Binding it to your own flag: beside a `BbForm`, another async action (a
1183
+ footer Delete) binds `:prevent-close="deleting || undefined"`; a plain
1184
+ `false` while idle would switch the form's lock off. A dialog with no
1185
+ `BbForm` binds `:prevent-close="saving"`.
1186
+
1059
1187
  `persistent` keeps the ✕ (and the slots' `close`) as the deliberate exit;
1060
1188
  `prevent-close` removes every exit but the model.
1061
1189
 
@@ -1580,6 +1708,10 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1580
1708
  `href`/`to` to the button.
1581
1709
  - **Don't toggle `v-model` from `@hide`/`@hidden` handlers** to "cancel" a
1582
1710
  close — use `persistent` instead; the close paths already guard correctly.
1711
+ - **Catch `complete()` in a reusable dialog** whenever an opener may pass an
1712
+ `onComplete` that rejects (`complete(data).catch(…)`, as in § The page
1713
+ saves). Uncaught, the rejection travels through the form's `@submit` and
1714
+ reaches `app.config.errorHandler`.
1583
1715
  - **Don't leave the `header` slot without a close control** — it replaces the
1584
1716
  default ×, so you must render your own wired to `close`.
1585
1717
  - **Don't query dialog content before it has opened, or after it has closed**,
@@ -1625,7 +1757,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1625
1757
  | --- | --- | --- | --- | --- |
1626
1758
  | `pt:<part>`, `pt:<part>:<state>`, `pt` | `PtValue` | | | Passthrough — a class list, or `{ class, style, attrs }`, bound to one named part of the component; the state form applies only while that state is on. Parts: `close`, `content`, `description`, `footer`, `header`, `root`, `sheet`, `title`. States: `fullscreen`, `open`. What each one is: _Passthrough parts and states_ below. `pt` is the object form with the same keys minus the prefix (`{ icon: '…', 'icon:loading': '…' }`). See `guides/passthrough.md`. In `<script>` a `pt` value is typed `BbDialogProps['pt']` (`PtValue` is not exported): § Typing `pt` in `<script>`. |
1627
1759
  | `adaptive` | `boolean \| undefined` | ``config.adaptive` (`true`)` | | On mobile, renders as a `BbOffCanvas` drawer instead of a centred dialog; the surface is chosen when the dialog opens and kept until it closes. Unset: the global `config.adaptive`. |
1628
- | `defaultData` | `Record<string, any> \| undefined` | | | The `data` of an opening that passes none (a create), as an object: copied at once and exposed as `data`, so inputs bind `data.x` with no guard. An opening with data lays it over this (`{ ...defaultData, ...row }`, one level). `reset()` restores a fresh copy, and every close does too. |
1760
+ | `defaultData` | `Record<string, any> \| undefined` | | | The `data` of an opening that passes none (a create), as an object: copied at once and exposed as `data`, so inputs bind `data.x` with no guard. An opening with data lays it over this (`{ ...defaultData, ...row }`, one level). `reset()` restores its keys, keeping an edit's `id`; a close restores it. |
1629
1761
  | `description` | `string \| undefined` | | | One short line under the title, muted (the shadcn `DialogDescription` shape), and the only element `aria-describedby` points at. Keep it to a sentence; anything longer belongs in the body. |
1630
1762
  | `eager` | `boolean \| undefined` | `false` | | Keep the body mounted while closed, from before the first opening. Without it the body mounts per opening and unmounts after `@hidden`. `data` still clears while closed, so an eager body guards its reads. |
1631
1763
  | `focusTarget` | `string \| undefined` | | | CSS selector of the element to focus after opening. Defaults to the first tabbable element. |
@@ -1636,7 +1768,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1636
1768
  | `navigation` | `boolean \| undefined` | `false` | | Syncs the open state with the URL: opening writes `?<queryKey>=true`, closing removes it, a URL carrying the key opens the dialog on load, and Back closes it (refused like Escape when locked). Template dialogs only; a record id stays in your own query. |
1637
1769
  | `offCanvasProps` | `Partial<BbOffCanvasProps> \| undefined` | | | Props for the phone sheet (the `BbOffCanvas` that `adaptive` renders on mobile), spread over its defaults. A `pt` inside it styles only the sheet (passthrough guide § Inside the sheet). |
1638
1770
  | `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 `preventClose` to block every dismissal. Programmatic close is still allowed. |
1639
- | `preventClose` | `boolean \| undefined` | ``undefined` (follows a saving form inside)` | | Refuses every user close (backdrop, Escape and the ✕, and the slots' `close`) with the deny nudge; `v-model` still closes. Unset: locked while a `BbForm` inside saves (an async `@submit`). `true`: always. `false`: never, not even mid-save. |
1771
+ | `preventClose` | `boolean \| undefined` | ``undefined` (follows a saving form inside)` | | Refuses every user close (backdrop, Escape and the ✕, and the slots' `close`) with the deny nudge; `v-model` still closes. Unset: locked while a `BbForm` inside saves (an async `@submit`). `true`: always. `false`: never, not even mid-save. Beside a form: `busy \|\| undefined`. |
1640
1772
  | `queryKey` | `string \| undefined` | `"dialog"` | | The query key `navigation` writes. |
1641
1773
  | `replace` | `boolean \| undefined` | `false` | | With `navigation`, replaces the history entry instead of pushing one, so Back leaves the page instead of closing the dialog. |
1642
1774
  | `size` | `Responsive<NonNullable<number \| CssSize \| 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. |
@@ -1671,7 +1803,7 @@ States are listed in precedence order: when two are on at once and their entries
1671
1803
  ## Events
1672
1804
 
1673
1805
  - `hidden` — `(): void` — Emitted after the close transition completes and the dialog is fully dismissed.
1674
- - `hide` — `(): void` — Emitted as soon as the dialog starts to close (model becomes false).
1806
+ - `hide` — `(result: BbDialogResult): void` — Emitted as soon as the dialog starts to close (model becomes false), with the opening's result: the same object its promise settles with, `{ completed: true, value }` or `{ completed: false, reason }` (Q73.6).
1675
1807
  - `show` — `(): void` — Emitted as soon as the dialog starts to open (model becomes true).
1676
1808
  - `shown` — `(): void` — Emitted once the open transition has run (symmetric with `hidden`): the dialog is settled, so code that measures or anchors to its content sees the final layout. Not emitted for a dialog closed before it finished opening.
1677
1809
  - `update:modelValue` — `(value: boolean): void` — Emitted with the new value whenever `v-model` changes.
@@ -1679,10 +1811,10 @@ States are listed in precedence order: when two are on at once and their entries
1679
1811
 
1680
1812
  ## Slots
1681
1813
 
1682
- - `default` — scope: `BbDialogDefaultSlotProps` `{ close: () => void; complete: (value?: any) => void; reset: () => void; data: any }` — Primary body content of the dialog, rendered in the scrollable body area.
1814
+ - `default` — scope: `BbDialogDefaultSlotProps` `{ close: () => void; complete: (value?: any) => Promise<void>; reset: () => void; data: any }` — Primary body content of the dialog, rendered in the scrollable body area.
1683
1815
  - `description` — scope: `BbDialogDescriptionSlotProps` `{ text: string; data: any }` — Replaces the description text under the title. Renders the header even when `title` and `description` are unset — a slotted description is a description, the same rule `#title` follows.
1684
- - `footer` — scope: `BbDialogFooterSlotProps` `{ close: () => void; complete: (value?: any) => void; reset: () => void; data: any }` — Content rendered in the dialog footer, below the body area. Typically used for action buttons.
1685
- - `header` — scope: `BbDialogHeaderSlotProps` `{ titleId: string; close: () => void; complete: (value?: any) => void; reset: () => void; title: string; data: any }` — Replaces the whole header (title and ✕) on both surfaces, so it owns closing: wire your control to `close`, and `titleId` onto your title. It renders even without `title`. Its root is a flex item: give it `flex-1` to fill the width.
1816
+ - `footer` — scope: `BbDialogFooterSlotProps` `{ close: () => void; complete: (value?: any) => Promise<void>; reset: () => void; data: any }` — Content rendered in the dialog footer, below the body area. Typically used for action buttons.
1817
+ - `header` — scope: `BbDialogHeaderSlotProps` `{ titleId: string; close: () => void; complete: (value?: any) => Promise<void>; reset: () => void; title: string; data: any }` — Replaces the whole header (title and ✕) on both surfaces, so it owns closing: wire your control to `close`, and `titleId` onto your title. It renders even without `title`. Its root is a flex item: give it `flex-1` to fill the width.
1686
1818
  - `title` — scope: `BbDialogTitleSlotProps` `{ text: string; data: any }` — Replaces the default title text inside the header. Remains linked to the dialog through `aria-labelledby`. Providing this slot renders the header even when `title` is unset — a slotted title is a title.
1687
1819
 
1688
1820
  ## CSS custom properties
@@ -2124,8 +2124,8 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
2124
2124
  width; pass a number when menus must align across rows.
2125
2125
  - Item `variant`s other than `destructive` render only a class hook — register
2126
2126
  names via the `dropdownItemVariants` plugin option and ship their CSS.
2127
- - `<BbDropdownGroup>` must sit in a `BbDropdown` (or `BbDropdownButton`)
2128
- default slot, its `id` must match a group's `id`, and a `multiple` group's
2127
+ - `<BbDropdownGroup>` must sit in a `BbDropdown`'s default slot (a
2128
+ `BbDropdownButton`'s `#dropdown`), its `id` must match a group's `id`, and a `multiple` group's
2129
2129
  model must be an array — all three misuses throw.
2130
2130
  - Placeholder texts default to the config locale's "Loading…" / "No results"
2131
2131
  strings (`locale` plugin option) unless you set `loading-text` /