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
package/README.md CHANGED
@@ -11,7 +11,8 @@ Published artifacts are **`dist/`** (ESM JavaScript, `.d.ts`, the two stylesheet
11
11
  ### Styles
12
12
 
13
13
  - **Global stylesheet** — import `bitboss-ui/styles.css` once. It bundles design tokens, base rules, and styles for all exported components (built from the library’s PostCSS/Tailwind pipeline).
14
- - **Optional reset** — `bitboss-ui/reset.css`, only if your app has no reset of its own. Import it _before_ `styles.css` (or let the build plugin inject it with `resetCss: true`).
14
+ - **Optional reset** — `bitboss-ui/reset.css`, only if your app has no reset of its own. Import it _before_ `styles.css` (or let the build plugin inject it with `resetCss: true`). A reset is a **prerequisite** of the components, not a nicety: their sizes are written for `border-box`, and native controls and fieldsets are expected to arrive without their browser chrome. Use this one, or your own — Tailwind's Preflight covers it, which is why `resetCss` defaults to off. The component test suite runs against this file.
15
+ - **Cascade layers (opt-in)** — on Tailwind v4 the library must sit in your layer order or its unlayered rules outrank every utility you put on a component. `cssLayers: TAILWIND_V4_LAYERS` in the build plugin (both exported from `bitboss-ui/vite`) places it after Preflight and before the utilities. Details in `ai/guides/installation-and-plugin-setup.md` § Cascade layers.
15
16
 
16
17
  ### TypeScript: use `vue-tsc` 3
17
18
 
@@ -181,6 +182,19 @@ export default defineConfig({
181
182
  });
182
183
  ```
183
184
 
185
+ **Icons, in one paragraph.** `local:` ships every SVG in `iconDir`. Provider
186
+ icons (`lucide:*`, `tabler:*`, any installed `@iconify-json/*` set) ship only
187
+ what the plugin finds as a `prefix:name` literal anywhere in the project — it
188
+ reads every file under the Vite root as bytes, PHP included, skipping only
189
+ gitignored, hidden, binary, `node_modules`/`vendor`/`storage`/`outDir` and
190
+ `iconsScanner.exclude` paths — plus whatever you list in `iconWhitelist`
191
+ (exact names or globs, for names composed at runtime or stored in a database).
192
+ This holds in dev exactly as in prod: dev registers the same set and re-scans a
193
+ file on every save. A name that does not resolve renders a built-in placeholder
194
+ (`data-bb-icon-missing="<name>"`) and logs one error with the fix; it never
195
+ throws. Details: `ai/guides/icons-policy.md` and
196
+ `ai/guides/installation-and-plugin-setup.md` § Icons.
197
+
184
198
  ### Nuxt module
185
199
 
186
200
  ```ts
@@ -241,8 +255,9 @@ combined — and nothing but the MCP server loads them, so installing
241
255
  and `bitboss-ui mcp` tells you what to install rather than dying on a module
242
256
  resolution error.
243
257
 
244
- The agent harness launches the server itself (`npx bitboss-ui@<installed version> mcp`,
245
- pinned so npx can never fall back to fetching a different version from the registry) on demand —
258
+ The agent harness launches the server itself (`node node_modules/bitboss-ui/bin/bitboss-ui.mjs mcp`,
259
+ the installed binary rather than `npx`, so it can never fetch from the registry, its optional
260
+ peers resolve in your project, and it cannot drift behind an upgrade) on demand —
246
261
  you never run it by hand. `--mcp` merges the server entry into each harness's
247
262
  own config (never overwriting other servers you've registered), and each write
248
263
  is idempotent:
@@ -360,6 +375,43 @@ is left alone. For a bulk sweep of a v2 codebase, opt into the fix:
360
375
  `assumeV2` is off by default because the two readings produce opposite
361
376
  layouts — turn it on only for files you know came from v2, then turn it off.
362
377
 
378
+ #### The one rule that reads CSS: `no-unknown-token`
379
+
380
+ `bitboss-ui/no-unknown-token` flags a bare `var(--bb-…)` naming a token the
381
+ installed package does not declare, in a `<style>` block or a static
382
+ `style="…"` attribute. It is a `warn`, like `no-unknown-event`.
383
+
384
+ An invented token is not a near miss that degrades — CSS treats an undefined
385
+ custom property as invalid-at-computed-value-time and throws away the **whole
386
+ declaration**, so the rule silently does nothing and the element keeps whatever
387
+ it inherited. One pilot lost a calendar grid's separators that way and read it
388
+ as a layout bug.
389
+
390
+ **A fallback disarms it**, because that is defined behaviour rather than a
391
+ silent drop:
392
+
393
+ ```css
394
+ .panel {
395
+ background: var(--bb-canvas); /* warns: no such token */
396
+ background: var(--bb-canvas, #f8fafc); /* fine: yours to define */
397
+ }
398
+ ```
399
+
400
+ The token list is read from the `dist/styles.css` you installed, so the rule
401
+ can never claim a token is invented when your version declares it; with no
402
+ stylesheet to read it stays silent. `allowTokens: string[]` exempts names you
403
+ define yourself, and `stylesPath` points it at a different sheet.
404
+
405
+ Remember the two tiers before reaching for a `--bb-*` name at all: `--bb-*` is
406
+ the small global theme set, while each component declares its own **unprefixed**
407
+ tokens (`.bb-table { --cell-h: 32px }`). A missing `--bb-*` name usually means
408
+ the knob you want is a component token, not a gap in the library.
409
+
410
+ Two sibling checks stay in `bitboss-ui check` and are deliberately **not**
411
+ ESLint rules: the hand-rolled avatar and spinner hints. Those are guesses (a
412
+ round `<img>` is sometimes a logo), and a lint rule that fires on correct code
413
+ spends the trust the rest of these depend on.
414
+
363
415
  Every real HTML attribute (plus `aria-*` / `data-*`) falls through untouched, so
364
416
  attribute inheritance is never flagged. Escape hatches when you do need one:
365
417
 
@@ -36,6 +36,7 @@ import { fileURLToPath } from 'node:url';
36
36
  import { z } from 'zod';
37
37
  import {
38
38
  loadManifest,
39
+ possessive,
39
40
  validateVueSnippet,
40
41
  } from '../scripts/lib/validate-bb-markup.mjs';
41
42
 
@@ -70,26 +71,180 @@ function getManifestMap() {
70
71
  return cachedManifestMap;
71
72
  }
72
73
 
73
- /** `search_components({ query })` — substring match over name/description. */
74
+ /** `search_components({ query })` — token match over name/description. */
74
75
  const SEARCH_DEFAULT_LIMIT = 20;
75
76
 
76
77
  /**
77
- * Relevance score for one component against a lowercased needle. Higher wins;
78
+ * Ranking for the manifest's type tiers. `api` types are the ones an app is
79
+ * expected to reach for — they appear in a component's own props or events —
80
+ * so they lead. `wrapper` (`<Component>Props`) matters only when wrapping one
81
+ * of our components, and `advanced` is registries, contexts and adapters.
82
+ * Publishing 166 exports as a flat list made every one look equally important.
83
+ */
84
+ const TYPE_USAGE_RANK = { api: 0, wrapper: 1, advanced: 2 };
85
+
86
+ /**
87
+ * WHY THIS IS NOT A PLAIN SUBSTRING MATCH ANY MORE.
88
+ *
89
+ * It was, and a consumer pilot (2026-09-09) hand-rolled three components the
90
+ * library ships because the whole query string had to appear verbatim in one
91
+ * field. `table` found BbTable; `data table` — the term most of the industry
92
+ * uses — found NOTHING, because no name or description contains that exact
93
+ * pair. Same for `command palette`, `stepper wizard`, `skeleton loading`.
94
+ *
95
+ * An empty result is the worst possible answer for a tool whose stated job is
96
+ * "find the right Bb* component before writing usage code": it is
97
+ * indistinguishable from "nothing here, build it yourself". So two things
98
+ * changed. Multi-word queries are TOKENISED, and every search — hit or miss —
99
+ * consults the component picker's "Not in this library" table, which is where
100
+ * the authoritative answers for the invented names already lived.
101
+ */
102
+
103
+ /** English filler that would otherwise match half the corpus on its own. */
104
+ const SEARCH_STOPWORDS = new Set([
105
+ 'a',
106
+ 'an',
107
+ 'the',
108
+ 'and',
109
+ 'or',
110
+ 'not',
111
+ 'no',
112
+ 'is',
113
+ 'it',
114
+ 'in',
115
+ 'on',
116
+ 'for',
117
+ 'of',
118
+ 'to',
119
+ 'with',
120
+ 'component',
121
+ 'components',
122
+ 'ui',
123
+ 'view',
124
+ 'widget',
125
+ 'element',
126
+ ]);
127
+
128
+ /** `'data table view'` → `['data', 'table']`. Order is not significant. */
129
+ function tokenize(query) {
130
+ const raw = String(query ?? '')
131
+ .toLowerCase()
132
+ .split(/[^a-z0-9]+/)
133
+ .filter(Boolean);
134
+ const meaningful = raw.filter((t) => !SEARCH_STOPWORDS.has(t));
135
+ // An all-stopword query ('the view') still deserves its literal reading
136
+ // rather than an empty token list that matches everything.
137
+ return meaningful.length > 0 ? meaningful : raw;
138
+ }
139
+
140
+ /**
141
+ * Relevance score for one component against ONE lowercased token. Higher wins;
78
142
  * 0 means "no match". Name hits always outrank description hits so
79
143
  * `search_components({ query: 'table' })` leads with BbTable, not with every
80
144
  * component whose prose mentions a table.
81
145
  */
82
- function scoreComponent(component, needle) {
146
+ function scoreToken(component, needle) {
83
147
  const name = component.name.toLowerCase();
84
148
  const description = (component.description ?? '').toLowerCase();
85
149
  if (name === needle) return 100;
86
150
  if (name === `bb${needle}`) return 95; // 'button' → BbButton
87
151
  if (name.startsWith(needle) || name.startsWith(`bb${needle}`)) return 80;
88
152
  if (name.includes(needle)) return 60;
89
- if (description.includes(needle)) return 30;
153
+ // Descriptions match at a WORD START, never mid-word: a plain substring made
154
+ // 'not' hit "notification" and returned four components for a nonsense
155
+ // query. Trailing suffixes still count, so 'card' finds "cards".
156
+ if (new RegExp(`\\b${needle}`).test(description)) return 30;
90
157
  return 0;
91
158
  }
92
159
 
160
+ /**
161
+ * Score against every token. A component that matches MORE of the query ranks
162
+ * above one that matches a single token harder, so `data table` puts BbTable
163
+ * (both tokens) above a component whose description merely says "data".
164
+ */
165
+ function scoreComponent(component, tokens) {
166
+ let total = 0;
167
+ let matched = 0;
168
+ for (const token of tokens) {
169
+ const score = scoreToken(component, token);
170
+ if (score > 0) {
171
+ matched += 1;
172
+ total += score;
173
+ }
174
+ }
175
+ return matched === 0 ? { matched: 0, score: 0 } : { matched, score: total };
176
+ }
177
+
178
+ /**
179
+ * The picker's "Not in this library" table, parsed from the shipped guide so
180
+ * there is exactly one source of truth. Rows carry both kinds of answer:
181
+ * redirects (`BbDrawer` → "It is BbOffCanvas") and verdicts (`BbCard` → "Does
182
+ * not exist — a card is a <div> with tokens"). Both are better answers to a
183
+ * miss than an empty array.
184
+ */
185
+ const PICKER_PATH = join(AI_ROOT, 'guides', 'component-picker.md');
186
+ let cachedNotInLibrary = null;
187
+
188
+ /** `'BbDataTable'` → `['bbdatatable', 'data', 'table']`. */
189
+ function aliasTokens(cell) {
190
+ const words = cell
191
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // links → their text
192
+ .replace(/[`*_]/g, '')
193
+ .split(/[^A-Za-z0-9]+/)
194
+ .filter(Boolean);
195
+ const tokens = new Set();
196
+ for (const word of words) {
197
+ tokens.add(word.toLowerCase());
198
+ // Split CamelCase so `BbDataTable` is reachable by 'data' and 'table'.
199
+ for (const part of word.replace(/^Bb/, '').split(/(?=[A-Z])/))
200
+ if (part.length > 1) tokens.add(part.toLowerCase());
201
+ }
202
+ return [...tokens].filter((t) => !SEARCH_STOPWORDS.has(t) && t !== 'bb');
203
+ }
204
+
205
+ function loadNotInLibrary() {
206
+ if (cachedNotInLibrary) return cachedNotInLibrary;
207
+ cachedNotInLibrary = [];
208
+ if (!existsSync(PICKER_PATH)) return cachedNotInLibrary;
209
+ const md = readFileSync(PICKER_PATH, 'utf-8');
210
+ const section = md.split(/^## Not in this library\s*$/m)[1];
211
+ if (!section) return cachedNotInLibrary;
212
+ for (const line of section.split('\n')) {
213
+ if (!line.startsWith('|')) continue;
214
+ // Tolerate a missing trailing `|`: markdown renders such a row fine, so
215
+ // dropping it here would silently lose an answer (it already did once).
216
+ const cells = line
217
+ .replace(/^\|/, '')
218
+ .replace(/\|\s*$/, '')
219
+ .split('|');
220
+ if (cells.length < 2) continue;
221
+ const assumed = cells[0].trim();
222
+ const reality = cells[1].trim();
223
+ // Skip the header and its `---` separator.
224
+ if (!assumed || /^-+$/.test(assumed) || /^Assumed\b/i.test(assumed))
225
+ continue;
226
+ cachedNotInLibrary.push({
227
+ assumed,
228
+ reality,
229
+ tokens: aliasTokens(assumed),
230
+ });
231
+ }
232
+ return cachedNotInLibrary;
233
+ }
234
+
235
+ /** Picker rows whose assumed-name cell shares a token with the query. */
236
+ function matchNotInLibrary(tokens) {
237
+ const wanted = new Set(tokens);
238
+ return loadNotInLibrary()
239
+ .map((row) => ({
240
+ row,
241
+ hits: row.tokens.filter((t) => wanted.has(t)).length,
242
+ }))
243
+ .filter(({ hits }) => hits > 0)
244
+ .sort((a, b) => b.hits - a.hits)
245
+ .map(({ row }) => ({ assumed: row.assumed, reality: row.reality }));
246
+ }
247
+
93
248
  function searchComponents({ query, limit, includeReferenceOnly } = {}) {
94
249
  const manifest = loadComponentsManifest();
95
250
  const needle = String(query ?? '')
@@ -104,13 +259,65 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
104
259
  ? manifest.components
105
260
  : manifest.components.filter((c) => !c.referenceOnly);
106
261
 
262
+ const tokens = tokenize(needle);
263
+ const notInLibrary = matchNotInLibrary(tokens);
264
+
265
+ // A matched picker row NAMES the real component ("`BbDrawer` / `BbSheet` →
266
+ // It is BbOffCanvas"). Rank those first: the picker knows the answer, and
267
+ // returning nothing for `drawer` while BbOffCanvas ships is the whole bug.
268
+ const redirected = new Set();
269
+ for (const { reality } of notInLibrary)
270
+ for (const name of reality.match(/\bBb[A-Z][A-Za-z]+/g) ?? [])
271
+ redirected.add(name);
272
+
107
273
  const scored = pool
108
- .map((c) => ({ c, score: scoreComponent(c, needle) }))
274
+ .map((c) => {
275
+ const base = scoreComponent(c, tokens);
276
+ const bonus = redirected.has(c.name) ? 90 : 0;
277
+ return bonus > 0
278
+ ? { c, matched: base.matched + 1, score: base.score + bonus }
279
+ : { c, ...base };
280
+ })
109
281
  .filter(({ score }) => score > 0)
110
- // Ties break alphabetically so results are stable between calls.
111
- .sort((a, b) => b.score - a.score || a.c.name.localeCompare(b.c.name));
282
+ // More of the query matched wins first; then strength; then alphabetical
283
+ // so results are stable between calls.
284
+ .sort(
285
+ (a, b) =>
286
+ b.matched - a.matched ||
287
+ b.score - a.score ||
288
+ a.c.name.localeCompare(b.c.name)
289
+ );
112
290
 
113
291
  const shown = scored.slice(0, max);
292
+
293
+ /**
294
+ * Public type exports whose name matches the query. Scored the same way as
295
+ * a component name (exact, prefix, substring) but never mixed into the
296
+ * component list — a type is not something you render, and ranking it
297
+ * beside components is how it gets written as a tag.
298
+ */
299
+ const matchedTypes = (manifest.types ?? [])
300
+ .filter((type) => {
301
+ const lower = type.name.toLowerCase();
302
+ return tokens.some(
303
+ (token) => lower === token || lower.includes(token.toLowerCase())
304
+ );
305
+ })
306
+ // `api` first: those are the types you meet writing markup. The rest are
307
+ // wrapper props and internals, which rank below anything you would use.
308
+ .sort((a, b) => TYPE_USAGE_RANK[a.usage] - TYPE_USAGE_RANK[b.usage])
309
+ .slice(0, max)
310
+ .map((type) => ({
311
+ name: type.name,
312
+ kind: 'type',
313
+ usage: type.usage,
314
+ component: type.component,
315
+ ...(type.deprecated ? { deprecated: true } : {}),
316
+ ...(type.replacedBy ? { replacedBy: type.replacedBy } : {}),
317
+ usedBy: type.usedBy ?? [],
318
+ import: `import type { ${type.replacedBy ?? type.name} } from 'bitboss-ui';`,
319
+ }));
320
+
114
321
  return {
115
322
  query: query ?? '',
116
323
  count: shown.length,
@@ -128,6 +335,23 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
128
335
  referenceOnly: Boolean(c.referenceOnly),
129
336
  contract: c.contract,
130
337
  })),
