bitboss-ui 3.0.0-beta.61 → 3.0.0-beta.63

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 (108) hide show
  1. package/bin/bitboss-ui-mcp.mjs +103 -15
  2. package/bin/bitboss-ui.mjs +3 -1
  3. package/dist/ai/BbAlert.md +3 -0
  4. package/dist/ai/BbConfirm.md +46 -22
  5. package/dist/ai/BbForm.md +68 -5
  6. package/dist/ai/BbHost.md +11 -13
  7. package/dist/ai/BbPtScope.md +6 -4
  8. package/dist/ai/BbTable.md +180 -47
  9. package/dist/ai/BbToast.md +2 -1
  10. package/dist/ai/changelog.json +70 -4
  11. package/dist/ai/components.json +6 -6
  12. package/dist/ai/composables/useConfirm.md +8 -5
  13. package/dist/ai/composables/useToast.md +15 -15
  14. package/dist/ai/guides/agent-contract.md +3 -2
  15. package/dist/ai/guides/eslint-rules.md +2 -0
  16. package/dist/ai/guides/inertia-helpers.md +39 -7
  17. package/dist/ai/guides/installation-and-plugin-setup.md +2 -1
  18. package/dist/ai/guides/migration/components/bb-host.md +4 -2
  19. package/dist/ai/guides/migration/components/use-confirm.md +15 -15
  20. package/dist/ai/guides/migration/v2-to-v3.md +23 -0
  21. package/dist/ai/guides/migration/v3-beta-tooling.md +6 -0
  22. package/dist/ai/guides/passthrough.md +58 -58
  23. package/dist/ai/guides/validated-forms.md +8 -2
  24. package/dist/ai/helpers/inertia/manifest.json +18 -10
  25. package/dist/ai/helpers/inertia/useConfirmOnRouteLeave.ts +11 -2
  26. package/dist/ai/helpers/inertia/usePromiseForm.ts +17 -3
  27. package/dist/ai/helpers/inertia/useResourceContext.ts +1 -2
  28. package/dist/ai/manifest/components/BbHost.tree.json +1 -2
  29. package/dist/ai/manifest/components/BbTable.json +11 -7
  30. package/dist/ai/manifest/components/BbTable.tree.json +4 -1
  31. package/dist/ai/manifest/index.json +2 -2
  32. package/dist/ai/manifest/types.advanced.json +2 -2
  33. package/dist/ai/recipes/inertia/onboarding.md +10 -6
  34. package/dist/ai/recipes/inertia/record-form.md +6 -2
  35. package/dist/ai/recipes/inertia/wizard-form.md +9 -5
  36. package/dist/ai/source/BbConfirm.md +33 -21
  37. package/dist/ai/source/BbConfirmPortal.md +7 -16
  38. package/dist/ai/source/BbDatePickerInput.md +6 -1
  39. package/dist/ai/source/BbForm.md +7 -2
  40. package/dist/ai/source/BbHost.md +3 -3
  41. package/dist/ai/source/BbTable.md +440 -120
  42. package/dist/ai/source/BbTimePickerInput.md +5 -1
  43. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +83 -74
  44. package/dist/components/BbConfirm/BbConfirmHost.d.ts +10 -0
  45. package/dist/components/BbConfirm/BbConfirmHost.js +32 -0
  46. package/dist/components/BbConfirm/BbConfirmPortal.vue_vue_type_script_setup_true_lang.js +8 -12
  47. package/dist/components/BbConfirm/confirmEntry.d.ts +4 -0
  48. package/dist/components/BbConfirm/confirmEntry.js +4 -0
  49. package/dist/components/BbConfirm/types.d.ts +2 -2
  50. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +211 -210
  51. package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +3 -1
  52. package/dist/components/BbHost/BbHost.vue_vue_type_script_setup_true_lang.js +2 -2
  53. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +1302 -1184
  54. package/dist/components/BbTable/BbTableDataRow.js +17 -12
  55. package/dist/components/BbTable/BbTableExpandRow.d.ts +1 -1
  56. package/dist/components/BbTable/BbTableExpandRow.js +29 -25
  57. package/dist/components/BbTable/BbTableSelectToggle.d.ts +1 -1
  58. package/dist/components/BbTable/types.d.ts +51 -18
  59. package/dist/components/BbTable/types.js +3 -0
  60. package/dist/components/BbTable/utils.d.ts +73 -13
  61. package/dist/components/BbTable/utils.js +148 -37
  62. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +108 -106
  63. package/dist/composables/useBbFormContext.d.ts +7 -3
  64. package/dist/composables/useConfirm.d.ts +327 -33
  65. package/dist/composables/useConfirm.js +93 -71
  66. package/dist/composables/useFormBinding.d.ts +5 -0
  67. package/dist/composables/useFormBinding.js +2 -1
  68. package/dist/i18n/locales/cs.json +1 -0
  69. package/dist/i18n/locales/da.json +1 -0
  70. package/dist/i18n/locales/de-CH.json +1 -0
  71. package/dist/i18n/locales/de.json +1 -0
  72. package/dist/i18n/locales/el.json +1 -0
  73. package/dist/i18n/locales/en-GB.json +1 -0
  74. package/dist/i18n/locales/en.json +1 -0
  75. package/dist/i18n/locales/es-MX.json +1 -0
  76. package/dist/i18n/locales/es.json +1 -0
  77. package/dist/i18n/locales/fi.json +1 -0
  78. package/dist/i18n/locales/fr.json +1 -0
  79. package/dist/i18n/locales/hu.json +1 -0
  80. package/dist/i18n/locales/it.json +1 -0
  81. package/dist/i18n/locales/ja.json +1 -0
  82. package/dist/i18n/locales/ko.json +1 -0
  83. package/dist/i18n/locales/nb.json +1 -0
  84. package/dist/i18n/locales/nl.json +1 -0
  85. package/dist/i18n/locales/pl.json +1 -0
  86. package/dist/i18n/locales/pt-BR.json +1 -0
  87. package/dist/i18n/locales/pt-PT.json +1 -0
  88. package/dist/i18n/locales/ro.json +1 -0
  89. package/dist/i18n/locales/ru.json +1 -0
  90. package/dist/i18n/locales/sv.json +1 -0
  91. package/dist/i18n/locales/tr.json +1 -0
  92. package/dist/i18n/locales/uk.json +1 -0
  93. package/dist/i18n/locales/zh-CN.json +1 -0
  94. package/dist/i18n/locales/zh-HK.json +1 -0
  95. package/dist/i18n/locales/zh-TW.json +1 -0
  96. package/dist/llms-full.txt +524 -212
  97. package/dist/llms-medium.txt +5 -3
  98. package/dist/locale-blueprint.json +1 -0
  99. package/dist/styles.css +2 -2
  100. package/dist/types/passthrough.d.ts +1 -1
  101. package/dist/types/ptComponentMap.d.ts +1 -1
  102. package/dist/utils/versionCheck.js +1 -1
  103. package/package.json +8 -4
  104. package/scripts/lib/doc-sections.mjs +26 -6
  105. package/scripts/lib/eslint-disable.mjs +2 -0
  106. package/scripts/lib/eslint-plugin.d.ts +2 -0
  107. package/scripts/lib/eslint-plugin.mjs +105 -0
  108. package/scripts/lib/validate-bb-markup.mjs +182 -66
