bitboss-ui 3.0.0-alpha.5 → 3.0.0-alpha.6

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 (144) hide show
  1. package/README.md +44 -14
  2. package/bin/bitboss-ui-mcp.mjs +16 -2
  3. package/bin/bitboss-ui.mjs +93 -3
  4. package/dist/ai/BbAlert.md +9 -7
  5. package/dist/ai/BbAvatar.md +14 -7
  6. package/dist/ai/BbBadgeButton.md +13 -0
  7. package/dist/ai/BbBaseButton.md +19 -3
  8. package/dist/ai/BbButton.md +48 -4
  9. package/dist/ai/BbDropdownButton.md +0 -1
  10. package/dist/ai/BbDropzone.md +136 -31
  11. package/dist/ai/BbRadio.md +7 -5
  12. package/dist/ai/BbRadioGroup.md +3 -2
  13. package/dist/ai/BbSelect.md +1 -1
  14. package/dist/ai/BbSelectPopover.md +7 -1
  15. package/dist/ai/BbSwitchGroup.md +0 -3
  16. package/dist/ai/BbTable.md +17 -19
  17. package/dist/ai/BbTabs.md +26 -1
  18. package/dist/ai/changelog.json +284 -0
  19. package/dist/ai/components.json +301 -11
  20. package/dist/ai/composables/useBbTableContext.md +12 -12
  21. package/dist/ai/guides/agent-contract.md +33 -1
  22. package/dist/ai/guides/ai-router.md +5 -2
  23. package/dist/ai/guides/app-layout.md +43 -3
  24. package/dist/ai/guides/coherence-playbook.md +10 -10
  25. package/dist/ai/guides/component-picker.md +45 -8
  26. package/dist/ai/guides/design-tokens.md +19 -6
  27. package/dist/ai/guides/fetch-items-playbook.md +3 -1
  28. package/dist/ai/guides/inertia-helpers.md +91 -16
  29. package/dist/ai/guides/inline-edit-playbook.md +41 -0
  30. package/dist/ai/guides/installation-and-plugin-setup.md +60 -10
  31. package/dist/ai/guides/migration/components/bb-date-picker-input.md +38 -0
  32. package/dist/ai/guides/migration/components/bb-select-popover.md +2 -0
  33. package/dist/ai/guides/migration/components/bb-select.md +26 -0
  34. package/dist/ai/guides/migration/components/bb-spinner.md +18 -0
  35. package/dist/ai/guides/migration/components/bb-text-input.md +9 -0
  36. package/dist/ai/guides/migration/v2-to-v3.md +2 -1
  37. package/dist/ai/guides/options-items-playbook.md +1 -1
  38. package/dist/ai/guides/page-shell.md +57 -9
  39. package/dist/ai/guides/validated-forms.md +3 -1
  40. package/dist/ai/recipes/inertia/approvals-inbox.md +9 -3
  41. package/dist/ai/recipes/inertia/layout-scaffold.md +98 -2
  42. package/dist/ai/recipes/inertia/records-workspace.md +1 -1
  43. package/dist/ai/recipes/inertia/upload-center.md +1 -1
  44. package/dist/ai/recipes/nuxt/approvals-inbox.md +16 -10
  45. package/dist/ai/recipes/nuxt/layout-scaffold.md +98 -2
  46. package/dist/ai/recipes/vue/approvals-inbox.md +16 -10
  47. package/dist/ai/recipes/vue/layout-scaffold.md +98 -2
  48. package/dist/ai/source/BbAccordion.md +12 -0
  49. package/dist/ai/source/BbAlert.md +33 -3
  50. package/dist/ai/source/BbBadge.md +3 -3
  51. package/dist/ai/source/BbBadgeButton.md +3 -3
  52. package/dist/ai/source/BbBaseButton.md +132 -5
  53. package/dist/ai/source/BbBaseColorPalette.md +9 -1
  54. package/dist/ai/source/BbBaseDatePicker.md +19 -4
  55. package/dist/ai/source/BbBaseDatePickerInput.md +28 -17
  56. package/dist/ai/source/BbBaseInputContainer.md +6 -7
  57. package/dist/ai/source/BbBaseRadio.md +6 -2
  58. package/dist/ai/source/BbBaseRadioGroup.md +6 -0
  59. package/dist/ai/source/BbBaseRadioIcon.md +5 -5
  60. package/dist/ai/source/BbBaseSelect.md +15 -2
  61. package/dist/ai/source/BbBaseSlider.md +11 -2
  62. package/dist/ai/source/BbBaseTimePickerInput.md +32 -26
  63. package/dist/ai/source/BbButton.md +83 -0
  64. package/dist/ai/source/BbCollapsible.md +1 -1
  65. package/dist/ai/source/BbDatePickerInput.md +1 -1
  66. package/dist/ai/source/BbDialog.md +6 -2
  67. package/dist/ai/source/BbDropdown.md +47 -2
  68. package/dist/ai/source/BbDropdownButton.md +1 -1
  69. package/dist/ai/source/BbDropdownGroup.md +47 -2
  70. package/dist/ai/source/BbDropzone.md +5 -4
  71. package/dist/ai/source/BbOffCanvas.md +5 -3
  72. package/dist/ai/source/BbPagination.md +4 -4
  73. package/dist/ai/source/BbPopover.md +8 -0
  74. package/dist/ai/source/BbSelectPopover.md +43 -18
  75. package/dist/ai/source/BbSmoothHeight.md +1 -1
  76. package/dist/ai/source/BbSpinner.md +26 -11
  77. package/dist/ai/source/BbTable.md +14 -43
  78. package/dist/ai/source/BbTabs.md +11 -2
  79. package/dist/ai/source/BbTabsList.md +11 -2
  80. package/dist/ai/source/BbTabsPanes.md +11 -2
  81. package/dist/ai/source/BbTabsRoot.md +11 -2
  82. package/dist/ai/source/BbTimePickerInput.md +1 -1
  83. package/dist/ai/source/BbToast.md +7 -8
  84. package/dist/ai/source/BbToastPortal.md +7 -8
  85. package/dist/ai/source/BbTooltip.md +36 -12
  86. package/dist/ai/source/BbTree.md +10 -0
  87. package/dist/ai/source/FlatListBox.md +20 -7
  88. package/dist/ai/source/GroupedListBox.md +20 -7
  89. package/dist/assets/svgs/spinner.svg_raw.js +1 -1
  90. package/dist/badge-variants.d.ts +0 -6
  91. package/dist/button-variants.d.ts +0 -1
  92. package/dist/components/BbAlert/BbAlert.vue_vue_type_script_setup_true_lang.js +1 -1
  93. package/dist/components/BbAlert/types.d.ts +10 -1
  94. package/dist/components/BbBadge/BbBadgeButton.vue_vue_type_script_setup_true_lang.js +22 -1
  95. package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +31 -1
  96. package/dist/components/BbBaseButton/types.d.ts +77 -0
  97. package/dist/components/BbBaseButton/types.js +13 -0
  98. package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +194 -194
  99. package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +2 -1
  100. package/dist/components/BbBaseDatePicker/BbBaseDatePickerMonthSelector.vue_vue_type_script_setup_true_lang.js +6 -3
  101. package/dist/components/BbBaseDatePicker/BbBaseDatePickerYearSelector.vue_vue_type_script_setup_true_lang.js +45 -44
  102. package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +11 -11
  103. package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue_vue_type_script_setup_true_lang.js +0 -1
  104. package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +0 -1
  105. package/dist/components/BbBaseRadioGroup/BbBaseRadioGroup.vue_vue_type_script_setup_true_lang.js +2 -0
  106. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +62 -62
  107. package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +5 -1
  108. package/dist/components/BbBaseTag/BbBaseTag.vue.d.ts +1 -1
  109. package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +41 -44
  110. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +18 -1
  111. package/dist/components/BbButton/types.d.ts +77 -0
  112. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +1 -1
  113. package/dist/components/BbDropdown/types.d.ts +33 -2
  114. package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +1 -2
  115. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +55 -55
  116. package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +9 -8
  117. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +106 -103
  118. package/dist/components/BbTable/BbTable.vue.d.ts +1 -1
  119. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +364 -371
  120. package/dist/components/BbTable/types.d.ts +3 -8
  121. package/dist/components/BbTabs/types.d.ts +11 -2
  122. package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +81 -78
  123. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +18 -15
  124. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +90 -87
  125. package/dist/composables/useBbTableContext.d.ts +2 -2
  126. package/dist/composables/useBbTabsContext.js +6 -4
  127. package/dist/composables/useListboxFocus.d.ts +2 -0
  128. package/dist/composables/useListboxFocus.js +30 -19
  129. package/dist/index.d.ts +2 -1
  130. package/dist/llms-full.txt +1155 -197
  131. package/dist/llms-medium.txt +143 -21
  132. package/dist/locale-registry.d.ts +21 -0
  133. package/dist/nuxt-module.d.ts +14 -0
  134. package/dist/styles.css +1 -1
  135. package/dist/validated/index.d.ts +14 -0
  136. package/dist/vite-plugin.d.ts +27 -0
  137. package/dist/vite.js +173 -145
  138. package/package.json +20 -9
  139. package/scripts/README.md +66 -0
  140. package/scripts/lib/eslint-plugin.d.ts +65 -0
  141. package/scripts/lib/eslint-plugin.mjs +3 -3
  142. package/scripts/lib/hand-roll-hints.mjs +173 -0
  143. package/scripts/lib/mcp-config.mjs +39 -1
  144. package/scripts/lib/validate-bb-markup.mjs +106 -0