338
+ // Authoritative answers for names agents invent. Present on hits too: a
339
+ // `card` query that ranks BbBaseButton (its description mentions cards)
340
+ // still needs the picker's verdict that there is no BbCard.
341
+ ...(notInLibrary.length > 0 ? { notInLibrary } : {}),
342
+ // A query that names a public TYPE gets it back as a type, so the answer
343
+ // to "is BbTableColumn a thing" is never an empty component list. The
344
+ // consumer who hit this concluded the export was undocumented and
345
+ // allowlisted the tag.
346
+ ...(matchedTypes.length > 0 ? { types: matchedTypes } : {}),
347
+ guidance:
348
+ notInLibrary.length > 0
349
+ ? 'The `notInLibrary` entries are the authoritative answer for this query — prefer them over the ranked components, and do not hand-roll what they redirect you to.'
350
+ : shown.length > 0
351
+ ? 'Ranked by relevance. Confirm props with `get_component` before writing usage code.'
352
+ : matchedTypes.length > 0
353
+ ? 'No component matches, but `types` does: these are TYPE exports. Import one for the value you pass to the prop named in its `usedBy` — never write it as a tag, and never allowlist it.'
354
+ : 'No manifest match and no picker entry. Zero results does NOT mean "not covered" — search a single plain noun (`table`, not `data table`), then read the guide `component-picker` (its "Not in this library" table is the full list of names that do not exist and what to use instead) before building anything by hand.',
131
355
  };