@@ -1923,9 +1923,17 @@ let cachedDocIndex = null;
1923
1923
  function docIndex() {
1924
1924
  if (cachedDocIndex) return cachedDocIndex;
1925
1925
  const entries = [];
1926
- const addSections = (source, markdown, read) => {
1926
+ // What a document says before its first `##` (its lead), and then every
1927
+ // section. The lead used to be skipped, and a composable was cut at 4000
1928
+ // chars: useToast's object-form row sat past the cut (2026-10-20).
1929
+ // `check-mcp` walks dist/ai and fails on any doc unit left out of here.
1930
+ const addSections = (source, markdown, read, lead, skip = () => false) => {
1931
+ const intro = stripFrontmatter(markdown).split(/^## /m)[0].trim();
1932
+ if (lead && intro.length >= 40)
1933
+ entries.push({ source, ...lead, text: intro });
1927
1934
  for (const section of splitSections(markdown)) {
1928
- if (section.own.length < 40) continue;
1935
+ // `skip` first: it tracks the `##` it is under, even a bare one.
1936
+ if (skip(section) || section.own.length < 40) continue;
1929
1937
  entries.push({
1930
1938
  source,
1931
1939
  section: section.title,
@@ -1936,13 +1944,18 @@ function docIndex() {
1936
1944
  };
1937
1945
  const guidesDir = join(AI_ROOT, 'guides');
1938
1946
  for (const path of collectGuidePaths(guidesDir)) {
1947
+ const markdown = readFileSync(join(guidesDir, path), 'utf-8');
1939
1948
  addSections(
1940
1949
  `guide ${path}`,
1941
- readFileSync(join(guidesDir, path), 'utf-8'),
1950
+ markdown,
1942
1951
  (title) => ({
1943
1952
  tool: 'get_guide',
1944
1953
  args: { name: path, sections: [title] },
1945
- })
1954
+ }),
1955
+ {
1956
+ section: frontmatter(markdown).title ?? path,
1957
+ read: { tool: 'get_guide', args: { name: path } },
1958
+ }
1946
1959
  );
1947
1960
  }
1948
1961
  const composablesDir = join(AI_ROOT, 'composables');
@@ -1952,16 +1965,22 @@ function docIndex() {
1952
1965
  )) {
1953
1966
  const markdown = readFileSync(join(composablesDir, file), 'utf-8');
1954
1967
  const name = file.slice(0, -3);
1955
- entries.push({
1956
- source: `composable ${name}`,
1957
- section: frontmatter(markdown).summary ?? name,
1958
- text: stripFrontmatter(markdown).slice(0, 4000),
1959
- read: { tool: 'get_composable', args: { name } },
1960
- });
1968
+ addSections(
1969
+ `composable ${name}`,
1970
+ markdown,
1971
+ (title) => ({
1972
+ tool: 'get_composable',
1973
+ args: { name, sections: [title] },
1974
+ }),
1975
+ {
1976
+ section: frontmatter(markdown).summary ?? name,
1977
+ read: { tool: 'get_composable', args: { name } },
1978
+ }
1979
+ );
1961
1980
  }
1962
1981
  }
1963
1982
  const manifest = loadComponentsManifest();
1964
- for (const component of manifest.components.filter((c) => !c.referenceOnly)) {
1983
+ for (const component of manifest.components) {
1965
1984
  const path = join(PACKAGE_ROOT, 'dist', component.contract ?? '');
1966
1985
  if (component.contract && existsSync(path)) {
1967
1986
  for (const section of contractGuideSections(
@@ -1979,6 +1998,11 @@ function docIndex() {
1979
1998
  });
1980
1999
  }
1981
2000
  }
2001
+ // BbToast and BbConfirm are reference-only (a composable drives them)
2002
+ // but their guides are the docs: § Variant icons never ranked
2003
+ // (progetto-32, 2026-10-20). Internals have no guide, so nothing else
2004
+ // comes in; their props stay out.
2005
+ if (component.referenceOnly) continue;
1982
2006
  for (const prop of component.props) {
1983
2007
  if (prop.name.startsWith('pt')) continue;
1984
2008
  entries.push({
@@ -2003,6 +2027,45 @@ function docIndex() {
2003
2027
  },
2004
2028
  });
2005
2029
  }
2030
+ // Slots, v-models and pt parts / states: `members` serves each row, and
2031
+ // "BbSelect option slot" had nothing to find.
2032
+ const member = (section, text, name) =>
2033
+ entries.push({
2034
+ source: `component ${component.name}`,
2035
+ section,
2036
+ text,
2037
+ read: {
2038
+ tool: 'get_component',
2039
+ args: { name: component.name, members: [name] },
2040
+ },
2041
+ });
2042
+ for (const slot of component.slots ?? [])
2043
+ member(
2044
+ `slot #${slot.name}`,
2045
+ `#${slot.name} slot. ${slot.description ?? ''}`,
2046
+ `#${slot.name}`
2047
+ );
2048
+ for (const model of component.models ?? []) {
2049
+ const name =
2050
+ model.name === 'modelValue' ? 'v-model' : `v-model:${model.name}`;
2051
+ member(
2052
+ name,
2053
+ `${name}: ${model.type ?? ''}. ${model.description ?? ''}`,
2054
+ name
2055
+ );
2056
+ }
2057
+ for (const part of component.pt?.parts ?? [])
2058
+ member(
2059
+ `pt part ${part.name}`,
2060
+ `pt:${part.name} part. ${part.description ?? ''}`,
2061
+ `pt:${part.name}`
2062
+ );
2063
+ for (const state of component.pt?.states ?? [])
2064
+ member(
2065
+ `pt state ${state.name}`,
2066
+ `pt state ${state.name}. ${state.description ?? ''}`,
2067
+ `pt:${state.name}`
2068
+ );
2006
2069
  /*
2007
2070
  * The rename registry: an agent upgrading an app searches the OLD name
2008
2071
  * (`enabled-while-loading`, `pt:input`, `#no-data`), which no current
@@ -2053,6 +2116,22 @@ function docIndex() {
2053
2116
  text: frontmatter(markdown).summary ?? name,
2054
2117
  read: { tool: 'get_recipe', args: { platform, name } },
2055
2118
  });
2119
+ // The prose sections; the code files under "The code" are `files`.
2120
+ let inCode = false;
2121
+ addSections(
2122
+ `recipe ${platform}/${name}`,
2123
+ markdown,
2124
+ (title) => ({
2125
+ tool: 'get_recipe',
2126
+ args: { platform, name, sections: [title] },
2127
+ }),
2128
+ null,
2129
+ (section) => {
2130
+ if (section.level === 2)
2131
+ inCode = /^the (code|files)\b/i.test(section.title);
2132
+ return inCode;
2133
+ }
2134
+ );
2056
2135
  }
2057
2136
  }
2058
2137
  // Class renames (the sweep map rows of changelog.json): an app's
@@ -2151,10 +2230,18 @@ function docIndex() {
2151
2230
  if (existsSync(readme)) {
2152
2231
  // Served by get_guide under the name 'README' (it used to point at a
2153
2232
  // "read file" call, which is not a tool).
2154
- addSections('README', readFileSync(readme, 'utf-8'), (title) => ({
2155
- tool: 'get_guide',
2156
- args: { name: 'README', sections: [title] },
2157
- }));
2233
+ addSections(
2234
+ 'README',
2235
+ readFileSync(readme, 'utf-8'),
2236
+ (title) => ({
2237
+ tool: 'get_guide',
2238
+ args: { name: 'README', sections: [title] },
2239
+ }),
2240
+ {
2241
+ section: 'README',
2242
+ read: { tool: 'get_guide', args: { name: 'README' } },
2243
+ }
2244
+ );
2158
2245
  }
2159
2246
  cachedDocIndex = entries;
2160
2247
  return entries;
@@ -3341,6 +3428,7 @@ export {
3341
3428
  SERVER_INSTRUCTIONS,
3342
3429
  searchComponents,
3343
3430
  searchDocs,
3431
+ docIndex,
3344
3432
  getComposable,
3345
3433
  getComponent,
3346
3434
  listTokens,
@@ -1735,7 +1735,9 @@ Commands:
1735
1735
  \`warnings[]\` (never fail the run) use the same shape with
1736
1736
  \`severity: "warning"\`: \`likely-squashed-slot\` (a slot
1737
1737
  name that looks like a camelCase key squashed, e.g.
1738
- \`#createdat\`), \`local-bb-component\` (your own
1738
+ \`#createdat\`), \`computed-slot-key\` (a computed slot
1739
+ name over keys \`check\` cannot see, not spelled
1740
+ through \`slotKey(…)\`), \`local-bb-component\` (your own
1739
1741
  component uses the \`Bb\` prefix; rename it),
1740
1742
  \`single-reason\` (an \`items\` provider that tests
1741
1743
  \`reason\` against one cause other than \`'search'\`;
@@ -107,6 +107,9 @@ distinct success tone.
107
107
 
108
108
  #### Variant icons
109
109
 
110
+ A default icon per variant, set once for the whole app: the object form of
111
+ the plugin's `alertVariants` option.
112
+
110
113
  ```ts
111
114
  // vite.config.ts (Nuxt: the `bitboss` key)
112
115
  bitbossUi({
@@ -72,23 +72,21 @@ first — this is that gate.
72
72
  Mount `<BbHost />` **exactly once, at the app root** (`App.vue` under Vue,
73
73
  `app.vue` under Nuxt, the `createInertiaApp` render root under Inertia) — a
74
74
  single instance serves the whole app, and `confirm()` can then be called from
75
- anywhere, components and stores alike. **Calls do not queue — a new
76
- `confirm()` supersedes the open one**, and the superseded promise settles
77
- `false` (an implicit dismiss; no callback runs). So two `confirm()` calls in a
78
- row show only the second, and the first silently answers "no". If you need both
79
- questions asked, `await` the first before opening the second.
75
+ anywhere, components and stores alike. **Every call opens its own dialog**,
76
+ and a new one covers the open one, which stays pending and comes back when
77
+ the newer one closes (see [Several confirms at once](#several-confirms-at-once)).
80
78
  A second host renders nothing (a dev-mode console warning says so). With no
81
79
  host, `await confirm(...)` resolves `false` after one second, with a dev
82
80
  warning. A host that unmounts while its dialog is open (a layout the user
83
81
  navigated out of) gets the same second to come back; after that the open
84
82
  confirm resolves `false` without running a callback, so it never hangs.
85
83
 
86
- **Areas with their own look.** The dialog takes the passthrough scope and the
87
- tokens of the place its HOST is mounted. When the app is split into areas that
88
- each wrap their pages in `<BbPtScope>` ([passthrough guide](./guides/passthrough.md) § Named scopes),
89
- mount one `<BbHost />` inside each area's layout, within its scope, and none
90
- at the app root — which holds only if **every** page renders inside exactly one
91
- of those layouts, so there is still one host at a time.
84
+ **Areas with their own look.** A confirm takes the passthrough scope and the
85
+ tokens of the component that called `useConfirm()`, wherever the host is
86
+ mounted: a page inside a `<BbPtScope>` ([passthrough guide](./guides/passthrough.md) § Named scopes)
87
+ gets confirms in its area's look with nothing to set up. A component portal
88
+ injects from that component's tree too. A `useConfirm()` called outside a
89
+ component (a store, module level) takes the host's.
92
90
 
93
91
  ```vue
94
92
  <template>
@@ -105,10 +103,9 @@ import { BbHost } from 'bitboss-ui';
105
103
  standalone example has no app root to mount it in.** In a real app the host is
106
104
  already mounted at the root; a page that calls `confirm()` mounts nothing.
107
105
 
108
- The component itself takes almost no props: everything — content, buttons,
109
- behavior — travels in the message you pass to `confirm()`. The one exception is
110
- `size`, a host-wide default width every confirm inherits unless its message
111
- sets its own (see [Size, fullscreen and focus](#size-fullscreen-and-focus)).
106
+ Everything — content, buttons, behavior — travels in the message you pass to
107
+ `confirm()`; the width every confirm defaults to is the plugin's
108
+ `confirm.defaultSize` (see [Size, fullscreen and focus](#size-fullscreen-and-focus)).
112
109
  At least one of `title`, `text`, `portal` should be set (dev warning
113
110
  otherwise).
114
111
 
@@ -402,7 +399,7 @@ the promise `false`, so your `if (ok)` branch is never reached accidentally.
402
399
  decline-only behavior in `no.onClick`, not in the `if (!ok)` branch. The rule
403
400
  that decides whether `no.onClick` runs: **user-initiated** dismissals
404
401
  (backdrop, `Escape`, the X) honor `dismissAsNo`; **system-initiated** ones
405
- (`duration` timeout, owner unmount, a superseding `confirm()`, `dismissAll()`)
402
+ (`duration` timeout, owner unmount, `dismissAll()`)
406
403
  always resolve quietly, never running any callback. The knobs:
407
404
 
408
405
  - `dismissAsNo` (default `true`) — user-initiated implicit dismissals also run
@@ -435,7 +432,7 @@ the peek closes. For app-lifetime confirms that survive any unmount, call
435
432
  `useConfirm()` outside a component scope (app setup, a store, module level).
436
433
 
437
434
  `useConfirm()` also returns `dismissAll()` — an imperative escape hatch that
438
- quietly dismisses the open dialog regardless of owner, for
435
+ quietly dismisses every open confirm regardless of owner, for
439
436
  app-level events like logout or a global error boundary.
440
437
 
441
438
  The classic case for `persistent` is the unsaved-changes prompt: an accidental
@@ -511,6 +508,8 @@ By default the dialog closes as soon as the promise settles. Set
511
508
  - `close()` only hides the dialog; **it never resolves anything**. Calling it
512
509
  on a dialog whose buttons haven't run yet leaves that `confirm()` promise
513
510
  pending forever — it is a flow terminator, not a substitute for the buttons.
511
+ - `close()` hides the newest open confirm opened through the **same**
512
+ `useConfirm()`, never another component's.
514
513
 
515
514
  **Keep the dialog open across steps**
516
515
 
@@ -585,6 +584,31 @@ const exportWorkspace = async () => {
585
584
  </script>
586
585
  ```
587
586
 
587
+ ### Several confirms at once
588
+
589
+ Confirms stack. A `confirm()` while another is open (from a button's
590
+ `onClick`, or two calls without an `await` between them) covers the open one
591
+ in place; the one beneath stays pending, and comes back, still waiting for
592
+ its answer, when the newer one closes. Escape and the backdrop answer only
593
+ the one on top. Two un-awaited calls are therefore both asked, newest first:
594
+ `await` each one when the order matters.
595
+
596
+ `stack` picks the `BbDialog` stack a confirm joins (every confirm joins the
597
+ confirms' own by default). Name a dialog's `stack` and the confirm replaces
598
+ that dialog's view while it is open, as a drill-in step would ([BbDialog](./BbDialog.md)
599
+ § Stacked dialogs); a name no other dialog uses shows it on top of the other
600
+ confirms, each dimming the one beneath.
601
+
602
+ ```ts
603
+ // An unsaved-changes question that takes the editor's place, not a layer on it.
604
+ const discard = await confirm({
605
+ title: 'Discard your changes?',
606
+ stack: 'editor', // the editor's <BbDialog stack="editor">
607
+ variant: 'destructive',
608
+ yes: 'Discard',
609
+ });
610
+ ```
611
+
588
612
  ### Rich content with portals
589
613
 
590
614
  When plain `text` isn't enough, render custom content in the dialog body via a
@@ -740,9 +764,9 @@ The map is typed where you write it by `definePt`, never with an imported
740
764
 
741
765
  - **Don't hand-build a yes/no dialog** out of `BbDialog` + buttons — busy
742
766
  states, dismissal semantics, ownership and focus handling already live here.
743
- - **Don't fire two `confirm()` calls without awaiting the first.** The second
744
- supersedes the first, which settles `false` — so a "delete these two things"
745
- flow written as two un-awaited confirms deletes nothing and asks once.
767
+ - **Two `confirm()` calls without an `await` between them ask both, newest
768
+ first** — the second covers the first. `await` each one when the questions
769
+ must come in order.
746
770
  - **Don't forget the `await`** — `confirm(...)` without it never gates
747
771
  anything.
748
772
  - **Don't call `close()` to "cancel"** a dialog whose buttons haven't run —
@@ -753,8 +777,8 @@ The map is typed where you write it by `definePt`, never with an imported
753
777
  - With `returning: true`, an implicit dismiss under the default
754
778
  `dismissAsNo: true` resolves with `no.onClick`'s return value — give No a
755
779
  callback that returns a sensible "declined" value.
756
- System dismissals (`duration`, `dismissAll()`, a superseding `confirm()`,
757
- owner unmount) still resolve `false`, so handle `false` too.
780
+ System dismissals (`duration`, `dismissAll()`, owner unmount) still resolve
781
+ `false`, so handle `false` too.
758
782
  - `confirm()` rejects keys `BbConfirmMessage` doesn't declare, also on a
759
783
  message built in a variable first: build it from `BbConfirmMessage` fields
760
784
  only (spread a wider object and the extra keys fail to type-check).
package/dist/ai/BbForm.md CHANGED
@@ -1076,7 +1076,11 @@ const bodyRules = computed(() => (publishing.value ? 'required' : ''));
1076
1076
  ```
1077
1077
 
1078
1078
  Both switch live: the next validation uses the new schema or rules, and
1079
- switching back lets a submit through that was blocked a moment ago.
1079
+ switching back lets a submit through that was blocked a moment ago. A message
1080
+ already on screen is not taken down by the switch: it stays until the field
1081
+ validates again (an edit, a blur, the next submit). If the condition lives in
1082
+ another field and the stale message would mislead, show it through
1083
+ [`field-errors`](#a-message-of-your-own-that-comes-and-goes) instead.
1080
1084
 
1081
1085
  ### While it saves
1082
1086
 
@@ -1353,6 +1357,54 @@ async function save(values: Record<string, unknown>) {
1353
1357
  }
1354
1358
  ```
1355
1359
 
1360
+ ##### A message of your own that comes and goes
1361
+
1362
+ The object does not have to come from a server. A check that spans fields is
1363
+ a `computed`: its message shows exactly while the check fails, with no code to
1364
+ clear it. Gate it on a submit attempt so it does not greet an empty form:
1365
+
1366
+ ```vue
1367
+ <script setup lang="ts">
1368
+ import { computed, ref } from 'vue';
1369
+ import { BbButton, BbDatePickerInput, BbForm, BbSelect } from 'bitboss-ui';
1370
+
1371
+ const patients = [
1372
+ { label: 'Anna Rossi', value: 1 },
1373
+ { label: 'Luca Bianchi', value: 2 },
1374
+ ];
1375
+ const visit = ref<{ patient: number | null; date: string | null }>({
1376
+ patient: null,
1377
+ date: null,
1378
+ });
1379
+ const tried = ref(false);
1380
+ const missingDate = computed(() => !visit.value.patient && !visit.value.date);
1381
+
1382
+ const ownErrors = computed(() => ({
1383
+ date:
1384
+ tried.value && missingDate.value
1385
+ ? 'Pick a date when no patient is selected.'
1386
+ : undefined,
1387
+ }));
1388
+
1389
+ const save = (values: Record<string, unknown>) => {
1390
+ tried.value = true;
1391
+ if (missingDate.value) return; // field-errors never block a submit
1392
+ console.log('book', values);
1393
+ };
1394
+ </script>
1395
+
1396
+ <template>
1397
+ <BbForm :model="visit" :field-errors="ownErrors" @submit="save">
1398
+ <BbSelect label="Patient" name="patient" :items="patients" clearable />
1399
+ <BbDatePickerInput label="Date" name="date" />
1400
+ <BbButton type="submit">Book</BbButton>
1401
+ </BbForm>
1402
+ </template>
1403
+ ```
1404
+
1405
+ Picking a patient takes the message down at once. When the page also holds
1406
+ server errors, pass both: `:field-errors="{ ...form.errors, ...ownErrors }"`.
1407
+
1356
1408
  - **The page owns them.** Editing the field does not hide its message; the
1357
1409
  next object does (Inertia replaces `form.errors` on every answer and clears
1358
1410
  it on success). Want it gone as the user types? Clear it yourself
@@ -1449,9 +1501,19 @@ const save = async (values: Record<string, unknown>) => {
1449
1501
  - A request-level message (Laravel's `message`, a 403) has no path: give it a
1450
1502
  key no field uses IN THE SAME CALL, `form.setErrors({ ...body.errors, _request: [body.message] })`, and it
1451
1503
  shows in the same summary.
1452
- - **Each `setErrors` call replaces the last one's errors** (it is one server
1453
- answer). Two calls in a row keep only the second: merge everything into one
1454
- map first.
1504
+ - **`setErrors` hands over one server answer; it never takes a message
1505
+ back.** A field's message stays until the user edits the field or it is
1506
+ validated again. A submit validates every field, so a submit → 422 →
1507
+ `setErrors` round always starts clean. A second call without a submit
1508
+ between them replaces `unmatchedErrors` only: the first call's field
1509
+ messages stay, and `null` or a missing key clears nothing. So put
1510
+ everything one answer says into one map.
1511
+ - **Don't use `setErrors` for a message you will take back yourself**, like
1512
+ your own cross-field check ("a date is required when no patient is
1513
+ selected"). Compute it into [`field-errors`](#a-message-of-your-own-that-comes-and-goes)
1514
+ instead: it shows while its key holds a message and goes the moment it
1515
+ does not. Calling `validate()` to wipe a `setErrors` message runs every
1516
+ rule on every field, so it puts messages on fields the user never touched.
1455
1517
  - `setErrors` does not emit `invalid`. It does move focus to the first failing
1456
1518
  field on screen, like a failed submit (`{ focus: false }` opts out; see
1457
1519
  [A failed submit focuses the first invalid field](#a-failed-submit-focuses-the-first-invalid-field)).
@@ -1617,7 +1679,8 @@ The `id` is also the `<form>`'s own (it follows a BbForm; a plain `<form>`
1617
1679
  submits but has no save state to follow), so a `<BbButton form="id" type="submit">`
1618
1680
  outside it submits it too, through the same validation, and spins while the
1619
1681
  save runs exactly like a submit button inside (`disable-auto-loading` opts
1620
- out): no `:loading` to bind.
1682
+ out): no `:loading` to bind. Server errors (`setErrors`, `field-errors`) then
1683
+ focus the first failing field, as they do after a submit from inside.
1621
1684
 
1622
1685
  ```vue
1623
1686
  <script setup lang="ts">
package/dist/ai/BbHost.md CHANGED
@@ -87,12 +87,13 @@ The same keys go in the build plugin (`bitbossUi({ confirm, toastPosition })`).
87
87
 
88
88
  ### Areas with their own look
89
89
 
90
- The host paints confirms and toasts where it is mounted, so they take the
91
- passthrough scope and tokens of the place the HOST sits. When the app is split
92
- into areas that each wrap their pages in `<BbPtScope>`
93
- ([passthrough guide](./guides/passthrough.md) § Named scopes), mount one
94
- `<BbHost />` inside each area's layout, within its scope, and none at the app
95
- root. That holds only if **every** page renders inside exactly one of those
90
+ A confirm takes the passthrough scope and tokens of the component that called
91
+ `useConfirm()`, wherever the host sits: nothing to set up. Toasts take the
92
+ scope and tokens of the place the HOST sits. When the app is split into areas
93
+ that each wrap their pages in `<BbPtScope>`
94
+ ([passthrough guide](./guides/passthrough.md) § Named scopes) and toasts need
95
+ each area's look, mount one `<BbHost />` inside each area's layout, within its
96
+ scope, and none at the app root. That holds only if **every** page renders inside exactly one of those
96
97
  layouts, so there is still one host at a time. See
97
98
  [Agent Contract § Global Host Rule](./guides/agent-contract.md).
98
99
 
@@ -142,14 +143,11 @@ without a mismatch.
142
143
  - Listed nodes are the ones `BbHost`'s API reaches or that declare a documented CSS variable; the rest fold into "also mounts". The complete tree, repeats and undocumented locals included, is `manifest/components/BbHost.tree.json`.
143
144
 
144
145
  - `BbHost` _(this component)_
145
- - `BbConfirm` _(internal — not importable, reach it through `BbConfirm`)_ — its own pt parts (`BbConfirm`): `text` → `div.bb-confirm__text`, `action` → `BbButton`; hands `BbDialog` the pt map `passthrough.forward(DIALOG_PARTS, BB_DIALOG_PT.states)` (ours → theirs)
146
- - `BbDialog` _(public — [contract](./BbDialog.md))_ — its own pt parts (`BbDialog`): `header` → `div.bb-dialog__header`, `title` → `span.bb-dialog__title`, `description` → `p.bb-dialog__description`, `close` → `CloseButton`, `content` → `div.bb-dialog__content`, `footer` → `div.bb-dialog__footer`; hands `BbOffCanvas` the pt map `{ root: 'root', header: 'header', title: 'title', description: 'description', content: 'content', footer: 'footer', close: 'close', sheet: 'root', }` (ours → theirs); documented CSS variables: `--px`, `--close-size`
147
- - `CloseButton` _(internal — not importable, reach it through `BbDialog`)_ — documented CSS variables: `--size`, `--p`
148
- - `BbOffCanvas` _(public — [contract](./BbOffCanvas.md))_ — its own pt parts (`BbOffCanvas`): `header` → `div.bb-offcanvas__header`, `title` → `span.bb-offcanvas__title`, `description` → `p.bb-offcanvas__description`, `close` → `CloseButton`, `content` → `div.bb-offcanvas__content`, `footer` → `div.bb-offcanvas__footer`; documented CSS variables: `--stack-x`, `--snap-y`, `--stack-fill`, `--stack-transition-duration`, `--px`, `--handle-w`, `--handle-h`, `--handle-bg`, `--safe-top`, `--close-size`; also mounts `CloseButton`
149
- - `BbButton` _(public — [contract](./BbButton.md))_ — its own pt parts (`BbButton`): `root` → `BbBaseButton`, `spinner` → `BbSpinner`, `icon` → `BbIcon`, `text` → `span.bb-button__text`; documented CSS variables: `--h-xs`, `--h`, `--icon-size`, `--px`, `--fs`, `--r`, `--gap`, `--bg`, `--bg-hover`, `--bg-pressed`, `--fg`, `--border-color`, `--ring-color`, `--ring-size`, `--bw`, `--fg-hover`, `--border-hover`, `--fg-pressed`, `--border-pressed`, `--bg-disabled`, `--fg-disabled`, `--border-disabled`, `--opacity-disabled`, `--elev-tier`, `--elev`; also mounts `BbIcon`, `BbSpinner`
150
- - `BbBaseButton` _(public — [contract](./BbBaseButton.md))_ — its own pt parts (`BbBaseButton`): `root` → `RouterComponent`, `root` → `a`, `root` → `component`; also mounts `RouterComponent`
151
146
  - `BbToast` _(internal — not importable, reach it through `BbToast`)_ — documented CSS variables: `--gap`, `--offset`, `--width`, `--lift`, `--stack-y`, `--stack-scale`, `--enter-y`, `--swipe-amount`, `--z`, `--r`, `--ring-size`, `--bg`, `--border-color`, `--fg`, `--icon-color`
152
- - `BbToastRegion` _(internal — not importable, reach it through `BbToast`)_ — its own pt parts (`BbToast`): `root` → `ol.bb-toast`; also mounts `BbToastMessage`
147
+ - `BbToastRegion` _(internal — not importable, reach it through `BbToast`)_ — its own pt parts (`BbToast`): `root` → `ol.bb-toast`
148
+ - `BbToastMessage` _(internal — not importable, reach it through `BbToast`)_ — also mounts `BbIcon`, `BbSpinner`
149
+ - `BbButton` _(public — [contract](./BbButton.md))_ — its own pt parts (`BbButton`): `root` → `BbBaseButton`, `spinner` → `BbSpinner`, `icon` → `BbIcon`, `text` → `span.bb-button__text`; documented CSS variables: `--h-xs`, `--h`, `--icon-size`, `--px`, `--fs`, `--r`, `--gap`, `--bg`, `--bg-hover`, `--bg-pressed`, `--fg`, `--border-color`, `--ring-color`, `--ring-size`, `--bw`, `--fg-hover`, `--border-hover`, `--fg-pressed`, `--border-pressed`, `--bg-disabled`, `--fg-disabled`, `--border-disabled`, `--opacity-disabled`, `--elev-tier`, `--elev`; also mounts `BbIcon`, `BbSpinner`
150
+ - `BbBaseButton` _(public — [contract](./BbBaseButton.md))_ — its own pt parts (`BbBaseButton`): `root` → `RouterComponent`, `root` → `a`, `root` → `component`; also mounts `RouterComponent`
153
151
 
154
152
  ## See Also
155
153
 
@@ -96,9 +96,10 @@ The scope follows the **component tree**, not the DOM:
96
96
  in. A directive written directly in `<BbPtScope>`'s slot, in the same
97
97
  template as the scope, sees the scope above it instead — keep the scope in a
98
98
  layout or a parent component.
99
- - `<BbHost />` renders every toast and confirm where it is mounted. For toasts
100
- and confirms in the area's look, mount one `<BbHost />` inside
101
- each area's layout, within its scope, and none at the app root — valid only
99
+ - A `confirm()` takes the scope of the component that called `useConfirm()`,
100
+ wherever `<BbHost />` is mounted. Toasts take the scope where the host is
101
+ mounted: for toasts in the area's look, mount one `<BbHost />` inside each
102
+ area's layout, within its scope, and none at the app root — valid only
102
103
  when every page renders inside exactly one of those layouts (see
103
104
  [Agent Contract § Global Host Rule](./guides/agent-contract.md)).
104
105
 
@@ -112,7 +113,8 @@ The scope follows the **component tree**, not the DOM:
112
113
 
113
114
  ### Works well with
114
115
 
115
- - `BbHost` — mount one inside each scoped area layout.
116
+ - `BbHost` — mount one inside each scoped area layout when toasts need the
117
+ area's look.
116
118
  - The [passthrough guide](./guides/passthrough.md) — grammar, merging and the
117
119
  full parts table.
118
120