package/README.md CHANGED
@@ -6,11 +6,29 @@ Vue 3 component library used across BitBoss products. It ships typed building bl
6
6
 
7
7
  ## What’s in the package
8
8
 
9
- Published artifacts are **`dist/`** (ESM JavaScript, `.d.ts`, and **`index.css`**) plus this **README**. The sections below match how the library is organised in code and in the [documentation](https://ui-components-docs.vercel.app/).
9
+ Published artifacts are **`dist/`** (ESM JavaScript, `.d.ts`, the two stylesheets **`styles.css`** and **`reset.css`**, and the `dist/ai/` knowledge base), plus the **`bin/`** CLI, **`scripts/lib/`** (ESLint plugin and markup validator), the root **`llms.txt`**, and this **README**. The sections below match how the library is organised in code and in the [documentation](https://ui-components-docs.vercel.app/).
10
10
 
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`).
15
+
16
+ ### TypeScript: use `vue-tsc` 3
17
+
18
+ Use **`vue-tsc` 3 or newer**. Under `vue-tsc` 2 the template type-checker
19
+ silently checks **nothing** about the props of the nine components declared with
20
+ `generic="T"` — `BbSelect`, `BbSelectPopover`, `BbTable`, `BbTabs`,
21
+ `BbTabsRoot`, `BbTree`, `BbRadioGroup`, `BbCheckboxGroup`, `BbSwitchGroup` —
22
+ because they emit as generic functions rather than `DefineComponent<…>`, a shape
23
+ Vue Language Tools 2 cannot read props out of.
24
+
25
+ The trap is that the failure is partial and invisible: unknown-**component**
26
+ checking still fires and non-generic components still reject bad props, so the
27
+ gate looks like it is working. A real consumer shipped
28
+ `<BbRadioGroup label="…">` (the prop is `legend`) through six release slices on
29
+ `vue-tsc@^2.2.12` with a green `--noEmit`.
30
+
31
+ Full detail: `ai/guides/installation-and-plugin-setup.md`.
14
32
 
15
33
  ### TypeScript: icons
16
34
 
@@ -71,13 +89,14 @@ For most **Base\*** and **Bb\*** components, the package exports matching **prop
71
89
 
72
90
  The same knowledge base the npm package ships is served publicly over a CDN, so
73
91
  an agent can read it before anything is installed. `@alpha` tracks the latest
74
- v3 prerelease; pin an exact version (`bitboss-ui@3.0.0-alpha.4`) for stable
75
- links.
92
+ v3 prerelease; pin an exact version (`bitboss-ui@3.0.0-alpha.5`) for stable
93
+ links. Sizes grow with the catalogue — treat them as the current order of
94
+ magnitude, not a contract.
76
95
 
77
- - [Core knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/llms-medium.txt) (~74 KB) — **start here if you can only fetch one file.** Hard rules, setup, component picker, design language, and the full component catalogue.
78
- - [Index](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/llms.txt) (~17 KB) — link index into every document.
96
+ - [Core knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/llms-medium.txt) (~110 KB) — **start here if you can only fetch one file.** Hard rules, setup, component picker, design language, and the full component catalogue.
97
+ - [Index](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/llms.txt) (~19 KB) — link index into every document.
79
98
  - [components.json](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/ai/components.json) — machine-readable API surface, for programmatic validation.
80
- - [Complete knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/llms-full.txt) (~2.9 MB) — everything concatenated. Bulk ingestion only; too large to prompt with.
99
+ - [Complete knowledge base](https://cdn.jsdelivr.net/npm/bitboss-ui@alpha/dist/llms-full.txt) (~3.2 MB) — everything concatenated. Bulk ingestion only; too large to prompt with.
81
100
 
82
101
  Already installed? Prefer the local copy under `node_modules/bitboss-ui/dist/ai/`,
83
102
  run `npx bitboss-ui ai-init` to write agent pointers, and
@@ -204,9 +223,17 @@ can **query** the knowledge base (search components, read a contract, validate
204
223
  markup) instead of reading files blind:
205
224
 
206
225
  ```bash
226
+ npm i -D @modelcontextprotocol/sdk zod
207
227
  npx bitboss-ui ai-init --mcp
208
228
  ```
209
229
 
230
+ Those two are **optional peer dependencies**, not dependencies: together they
231
+ weigh ~12 MB — more than every runtime dependency of the component library
232
+ combined — and nothing but the MCP server loads them, so installing
233
+ `bitboss-ui` never drags them in. `ai-init --mcp` warns if they are missing,
234
+ and `bitboss-ui mcp` tells you what to install rather than dying on a module
235
+ resolution error.
236
+
210
237
  The agent harness launches the server itself (`npx bitboss-ui@<installed version> mcp`,
211
238
  pinned so npx can never fall back to fetching a different version from the registry) on demand —
212
239
  you never run it by hand. `--mcp` merges the server entry into each harness's
@@ -333,13 +360,16 @@ A standard `llms.txt` discovery file is also published at:
333
360
 
334
361
  ### Package exports
335
362
 
336
- ```ts
337
- // catalogue entry
338
- import 'bitboss-ui/ai';
339
- // or resolve concrete files:
340
- // bitboss-ui/ai/components.json
341
- // bitboss-ui/ai/guides/ai-router.md
342
- ```
363
+ These are **resolution paths, not importable modules** — they resolve to
364
+ Markdown and JSON, so a side-effect `import` of them neither type-checks nor
365
+ bundles. Use them with `import.meta.resolve` / `require.resolve`, or read the
366
+ files directly:
367
+
368
+ | Subpath | Resolves to |
369
+ | ----------------------------------- | ------------------------------ |
370
+ | `bitboss-ui/ai` | `dist/ai/index.md` (catalogue) |
371
+ | `bitboss-ui/ai/components.json` | `dist/ai/components.json` |
372
+ | `bitboss-ui/ai/guides/ai-router.md` | `dist/ai/guides/ai-router.md` |
343
373
 
344
374
  Agents can also read paths under `node_modules/bitboss-ui/dist/ai/` directly.
345
375
 
@@ -376,7 +406,7 @@ MIT — see [`LICENSE`](LICENSE).
376
406
 
377
407
  The source repository is private while v3 is in development, so there is no
378
408
  public issue tracker yet. **Report bugs by email to
379
- [dev@bitboss.it](mailto:dev@bitboss.it)** — that address is the `bugs` contact
409
+ [hey@bitboss.it](mailto:hey@bitboss.it)** — that address is the `bugs` contact
380
410
  in `package.json`, so `npm bugs bitboss-ui` opens it too.
381
411
 
382
412
  To make a report actionable, include:
@@ -7,8 +7,22 @@
7
7
  *
8
8
  * Built on `@modelcontextprotocol/sdk` (McpServer over the stdio transport);
9
9
  * the SDK owns JSON-RPC framing, capability handshake, and protocol-version
10
- * negotiation. Tool input schemas are declared as zod shapes (zod ships with
11
- * the SDK); the SDK renders them to the JSON Schema advertised by tools/list.
10
+ * negotiation. Tool input schemas are declared as zod shapes; the SDK renders
11
+ * them to the JSON Schema advertised by tools/list.
12
+ *
13
+ * BOTH imports below are OPTIONAL PEERS, not dependencies — together ~12 MB,
14
+ * more than every runtime dependency of the component library combined, and
15
+ * loaded by nothing else. `bin/bitboss-ui.mjs` catches the resolution failure
16
+ * and names them; `missingMcpPeers()` warns at registration time.
17
+ *
18
+ * `zod`'s declared range is `^3.25 || ^4.0` — copied verbatim from the SDK's
19
+ * own dependency range, NOT invented. Do not "simplify" it to `>=` or to a
20
+ * single major: the schemas built here are validated by the SDK's OWN copy of
21
+ * zod, so a consumer hoisting a different MAJOR fails the SDK's brand checks
22
+ * while npm stays silent (both ranges are independently satisfied). Mirroring
23
+ * the range means it moves when the SDK's does. Everything used here
24
+ * (`z.string`, `z.enum`, `z.number`, `.describe`, `.int`, `.min`, `.optional`)
25
+ * is stable across zod 3 and 4, so the lower bound costs nothing.
12
26
  *
13
27
  * Run standalone: `npx bitboss-ui mcp` (delegated from bin/bitboss-ui.mjs).
14
28
  * Register with an MCP-capable harness via `npx bitboss-ui ai-init --mcp`.
@@ -35,11 +35,15 @@ import {
35
35
  statSync,
36
36
  writeFileSync,
37
37
  } from 'node:fs';
38
+ import { findHandRollHints } from '../scripts/lib/hand-roll-hints.mjs';
38
39
  import { createRequire } from 'node:module';
39
40
  import { dirname, isAbsolute, join, relative } from 'node:path';
40
41
  import { fileURLToPath } from 'node:url';
41
42
  import { parseArgs } from 'node:util';
42
- import { ensureMcpConfigs } from '../scripts/lib/mcp-config.mjs';
43
+ import {
44
+ ensureMcpConfigs,
45
+ missingMcpPeers,
46
+ } from '../scripts/lib/mcp-config.mjs';
43
47
  import {
44
48
  loadManifest,
45
49
  validateMarkdown,
@@ -238,6 +242,21 @@ function aiInit({ mcp = false } = {}) {
238
242
  action: result.action,
239
243
  });
240
244
  }
245
+
246
+ // The harness launches the server on demand, long after this command
247
+ // ran — so a missing peer would surface inside an agent as a server
248
+ // that never starts. Say it here, where the person is still watching.
249
+ const missing = missingMcpPeers(projectRoot);
250
+ if (missing.length > 0) {
251
+ console.warn(
252
+ `[bitboss-ui] the MCP server needs ${missing.join(' and ')}, which ${
253
+ missing.length > 1 ? 'are' : 'is'
254
+ } not installed:\n` +
255
+ `\n npm i -D ${missing.join(' ')}\n\n` +
256
+ ' Optional peers, not dependencies — ~12 MB that only the MCP\n' +
257
+ ' server uses. The harness entries above are written either way.'
258
+ );
259
+ }
241
260
  }
242
261
 
243
262
  console.log(`[bitboss-ui] ai-init complete (library v${version})`);
@@ -550,9 +569,34 @@ function checkCommand(globs, jsonMode, allowEmpty = false) {
550
569
  process.exit(1);
551
570
  }
552
571
  const manifest = loadManifest(manifestPath);
572
+
573
+ /*
574
+ * Every `--bb-*` the INSTALLED package declares, read from the stylesheet
575
+ * the consumer actually ships. Read from disk rather than hardcoded so this
576
+ * can never claim a token is invented when their version does declare it.
577
+ * Absent stylesheet → an empty set → the unknown-token hint stays silent,
578
+ * which is the right failure: no data, no accusation.
579
+ */
580
+ const stylesPath = join(PACKAGE_ROOT, 'dist', 'styles.css');
581
+ const knownTokens = existsSync(stylesPath)
582
+ ? new Set(
583
+ [
584
+ ...readFileSync(stylesPath, 'utf-8').matchAll(
585
+ /(--bb-[a-z0-9-]+)\s*:/gi
586
+ ),
587
+ ].map((m) => m[1].toLowerCase())
588
+ )
589
+ : new Set();
553
590
  const files = resolveCheckFiles(projectRoot, globs);
554
591
 
555
592
  const findings = [];
593
+ /*
594
+ * Advisory only — see scripts/lib/hand-roll-hints.mjs. Hints never touch the
595
+ * exit code, so a guess can never fail a consumer's build. `--no-hints`
596
+ * silences them for anyone who finds them noisy.
597
+ */
598
+ const hints = [];
599
+ const wantHints = !process.argv.includes('--no-hints');
556
600
  for (const file of files) {
557
601
  const relPath = relative(projectRoot, file);
558
602
  const content = readFileSync(file, 'utf-8');
@@ -562,10 +606,34 @@ function checkCommand(globs, jsonMode, allowEmpty = false) {
562
606
  for (const finding of fileFindings) {
563
607
  findings.push({ file: relPath, ...finding });
564
608
  }
609
+ // .md files are documentation ABOUT components; a round <img> in a doc
610
+ // example is illustrative, not a hand-roll.
611
+ if (wantHints && !file.endsWith('.md')) {
612
+ for (const hint of findHandRollHints(content, knownTokens)) {
613
+ hints.push({ file: relPath, ...hint });
614
+ }
615
+ }
565
616
  }
566
617
 
618
+ /** Advisory block, printed on success and on failure alike. Never fatal. */
619
+ const printHints = () => {
620
+ if (hints.length === 0) return;
621
+ console.log(
622
+ `\n[bitboss-ui] ${hints.length} hint(s) — advisory, not errors:`
623
+ );
624
+ for (const hint of hints) {
625
+ console.log(` ${hint.file}:${hint.line}`);
626
+ console.log(` → ${hint.message}`);
627
+ }
628
+ console.log(
629
+ ' Deliberate? Ignore these, record them in a HANDROLLED note, or pass --no-hints.'
630
+ );
631
+ };
632
+
567
633
  if (jsonMode) {
568
- console.log(JSON.stringify({ findings, files: files.length }, null, 2));
634
+ console.log(
635
+ JSON.stringify({ findings, hints, files: files.length }, null, 2)
636
+ );
569
637
  if (findings.length > 0) process.exit(1);
570
638
  if (files.length === 0 && globs.length > 0 && !allowEmpty) process.exit(1);
571
639
  return;
@@ -595,6 +663,7 @@ function checkCommand(globs, jsonMode, allowEmpty = false) {
595
663
  console.log(
596
664
  `[bitboss-ui] check OK — ${summarizeFileCounts(files)} file(s), zero unknown Bb* props/v-models/slots/nav-attrs.`
597
665
  );
666
+ printHints();
598
667
  return;
599
668
  }
600
669
 
@@ -620,6 +689,7 @@ function checkCommand(globs, jsonMode, allowEmpty = false) {
620
689
  console.error(
621
690
  '\nFix the markup (or the component API) so it stays copy-safe for agents.'
622
691
  );
692
+ printHints();
623
693
  process.exit(1);
624
694
  }
625
695
 
@@ -710,7 +780,27 @@ switch (command) {
710
780
  break;
711
781
  }
712
782
  case 'mcp': {
713
- const { main } = await import('./bitboss-ui-mcp.mjs');
783
+ // `@modelcontextprotocol/sdk` + `zod` are OPTIONAL peers, not
784
+ // dependencies: together they weigh ~12 MB — more than every runtime
785
+ // dependency of the component library combined, and the SDK drags in a
786
+ // whole HTTP stack (express, hono, cors, jose). Only this subcommand
787
+ // needs them, so consumers who never run the MCP server do not pay for
788
+ // it. That makes the import failure a SUPPORTED path, and a bare
789
+ // ERR_MODULE_NOT_FOUND stack is not an answer — say what to install.
790
+ let main;
791
+ try {
792
+ ({ main } = await import('./bitboss-ui-mcp.mjs'));
793
+ } catch (error) {
794
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
795
+ console.error(
796
+ 'bitboss-ui mcp needs two optional peer dependencies that are not installed:\n' +
797
+ '\n npm i -D @modelcontextprotocol/sdk zod\n\n' +
798
+ 'They are optional because they are ~12 MB and only this subcommand uses them.\n' +
799
+ `Resolution failed with: ${error.message}`
800
+ );
801
+ process.exitCode = 1;
802
+ break;
803
+ }
714
804
  await main();
715
805
  break;
716
806
  }
@@ -207,9 +207,13 @@ click handler for something a link can do.
207
207
  </div>
208
208
  ```
209
209
 
210
- These are the only two slots — there is no default or actions slot. A short
211
- inline link in `#text` (as above) is fine; anything more interactive belongs
212
- outside the alert, or in a `BbDialog`.
210
+ **The default slot is the text slot.** `<BbAlert>Something went wrong</BbAlert>`
211
+ is the shortest form and the one to reach for; `text` as a prop is for when a
212
+ string is all you have (an `:items` loop, a server message). When more than one
213
+ is present the most specific wins: `#text` > children > `text`.
214
+
215
+ There is no `actions` slot. A short inline link in the text (as above) is fine;
216
+ anything more interactive belongs outside the alert, or in a `BbDialog`.
213
217
 
214
218
  #### Brand art via a raw SVG
215
219
 
@@ -353,9 +357,6 @@ const publish = () => {
353
357
 
354
358
  ### Gotchas & anti-patterns
355
359
 
356
- - **There is no default slot.** `<BbAlert>some text</BbAlert>` renders
357
- nothing — Vue drops an unconsumed default slot silently, with no warning.
358
- Use `title`/`text` (or `#title`/`#text`, see above).
359
360
  - **Omitting `v-model` does not make an alert permanent** — the close button
360
361
  still dismisses it locally. Use `hide-close` for that.
361
362
  - Don't rebuild dismissal with your own button + `v-if`; the built-in close
@@ -407,7 +408,8 @@ const publish = () => {
407
408
 
408
409
  ## Slots
409
410
 
410
- - `text` — scope: `BbAlertTextSlotProps` — Replaces the default alert body text.
411
+ - `default` — scope: `any` — The alert body — the shortest form, and the one to reach for: `<BbAlert>Something went wrong</BbAlert>`. Same idiom as the rest of the `text`-prop family (BbButton, BbBaseButton, BbTooltip, BbIndicator). When more than one source is present…
412
+ - `text` — scope: `BbAlertTextSlotProps` — Replaces the default alert body text. Wins over children and `text`.
411
413
  - `title` — scope: `BbAlertTitleSlotProps` — Replaces the default alert title text.
412
414
 
413
415
  ## See Also
@@ -439,10 +439,12 @@ const feed: FeedEntry[] = [
439
439
  Load failures are normally caught by the image's `error` event. As a safety
440
440
  net — mainly for SSR/hydrated pages where the events fired before hydration —
441
441
  the component also checks the image `timeout` ms after mount (default `400`)
442
- and switches to the fallback if it hasn't loaded correctly by then. This makes
443
- `timeout` an effective **deadline**: an image slower than `timeout` falls back
444
- and stays on the fallback until `src` changes. If your avatars come from a
445
- slow origin, raise `timeout` rather than accepting spurious fallbacks.
442
+ and switches to the fallback if it hasn't loaded correctly by then. The
443
+ `<img>` stays mounted (visually hidden) through that switch, so the deadline
444
+ is **not** terminal: a slow image shows the fallback at `timeout` and swaps
445
+ itself back in the moment it finishes loading. Only a real `error` is
446
+ permanent until `src` changes. If your avatars come from a slow origin, raise
447
+ `timeout` rather than accepting a visible fallback-then-photo flip.
446
448
 
447
449
  Platform notes: the component is SSR-safe (Nuxt/Inertia) with no client-only
448
450
  markup; the only global config it reads is `iconDefaultSizes` for the size
@@ -461,15 +463,20 @@ keys.
461
463
 
462
464
  ### Gotchas & anti-patterns
463
465
 
464
- - **There is no `color` prop.** The fallback is always primary-token colored;
465
- don't reach for per-avatar inline backgrounds. Use variants and target the generated `bb-avatar--{{variant}}` or retheme `--bb-primary` /
466
- `--bb-primary-fg` if the palette is wrong.
466
+ - **There is no `color` prop, and no `variant` either.** The fallback is always
467
+ primary-token colored; don't reach for per-avatar inline backgrounds. If the
468
+ palette is wrong, retheme `--bb-primary` / `--bb-primary-fg` theming here
469
+ happens through the tokens, not a prop.
467
470
  - **Default `timeout` can false-fallback slow images** (see above) — tune it,
468
471
  don't reimplement image loading around the component.
469
472
  - Don't hand-position presence dots with absolute CSS — `BbIndicator dot`
470
473
  exists for that.
471
474
  - Don't wrap the avatar in a raw `<span @click>` for menus — put it inside
472
475
  `BbBaseButton`/`BbDropdown` so focus and keyboard behavior come for free.
476
+ - **The shape is a circle, and it is not configurable.** `border-radius: 50%`
477
+ is baked in. A rounded-SQUARE avatar — common in dense tables, where a square
478
+ reads as "entity" and a circle as "person" — has no answer here; build that
479
+ one yourself rather than fighting the radius.
473
480
  - Don't pre-crop sources to circles — the component center-crops; ship square
474
481
  or larger originals.
475
482
  - Don't leave `alt` empty for meaningful avatars: fallbacks lose their
@@ -14,24 +14,36 @@
14
14
  | --- | --- | --- | --- | --- |
15
15
  | `activeClass` | `string \| undefined` | | | CSS class applied when the component renders as a link and the target of the link is the current route or the url matches partially. Ported for Inertia compatibility. |
16
16
  | `ariaCurrentValue` | `"page" \| "step" \| "location" \| "date" \| "time" \| "true" \| "false" \| undefined` | | | Value forwarded to the `aria-current` attribute when the component renders as a router link and the target route is an exact match. Use to communicate the current location to assistive technologies. |
17
+ | `async` | `boolean \| undefined` | | | Inertia: runs the visit without blocking — the page stays interactive and several async visits can be in flight at once. |
18
+ | `cacheFor` | `string \| number \| (string \| number)[] \| undefined` | | | Inertia: how long a prefetched response stays fresh before it is re-fetched. A single duration, or `[staleAfter, expiresAfter]`. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue3` to be in… |
19
+ | `cacheTags` | `string \| string[] \| undefined` | | | Inertia: tags to file this visit's prefetch cache under, so a later request can invalidate the whole tagged group. |
20
+ | `component` | `string \| undefined` | | | Inertia: the page component this visit resolves to. Rarely set by hand — the server normally decides it. |
17
21
  | `data` | `object \| undefined` | | | Request payload forwarded to Inertia when navigating via `href` in an Inertia-enabled app. Ignored when not using Inertia. |
18
22
  | `disabled` | `boolean \| undefined` | | | Disables user interaction. - When rendering as a native button, sets the `disabled` attribute. - When rendering as a link (anchor/Inertia), removes `href`, adds `aria-disabled="true"`, and prevents navigation while keeping focusable semanti… |
19
23
  | `download` | `string \| boolean \| undefined` | | | Marks an `href` link as a download. Renders a plain `<a download>` doing a native navigation — never an Inertia/router visit — so file downloads (including same-origin, `blob:` and `data:` URLs) work. Pass a string to set the suggested file… |
20
24
  | `exactActiveClass` | `string \| undefined` | | | CSS class applied when the component renders as a link and the target of the link and the url matches exactly. Ported for Inertia compatibility. |
25
+ | `except` | `string[] \| undefined` | | | Inertia: the inverse of `only` — properties to EXCLUDE from a partial reload. Pass one or the other, not both. |
21
26
  | `external` | `boolean \| undefined` | | | Forces an `href` link to render as a plain `<a>` (native navigation), bypassing Inertia/router interception — the same intent as Nuxt's `NuxtLink` `external`. Use for links outside the SPA. One of the native-anchor signals alongside `target… |
22
27
  | `headers` | `object \| undefined` | | | Additional HTTP headers forwarded to Inertia when navigating via `href` in an Inertia-enabled app. |
23
28
  | `href` | `string \| undefined` | | | Hyperlink reference used when rendering as an anchor (or as an Inertia link in Inertia-enabled apps). If provided and not disabled, the component renders as an anchor/Inertia link. |
29
+ | `instant` | `boolean \| undefined` | | | Inertia: navigate optimistically on click and reconcile when the response lands, instead of waiting for the round trip. |
24
30
  | `method` | `"get" \| "post" \| "put" \| "patch" \| "delete" \| undefined` | | | HTTP method used for Inertia navigation when `href` is provided in an Inertia-enabled app. Ignored otherwise. |
25
31
  | `onBefore` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia right before the request is sent. |
26
32
  | `onCancel` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a request is cancelled. |
27
33
  | `onCancelToken` | `((cancelToken: unknown) => void) \| undefined` | | | Receives the Inertia cancel token source when a request is initiated. Can be used to cancel the request. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue3` to be installed. |
34
+ | `onError` | `((errors: Record<string, string>) => void) \| undefined` | | | Lifecycle hook invoked by Inertia when the server responds with VALIDATION errors — the ordinary 422 path. This is the only failure callback Inertia's `Link` accepts. `onHttpException` (server answered 5xx) and `onNetworkError` (the request… |
28
35
  | `onFinish` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia after the request has finished (regardless of success or error). |
29
36
  | `only` | `string[] \| undefined` | | | Limits the properties that are preserved in Inertia partial reloads. |
37
+ | `onPrefetched` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a prefetch for this link has completed and is cached. |
38
+ | `onPrefetching` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a prefetch for this link starts. |
30
39
  | `onProgress` | `((progress: { percentage: number \| undefined; }) => void) \| undefined` | | | Progress callback invoked by Inertia with the upload/download percentage when available. |
31
40
  | `onStart` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a request starts. |
32
41
  | `onSuccess` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a request succeeds. |
42
+ | `pageProps` | `Record<string, unknown> \| ((currentProps: Record<string, unknown>, sharedProps: Record<string, unknown>) => Record<string, unknown>) \| null \| undefined` | | | Inertia: props to merge into the next page optimistically, before the server responds. Either an object or a function of the current props. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue… |
43
+ | `prefetch` | `string \| boolean \| string[] \| undefined` | | | Inertia: fetch and cache this link's page ahead of the click. `true` uses the default trigger; a string or list of strings picks them (`'mount'`, `'hover'`, `'click'`). |
33
44
  | `preserveScroll` | `boolean \| ((props: Record<string, unknown>) => boolean) \| undefined` | | | Controls whether Inertia should preserve the current scroll position after navigation. Can be a boolean or a predicate receiving the visit props. |
34
45
  | `preserveState` | `boolean \| ((props: Record<string, unknown>) => boolean) \| null \| undefined` | | | Controls whether Inertia should preserve the current state after navigation. Can be a boolean or a predicate receiving the visit props. |
46
+ | `preserveUrl` | `boolean \| undefined` | | | Inertia: keep the current URL in the address bar even though the page content changes. |
35
47
  | `queryStringArrayFormat` | `"brackets" \| "indices" \| undefined` | | | Format to use when serializing array values into the query string for Inertia requests. |
36
48
  | `rel` | `string \| undefined` | | | Relationship between the current document and the linked resource. Useful for security when opening new tabs (e.g. `noopener noreferrer`). |
37
49
  | `replace` | `boolean \| undefined` | | | Uses history replacement instead of push navigation. - With Vue Router (`to`), calls `router.replace`. - With Inertia (`href`), performs a replace visit. |
@@ -39,6 +51,7 @@
39
51
  | `target` | `string \| undefined` | | | Target browsing context for anchor/Inertia links (e.g. `_self`, `_blank`). Ignored when rendering as a native button. |
40
52
  | `to` | `string \| wt \| bt \| undefined` | | | Route location to navigate to. When provided (and not disabled), the component renders as a Vue Router link. |
41
53
  | `type` | `"button" \| "submit" \| "reset" \| undefined` | | | Native `type` attribute used when rendering as a button (e.g. `button`, `submit`, `reset`). |
54
+ | `viewTransition` | `boolean \| undefined` | | | Inertia: run the page swap inside a View Transition, where the browser supports one. |
42
55
 
43
56
  ## Events
44
57
 
@@ -102,9 +102,12 @@ const pinged = ref(false);
102
102
  ### It ships no chrome — you style it
103
103
 
104
104
  The only classes it applies are structural: `bb-base-button` (a focus-ring
105
- scaffold, `cursor`, inherited text color and transitions), `bb-base-button--block`
106
- and `bb-base-button--disabled`. There is **no padding, background, border, or
107
- variant appearance** — that is the whole point. Give it a class (your own, or
105
+ scaffold, `cursor` and transitions), `bb-base-button--block` and
106
+ `bb-base-button--disabled`. There is **no padding, background, border, or
107
+ variant appearance** — that is the whole point. It sets **no text color**, so a
108
+ `text-*` utility on the element just works with no specificity fight; the text
109
+ inherits from its container (in anchor mode, the browser's blue-and-underlined
110
+ link chrome is neutralized so it inherits too). Give it a class (your own, or
108
111
  Tailwind utilities) and it looks like whatever you draw. This is what makes it
109
112
  the right base for a clickable card or a bespoke nav item:
110
113
 
@@ -454,25 +457,37 @@ const sent = ref(false);
454
457
  | --- | --- | --- | --- | --- |
455
458
  | `activeClass` | `string \| undefined` | | | CSS class applied when the component renders as a link and the target of the link is the current route or the url matches partially. Ported for Inertia compatibility. |
456
459
  | `ariaCurrentValue` | `"page" \| "step" \| "location" \| "date" \| "time" \| "true" \| "false" \| undefined` | | | Value forwarded to the `aria-current` attribute when the component renders as a router link and the target route is an exact match. Use to communicate the current location to assistive technologies. |
460
+ | `async` | `boolean \| undefined` | | | Inertia: runs the visit without blocking — the page stays interactive and several async visits can be in flight at once. |
457
461
  | `block` | `boolean \| undefined` | | | Makes the component take the full available width (block-level layout). Adds the `bb-base-button--block` modifier class. |
462
+ | `cacheFor` | `string \| number \| (string \| number)[] \| undefined` | | | Inertia: how long a prefetched response stays fresh before it is re-fetched. A single duration, or `[staleAfter, expiresAfter]`. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue3` to be in… |
463
+ | `cacheTags` | `string \| string[] \| undefined` | | | Inertia: tags to file this visit's prefetch cache under, so a later request can invalidate the whole tagged group. |
464
+ | `component` | `string \| undefined` | | | Inertia: the page component this visit resolves to. Rarely set by hand — the server normally decides it. |
458
465
  | `data` | `object \| undefined` | | | Request payload forwarded to Inertia when navigating via `href` in an Inertia-enabled app. Ignored when not using Inertia. |
459
466
  | `disabled` | `boolean \| undefined` | | | Disables user interaction. - When rendering as a native button, sets the `disabled` attribute. - When rendering as a link (anchor/Inertia), removes `href`, adds `aria-disabled="true"`, and prevents navigation while keeping focusable semanti… |
460
467
  | `download` | `string \| boolean \| undefined` | | | Marks an `href` link as a download. Renders a plain `<a download>` doing a native navigation — never an Inertia/router visit — so file downloads (including same-origin, `blob:` and `data:` URLs) work. Pass a string to set the suggested file… |
461
468
  | `exactActiveClass` | `string \| undefined` | | | CSS class applied when the component renders as a link and the target of the link and the url matches exactly. Ported for Inertia compatibility. |
469
+ | `except` | `string[] \| undefined` | | | Inertia: the inverse of `only` — properties to EXCLUDE from a partial reload. Pass one or the other, not both. |
462
470
  | `external` | `boolean \| undefined` | | | Forces an `href` link to render as a plain `<a>` (native navigation), bypassing Inertia/router interception — the same intent as Nuxt's `NuxtLink` `external`. Use for links outside the SPA. One of the native-anchor signals alongside `target… |
463
471
  | `headers` | `object \| undefined` | | | Additional HTTP headers forwarded to Inertia when navigating via `href` in an Inertia-enabled app. |
464
472
  | `href` | `string \| undefined` | | | Hyperlink reference used when rendering as an anchor (or as an Inertia link in Inertia-enabled apps). If provided and not disabled, the component renders as an anchor/Inertia link. |
473
+ | `instant` | `boolean \| undefined` | | | Inertia: navigate optimistically on click and reconcile when the response lands, instead of waiting for the round trip. |
465
474
  | `method` | `"get" \| "post" \| "put" \| "patch" \| "delete" \| undefined` | | | HTTP method used for Inertia navigation when `href` is provided in an Inertia-enabled app. Ignored otherwise. |
466
475
  | `onBefore` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia right before the request is sent. |
467
476
  | `onCancel` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a request is cancelled. |
468
477
  | `onCancelToken` | `((cancelToken: unknown) => void) \| undefined` | | | Receives the Inertia cancel token source when a request is initiated. Can be used to cancel the request. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue3` to be installed. |
478
+ | `onError` | `((errors: Record<string, string>) => void) \| undefined` | | | Lifecycle hook invoked by Inertia when the server responds with VALIDATION errors — the ordinary 422 path. This is the only failure callback Inertia's `Link` accepts. `onHttpException` (server answered 5xx) and `onNetworkError` (the request… |
469
479
  | `onFinish` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia after the request has finished (regardless of success or error). |
470
480
  | `only` | `string[] \| undefined` | | | Limits the properties that are preserved in Inertia partial reloads. |
481
+ | `onPrefetched` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a prefetch for this link has completed and is cached. |
482
+ | `onPrefetching` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a prefetch for this link starts. |
471
483
  | `onProgress` | `((progress: { percentage: number \| undefined; }) => void) \| undefined` | | | Progress callback invoked by Inertia with the upload/download percentage when available. |
472
484
  | `onStart` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a request starts. |
473
485
  | `onSuccess` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a request succeeds. |
486
+ | `pageProps` | `Record<string, unknown> \| ((currentProps: Record<string, unknown>, sharedProps: Record<string, unknown>) => Record<string, unknown>) \| null \| undefined` | | | Inertia: props to merge into the next page optimistically, before the server responds. Either an object or a function of the current props. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue… |
487
+ | `prefetch` | `string \| boolean \| string[] \| undefined` | | | Inertia: fetch and cache this link's page ahead of the click. `true` uses the default trigger; a string or list of strings picks them (`'mount'`, `'hover'`, `'click'`). |
474
488
  | `preserveScroll` | `boolean \| ((props: Record<string, unknown>) => boolean) \| undefined` | | | Controls whether Inertia should preserve the current scroll position after navigation. Can be a boolean or a predicate receiving the visit props. |
475
489
  | `preserveState` | `boolean \| ((props: Record<string, unknown>) => boolean) \| null \| undefined` | | | Controls whether Inertia should preserve the current state after navigation. Can be a boolean or a predicate receiving the visit props. |
490
+ | `preserveUrl` | `boolean \| undefined` | | | Inertia: keep the current URL in the address bar even though the page content changes. |
476
491
  | `queryStringArrayFormat` | `"brackets" \| "indices" \| undefined` | | | Format to use when serializing array values into the query string for Inertia requests. |
477
492
  | `rel` | `string \| undefined` | | | Relationship between the current document and the linked resource. Useful for security when opening new tabs (e.g. `noopener noreferrer`). |
478
493
  | `replace` | `boolean \| undefined` | | | Uses history replacement instead of push navigation. - With Vue Router (`to`), calls `router.replace`. - With Inertia (`href`), performs a replace visit. |
@@ -481,6 +496,7 @@ const sent = ref(false);
481
496
  | `text` | `string \| undefined` | | | Fallback text content rendered when no default slot is provided. |
482
497
  | `to` | `string \| wt \| bt \| undefined` | | | Route location to navigate to. When provided (and not disabled), the component renders as a Vue Router link. |
483
498
  | `type` | `"button" \| "submit" \| "reset" \| undefined` | `"button"` | | Native `type` attribute used when rendering as a button (e.g. `button`, `submit`, `reset`). |
499
+ | `viewTransition` | `boolean \| undefined` | | | Inertia: run the page swap inside a View Transition, where the browser supports one. |
484
500
 
485
501
  ## Events
486
502
 
@@ -642,12 +642,35 @@ Under Inertia, `href` accepts the full visit surface as props, grouped by
642
642
  concern:
643
643
 
644
644
  - **Request shape** — `method` (`get`…`delete`), `data`, `headers`, `only`
645
- (partial reloads), `query-string-array-format`.
645
+ (partial reloads), `except` (the inverse of `only` — pass one or the other),
646
+ `query-string-array-format`, `component`.
646
647
  - **History & scroll** — `replace`, `preserve-scroll`, `preserve-state` (each
647
- also accepts a callback form).
648
+ also accepts a callback form), `preserve-url` (change the page, keep the
649
+ address bar), `view-transition`.
650
+ - **Prefetching** — `prefetch` (`true`, or `'mount'`/`'hover'`/`'click'`),
651
+ `cache-for` (a duration, or `[staleAfter, expiresAfter]`), `cache-tags` for
652
+ group invalidation.
653
+ - **Speed** — `async` (don't block the page), `instant` (navigate optimistically
654
+ and reconcile on response), `page-props` (props to merge before the server
655
+ answers).
648
656
  - **Lifecycle hooks** — `onBefore`, `onStart`, `onProgress`, `onSuccess`,
649
- `onCancel`, `onFinish`, plus `onCancelToken` to capture a cancel handle.
650
- Bind them as props (`:on-success="..."`), not `@` listeners.
657
+ `onError`, `onCancel`, `onFinish`, plus `onCancelToken` to capture a cancel
658
+ handle and `onPrefetching`/`onPrefetched` for the prefetch pair. Bind them as
659
+ props (`:on-success="..."`), not `@` listeners.
660
+
661
+ **`onError` here means VALIDATION errors** — the ordinary 422 — and it is the
662
+ only failure callback `<Link>` accepts. Inertia's other two,
663
+ `onHttpException` (the server answered 5xx) and `onNetworkError` (the request
664
+ never arrived — offline, DNS, connection reset), are **`router.visit()` /
665
+ `useForm()` options, not Link props**; passing them to a link-shaped button
666
+ does nothing. Reach for a form or an explicit visit when you need to tell those
667
+ apart — see the Inertia helpers guide.
668
+
669
+ Every prop Inertia's own `<Link>` accepts is forwarded, and
670
+ `npm run check:inertia-props` fails the build both when Inertia adds one we
671
+ don't forward and when we declare a callback `<Link>` never accepted. The list
672
+ has to be hand-written, because `@inertiajs/vue3` is an optional peer the
673
+ library's public types are not allowed to import.
651
674
 
652
675
  ```vue
653
676
  <!-- Inertia: href becomes a visit; the whole option set rides along -->
@@ -658,11 +681,19 @@ concern:
658
681
  preserve-scroll
659
682
  :only="['invoices']"
660
683
  :on-success="() => drawer.close()"
684
+ :on-error="(errors) => (formErrors = errors)"
661
685
  >
662
686
  Create invoice
663
687
  </BbButton>
664
688
  ```
665
689
 
690
+ ```vue
691
+ <!-- Prefetch on hover so the detail page is already cached by the click -->
692
+ <BbButton href="/invoices/42" prefetch="hover" cache-for="30s" variant="link">
693
+ Invoice #42
694
+ </BbButton>
695
+ ```
696
+
666
697
  ### Works well with
667
698
 
668
699
  - `BbDropdownButton` — escalate to it when a primary action grows close
@@ -851,18 +882,24 @@ Anti-patterns:
851
882
  | `activeClass` | `string \| undefined` | | | Class to apply when the link is active. |
852
883
  | `append:icon` | `string \| undefined` | | | Icon to be added on the right of the text. |
853
884
  | `ariaCurrentValue` | `"page" \| "step" \| "location" \| "date" \| "time" \| "true" \| "false" \| undefined` | | | Value passed to the attribute `aria-current` when the link is exact active. |
885
+ | `async` | `boolean \| undefined` | | | Inertia: runs the visit without blocking — the page stays interactive and several async visits can be in flight at once. |
854
886
  | `block` | `boolean \| undefined` | | | Displays the component as full width. |
887
+ | `cacheFor` | `string \| number \| (string \| number)[] \| undefined` | | | Inertia: how long a prefetched response stays fresh before it is re-fetched. A single duration, or `[staleAfter, expiresAfter]`. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue3` to be in… |
888
+ | `cacheTags` | `string \| string[] \| undefined` | | | Inertia: tags to file this visit's prefetch cache under, so a later request can invalidate the whole tagged group. |
889
+ | `component` | `string \| undefined` | | | Inertia: the page component this visit resolves to. Rarely set by hand — the server normally decides it. |
855
890
  | `data` | `object \| undefined` | | | |
856
891
  | `disableAutoLoading` | `boolean \| undefined` | | | Disables the automatic loading state that tracks async click handlers (enabled by default). |
857
892
  | `disabled` | `boolean \| undefined` | | | Disables the component. |
858
893
  | `download` | `string \| boolean \| undefined` | | | Marks an `href` link as a download: renders a plain `<a download>` doing a native navigation (never an Inertia/router visit), so file downloads work. Pass a string for the suggested filename. A native-anchor signal alongside `target` and `e… |
859
894
  | `exactActiveClass` | `string \| undefined` | | | Class to apply when the link is exact active. |
895
+ | `except` | `string[] \| undefined` | | | Inertia: the inverse of `only` — properties to EXCLUDE from a partial reload. Pass one or the other, not both. |
860
896
  | `external` | `boolean \| undefined` | | | Forces an `href` link to render as a plain `<a>` (native navigation), bypassing Inertia/router interception — same intent as Nuxt's `external`. A native-anchor signal alongside `target` and `download`. |
861
897
  | `falseValue` | `any` | `false` | | Value emitted when the toggle is deactivated. |
862
898
  | `group` | `boolean \| undefined` | | | Identifies the button as part of a button group. |
863
899
  | `headers` | `object \| undefined` | | | |
864
900
  | `href` | `string \| undefined` | | | Returns the hyperlink's URL. Can be set, to change the URL. |
865
901
  | `icon` | `string \| undefined` | | | Used when only an icon with no text should be displayed. |
902
+ | `instant` | `boolean \| undefined` | | | Inertia: navigate optimistically on click and reconcile when the response lands, instead of waiting for the round trip. |
866
903
  | `interactiveWhileLoading` | `boolean \| undefined` | | | Keeps the button clickable while it is loading. Buttons are disabled while loading by default; this escape hatch exists for toggles that must stay interactive (e.g. the dropdown-button caret). |
867
904
  | `loading` | `boolean \| undefined` | | | Triggers a loading indicator. |
868
905
  | `method` | `"get" \| "post" \| "put" \| "patch" \| "delete" \| undefined` | | | |
@@ -870,14 +907,20 @@ Anti-patterns:
870
907
  | `onBefore` | `(() => void) \| undefined` | | | |
871
908
  | `onCancel` | `(() => void) \| undefined` | | | |
872
909
  | `onCancelToken` | `((cancelToken: unknown) => void) \| undefined` | | | |
910
+ | `onError` | `((errors: Record<string, string>) => void) \| undefined` | | | Lifecycle hook invoked by Inertia when the server responds with VALIDATION errors — the ordinary 422 path. This is the only failure callback Inertia's `Link` accepts. `onHttpException` (server answered 5xx) and `onNetworkError` (the request… |
873
911
  | `onFinish` | `(() => void) \| undefined` | | | |
874
912
  | `only` | `string[] \| undefined` | | | |
913
+ | `onPrefetched` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a prefetch for this link has completed and is cached. |
914
+ | `onPrefetching` | `(() => void) \| undefined` | | | Lifecycle hook invoked by Inertia when a prefetch for this link starts. |
875
915
  | `onProgress` | `((progress: { percentage: number \| undefined; }) => void) \| undefined` | | | |
876
916
  | `onStart` | `(() => void) \| undefined` | | | |
877
917
  | `onSuccess` | `(() => void) \| undefined` | | | |
918
+ | `pageProps` | `Record<string, unknown> \| ((currentProps: Record<string, unknown>, sharedProps: Record<string, unknown>) => Record<string, unknown>) \| null \| undefined` | | | Inertia: props to merge into the next page optimistically, before the server responds. Either an object or a function of the current props. Typed platform-agnostically (matching BbButton) so the library's types never require `@inertiajs/vue… |
919
+ | `prefetch` | `string \| boolean \| string[] \| undefined` | | | Inertia: fetch and cache this link's page ahead of the click. `true` uses the default trigger; a string or list of strings picks them (`'mount'`, `'hover'`, `'click'`). |
878
920
  | `prepend:icon` | `string \| undefined` | | | Icon to be added on the left of the text. |
879
921
  | `preserveScroll` | `boolean \| ((props: Record<string, unknown>) => boolean) \| undefined` | | | |
880
922
  | `preserveState` | `boolean \| ((props: Record<string, unknown>) => boolean) \| null \| undefined` | | | |
923
+ | `preserveUrl` | `boolean \| undefined` | | | Inertia: keep the current URL in the address bar even though the page content changes. |
881
924
  | `queryStringArrayFormat` | `"brackets" \| "indices" \| undefined` | | | |
882
925
  | `replace` | `boolean \| undefined` | | | Calls `router.replace` instead of `router.push`. |
883
926
  | `size` | `Responsive<Sizes> \| undefined` | `"md"` | | Sets the size of the button. A single value applies at every breakpoint; a per-breakpoint map (e.g. `{ default: 'sm', lg: 'md' }`) switches responsively. Purely CSS-driven — no runtime breakpoint watching. |
@@ -888,6 +931,7 @@ Anti-patterns:
888
931
  | `trueValue` | `any` | `true` | | Value emitted when the toggle is activated. |
889
932
  | `type` | `"button" \| "submit" \| "reset" \| undefined` | | | Gets the classification and default behavior of the button. |
890
933
  | `variant` | `ButtonVariantType \| undefined` | `"primary"` | | Visual variant of the button. Controls colors and surface style while keeping spacing and sizing unchanged. |
934
+ | `viewTransition` | `boolean \| undefined` | | | Inertia: run the page swap inside a View Transition, where the browser supports one. |
891
935
 
892
936
  ## Events
893
937
 
@@ -354,7 +354,6 @@ activator slot: the toggle button is part of the component's contract.
354
354
  ```vue
355
355
  <BbDropdownButton
356
356
  right:icon="lucide:ellipsis"
357
- size="sm"
358
357
  variant="outline"
359
358
  :items="[
360
359
  { key: 'open', text: 'Open', href: '#open' },