132
356
  }
133
357
 
@@ -180,6 +404,32 @@ function getComponent({ name, maxChars } = {}) {
180
404
  const manifest = loadComponentsManifest();
181
405
  const entry = manifest.components.find((c) => c.name === name);
182
406
  if (!entry) {
407
+ /*
408
+ * A public TYPE asked for as a component. `BbTableColumn`,
409
+ * `BbDropdownItem`, `BbTabsItem` and `BbBreadcrumbsItem` are exports an
410
+ * agent meets in a props table and reasonably looks up here; answering
411
+ * "unknown component — try search_components" sent one consumer to the
412
+ * `allowComponents` allowlist instead, which silences every check on the
413
+ * tag. Answer the real question: what it is, and where it is used.
414
+ */
415
+ const type = (manifest.types ?? []).find((t) => t.name === name);
416
+ if (type) {
417
+ const use = type.usedBy?.[0];
418
+ return {
419
+ found: true,
420
+ kind: 'type',
421
+ name,
422
+ usage: type.usage,
423
+ ...(type.deprecated ? { deprecated: true } : {}),
424
+ ...(type.replacedBy ? { replacedBy: type.replacedBy } : {}),
425
+ component: type.component,
426
+ usedBy: type.usedBy ?? [],
427
+ import: `import type { ${type.replacedBy ?? name} } from 'bitboss-ui';`,
428
+ hint: use
429
+ ? `A type, not a component: it types ${possessive(use.component)} \`${use.name}\` ${use.surface}. Call get_component('${use.component}') for the contract that documents its fields.`
430
+ : 'A type, not a component — import it for the value you pass, never write it as a tag.',
431
+ };
432
+ }
183
433
  return {
184
434
  found: false,
185
435
  name,
@@ -199,10 +449,31 @@ function getComponent({ name, maxChars } = {}) {
199
449
  maxChars,
200
450
  `The structured \`component\` field above is COMPLETE (all props/events/slots) — the cut text is guidance prose only.`
201
451
  );
452
+ /*
453
+ * The types this component's own API names, so "what do I import to build
454
+ * the value for `columns`?" is answered where the question is asked. Without
455
+ * it an agent reads `BbTableColumn<T>[]` in the props table and has nowhere
456
+ * to go — one consumer concluded the export was undocumented and allowlisted
457
+ * the tag. Only types the component actually uses, never the whole export
458
+ * list: a menu is not an answer.
459
+ */
460
+ const types = (manifest.types ?? [])
461
+ .filter((type) =>
462
+ (type.usedBy ?? []).some((use) => use.component === entry.name)
463
+ )
464
+ .filter((type) => !type.deprecated)
465
+ .map((type) => ({
466
+ name: type.name,
467
+ usage: type.usage,
468
+ usedBy: (type.usedBy ?? []).filter((use) => use.component === entry.name),
469
+ import: `import type { ${type.name} } from 'bitboss-ui';`,
470
+ }));
471
+
202
472
  return {
203
473
  found: true,
204
474
  // Never truncated: the precision-critical surface.
205
475
  component: entry,
476
+ ...(types.length > 0 ? { types } : {}),
206
477
  contractPath,
207
478
  contractContent: content,
208
479
  ...meta,
@@ -469,6 +740,109 @@ function validateCode({ code } = {}) {
469
740
  * becomes an `isError` result carrying `Error: <message>` — preserving the
470
741
  * exact result shape of the previous hand-rolled dispatch.
471
742
  */
743
+ const CHANGELOG_PATH = join(AI_ROOT, 'changelog.json');
744
+
745
+ /**
746
+ * Order two versions on this library's line: `3.0.0-beta.21` after
747
+ * `3.0.0-beta.9` (numeric, not lexical — the bug every hand-rolled compare
748
+ * has), and any prerelease before its own stable release.
749
+ */
750
+ function compareVersions(a, b) {
751
+ const parse = (v) => {
752
+ const [core, pre = ''] = String(v).split('-');
753
+ const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
754
+ const [tag = '', n = ''] = pre.split('.');
755
+ return { nums, tag, n: Number.parseInt(n, 10) || 0 };
756
+ };
757
+ const x = parse(a);
758
+ const y = parse(b);
759
+ for (let i = 0; i < 3; i += 1) {
760
+ if ((x.nums[i] ?? 0) !== (y.nums[i] ?? 0))
761
+ return (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
762
+ }
763
+ // No prerelease tag outranks any prerelease of the same core version.
764
+ if (!x.tag && y.tag) return 1;
765
+ if (x.tag && !y.tag) return -1;
766
+ if (x.tag !== y.tag) return x.tag < y.tag ? -1 : 1; // alpha < beta < rc
767
+ return x.n - y.n;
768
+ }
769
+
770
+ /**
771
+ * `list_changes({ since, until })` — the breaking changes between two releases.
772
+ *
773
+ * WHY THIS TOOL EXISTS. A consumer agent on 2026-09-09 asked "what changed
774
+ * between beta.20 and beta.21?", found nothing in the knowledge base that could
775
+ * answer it, and diffed the two published tarballs by hand. The data existed
776
+ * (`dist/ai/changelog.json`) but no tool served it, and its rows carried no
777
+ * release, so even reading the file could not have answered the question. Both
778
+ * halves are fixed: rows are stamped with `firstReleasedIn` at build time, and
779
+ * this serves the slice.
780
+ */
781
+ function listChanges({ since, until } = {}) {
782
+ if (!existsSync(CHANGELOG_PATH)) {
783
+ return {
784
+ error: `changelog.json not found at ${CHANGELOG_PATH} — reinstall bitboss-ui (or run \`npm run build\` when working in the library repo).`,
785
+ };
786
+ }
787
+ const doc = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8'));
788
+ const releases = doc.releases ?? [];
789
+ const known = new Set(releases);
790
+
791
+ // A typo'd version must not silently return "nothing changed" — that reads
792
+ // as a clean upgrade and is the worst possible wrong answer here.
793
+ for (const [label, value] of [
794
+ ['since', since],
795
+ ['until', until],
796
+ ]) {
797
+ if (value != null && !known.has(String(value))) {
798
+ return {
799
+ error: `Unknown ${label} version '${value}'.`,
800
+ knownReleases: releases,
801
+ hint: 'Pass a version exactly as listed in knownReleases, or omit both to get every break in the v2 → v3 upgrade.',
802
+ };
803
+ }
804
+ }
805
+
806
+ const all = doc.breaking ?? [];
807
+ // No range asked for: the whole v2 → v3 set, which is what the file has
808
+ // always meant.
809
+ if (since == null && until == null) {
810
+ return {
811
+ version: doc.version,
812
+ upgrade: doc.upgrade,
813
+ guide: doc.guide,
814
+ releases,
815
+ summary: doc.summary,
816
+ count: all.length,
817
+ breaking: all,
818
+ hint: 'Narrow to one upgrade hop with list_changes({ since: "<your current version>" }) — that answers "what breaks if I bump".',
819
+ };
820
+ }
821
+
822
+ const inRange = all.filter((entry) => {
823
+ const at = entry.firstReleasedIn;
824
+ // Unreleased rows belong to the future: they ship in whatever comes
825
+ // next, so they count only when the caller left `until` open.
826
+ if (at == null) return until == null;
827
+ if (since != null && compareVersions(at, String(since)) <= 0) return false;
828
+ if (until != null && compareVersions(at, String(until)) > 0) return false;
829
+ return true;
830
+ });
831
+
832
+ return {
833
+ version: doc.version,
834
+ since: since ?? null,
835
+ until: until ?? doc.version,
836
+ count: inRange.length,
837
+ breaking: inRange,
838
+ guide: doc.guide,
839
+ hint:
840
+ inRange.length === 0
841
+ ? '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.'
842
+ : 'Each entry names the component and the new spelling; `guide` is the migration page with the full explanation.',
843
+ };
844
+ }
845
+
472
846
  function toolCallback(fn) {
473
847
  return (args) => {
474
848
  try {
@@ -500,7 +874,7 @@ function buildServer() {
500
874
  'search_components',
501
875
  {
502
876
  description:
503
- 'Search the bitboss-ui component manifest by name and description, ranked by relevance (exact/prefix name matches first, description matches last). Returns consumer-API components only by default. Use this to find the right Bb* component before writing usage code.',
877
+ 'Search the bitboss-ui component manifest by name and description, ranked by relevance (exact/prefix name matches first, description matches last). Multi-word queries are matched per word, so "data table" finds BbTable. Also returns `notInLibrary`: the component picker\'s authoritative verdicts for names agents commonly invent — either a redirect ("BbDrawer" is BbOffCanvas) or a confirmation that it does not exist and what to do instead. Returns consumer-API components only by default. Use this to find the right Bb* component before writing usage code. An empty result does NOT mean the need is uncovered — read the `component-picker` guide before hand-rolling anything.',
504
878
  inputSchema: {
505
879
  query: z
506
880
  .string()
@@ -571,6 +945,29 @@ function buildServer() {
571
945
  toolCallback(listGuides)
572
946
  );
573
947
 
948
+ server.registerTool(
949
+ 'list_changes',
950
+ {
951
+ description:
952
+ 'Breaking changes between two bitboss-ui releases — what breaks if you bump. Call with since=<the version you are on> before upgrading. Omit both arguments for every break in the v2 → v3 upgrade. Only BREAKING changes are tracked; features and fixes are not.',
953
+ inputSchema: {
954
+ since: z
955
+ .string()
956
+ .optional()
957
+ .describe(
958
+ "Exclusive lower bound — the version you are currently on, e.g. '3.0.0-beta.20'. Its own breaks are excluded; you already have them."
959
+ ),
960
+ until: z
961
+ .string()
962
+ .optional()
963
+ .describe(
964
+ 'Inclusive upper bound (default: the installed version). Unreleased breaks are included only when this is omitted.'
965
+ ),
966
+ },
967
+ },
968
+ toolCallback(listChanges)
969
+ );
970
+
574
971
  server.registerTool(
575
972
  'get_guide',
576
973
  {
@@ -659,8 +1056,21 @@ export async function main() {
659
1056
  const server = buildServer();
660
1057
  const transport = new StdioServerTransport();
661
1058
  await server.connect(transport);
1059
+ // Settle on stdin's end as well as the transport's close. `onclose` does
1060
+ // not fire when the host simply closes the pipe, so awaiting it alone left
1061
+ // the caller's top-level `await main()` pending at exit and Node printed
1062
+ // "Detected unsettled top-level await" on every clean shutdown (2026-09-09).
1063
+ // `once` on both, and idempotent resolve, so whichever arrives first wins.
662
1064
  return new Promise((resolve) => {
663
- transport.onclose = () => resolve();
1065
+ let done = false;
1066
+ const settle = () => {
1067
+ if (done) return;
1068
+ done = true;
1069
+ resolve();
1070
+ };
1071
+ transport.onclose = settle;
1072
+ process.stdin.once('end', settle);
1073
+ process.stdin.once('close', settle);
664
1074
  });
665
1075
  }
666
1076