bitboss-ui 3.0.0-beta.62 → 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 (46) hide show
  1. package/dist/ai/BbConfirm.md +46 -22
  2. package/dist/ai/BbForm.md +66 -4
  3. package/dist/ai/BbHost.md +11 -13
  4. package/dist/ai/BbPtScope.md +6 -4
  5. package/dist/ai/BbTable.md +65 -5
  6. package/dist/ai/changelog.json +28 -4
  7. package/dist/ai/components.json +5 -5
  8. package/dist/ai/composables/useConfirm.md +8 -5
  9. package/dist/ai/guides/agent-contract.md +3 -2
  10. package/dist/ai/guides/migration/components/bb-host.md +4 -2
  11. package/dist/ai/guides/migration/components/use-confirm.md +15 -15
  12. package/dist/ai/guides/migration/v2-to-v3.md +9 -0
  13. package/dist/ai/guides/passthrough.md +57 -57
  14. package/dist/ai/guides/validated-forms.md +5 -0
  15. package/dist/ai/helpers/inertia/manifest.json +1 -1
  16. package/dist/ai/manifest/components/BbHost.tree.json +1 -2
  17. package/dist/ai/manifest/components/BbTable.json +2 -0
  18. package/dist/ai/manifest/index.json +2 -2
  19. package/dist/ai/manifest/types.advanced.json +1 -1
  20. package/dist/ai/source/BbConfirm.md +33 -21
  21. package/dist/ai/source/BbConfirmPortal.md +7 -16
  22. package/dist/ai/source/BbHost.md +3 -3
  23. package/dist/ai/source/BbTable.md +37 -9
  24. package/dist/components/BbConfirm/BbConfirm.vue_vue_type_script_setup_true_lang.js +83 -74
  25. package/dist/components/BbConfirm/BbConfirmHost.d.ts +10 -0
  26. package/dist/components/BbConfirm/BbConfirmHost.js +32 -0
  27. package/dist/components/BbConfirm/BbConfirmPortal.vue_vue_type_script_setup_true_lang.js +8 -12
  28. package/dist/components/BbConfirm/confirmEntry.d.ts +4 -0
  29. package/dist/components/BbConfirm/confirmEntry.js +4 -0
  30. package/dist/components/BbConfirm/types.d.ts +2 -2
  31. package/dist/components/BbHost/BbHost.vue_vue_type_script_setup_true_lang.js +2 -2
  32. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +426 -380
  33. package/dist/components/BbTable/BbTableDataRow.js +17 -12
  34. package/dist/components/BbTable/BbTableSelectToggle.d.ts +1 -1
  35. package/dist/components/BbTable/types.d.ts +14 -3
  36. package/dist/components/BbTable/types.js +2 -0
  37. package/dist/composables/useBbFormContext.d.ts +7 -3
  38. package/dist/composables/useConfirm.d.ts +327 -33
  39. package/dist/composables/useConfirm.js +93 -71
  40. package/dist/llms-full.txt +295 -129
  41. package/dist/llms-medium.txt +3 -2
  42. package/dist/styles.css +1 -1
  43. package/dist/types/passthrough.d.ts +1 -1
  44. package/dist/types/ptComponentMap.d.ts +1 -1
  45. package/dist/utils/versionCheck.js +1 -1
  46. package/package.json +1 -1
@@ -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)).
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
 
@@ -78,7 +78,9 @@ needs is declared here, not in markup:
78
78
  even when the value is nullish, so a virtual column whose `key` matches no
79
79
  field can still compute from `item`; set `formatOnNull: false` when a
80
80
  formatter must only see real values.
81
- - **`placeholder`** fills the cell when the pipeline still ends nullish.
81
+ - **`placeholder`** fills the cell when the pipeline still ends nullish. Its
82
+ cell carries `--placeholder`, a cell with a value `--value`: style a blank
83
+ through those, not a slot ([Empty cells](#empty-cells-value-and-placeholder)).
82
84
  - **`align`** (`'left' | 'center' | 'right'`) per column; the table-level
83
85
  `align` prop sets the default for the whole table (`left`).
84
86
  - **`width`** freezes the column: numbers and bare numeric strings are px
@@ -102,7 +104,8 @@ needs is declared here, not in markup:
102
104
  takes them, once, when the page hydrates.) The floors read every row up
103
105
  to 2,000; past that, the first 1,000 and a sample across the rest (a
104
106
  `virtual` table has no floors at all). No floor on `fixed` or
105
- `virtual` tables, slot columns, or under a custom `#thead`. A grid keyword or function is the track as written:
107
+ `virtual` tables, slot columns, or under a custom `#thead`, so never slot a
108
+ column only to restyle its blank: use [`--placeholder`](#empty-cells-value-and-placeholder). A grid keyword or function is the track as written:
106
109
  `'auto'`, `'2fr'`, `'minmax(0, 1fr)'` (a share that may shrink below its
107
110
  content), `'fit-content(200px)'`. When _every_ column declares a length, the
108
111
  lengths become floors and the surplus is shared in proportion to their
@@ -3529,6 +3532,60 @@ root (`--cue-w: 0px` turns it off):
3529
3532
  }
3530
3533
  ```
3531
3534
 
3535
+ #### Empty cells: `--value` and `--placeholder`
3536
+
3537
+ Every data cell says what the column's pipeline (`formatter`, then
3538
+ `placeholder`) produced: **`bb-table__cell--value`** when it shows a value,
3539
+ **`bb-table__cell--placeholder`** when the value was empty and the
3540
+ column's `placeholder` shows instead. The same two are `pt` states on
3541
+ `cell`. The class names the outcome, not the text, so a placeholder can read
3542
+ "–", "N/D" or anything else and the styling holds. A cell whose value is empty
3543
+ with no `placeholder` has neither. Slot cells carry it too (the class follows
3544
+ the `content` the slot is handed); the select, drag and actions cells never
3545
+ do.
3546
+
3547
+ ```vue
3548
+ <script setup lang="ts">
3549
+ import { BbTable, type BbTableColumn } from 'bitboss-ui';
3550
+
3551
+ type Person = { id: number; name: string; email: string | null; city: string };
3552
+
3553
+ const blank = (value: unknown) =>
3554
+ typeof value === 'string' && value.trim() === '' ? null : value;
3555
+
3556
+ const columns: Array<BbTableColumn<Person>> = [
3557
+ { key: 'name', label: 'Nome' },
3558
+ { key: 'email', label: 'Email', placeholder: 'N/D' },
3559
+ { key: 'city', label: 'Provincia', placeholder: '–', formatter: blank },
3560
+ ];
3561
+
3562
+ const people: Person[] = [
3563
+ { id: 1, name: 'Giulia Bianchi', email: null, city: 'Milano' },
3564
+ { id: 2, name: 'Marco Rossi', email: 'marco@example.com', city: ' ' },
3565
+ ];
3566
+ </script>
3567
+
3568
+ <template>
3569
+ <BbTable
3570
+ :columns="columns"
3571
+ item-value="id"
3572
+ :items="people"
3573
+ pt:cell:placeholder="text-(--bb-text-muted)"
3574
+ />
3575
+ </template>
3576
+ ```
3577
+
3578
+ Or in CSS, once for the whole app:
3579
+ `.bb-table__cell--placeholder { color: var(--bb-text-muted); }`.
3580
+
3581
+ - **The placeholder shows only for `null` / `undefined`.** An empty or
3582
+ whitespace-only string is a value. To treat it as blank, return `null` from
3583
+ the `formatter` (`blank` above), and the placeholder takes over.
3584
+ - **Don't slot a column just to restyle its blank.** A slot column gets no
3585
+ width floor (see Columns), so a wrapper that slots every column to print a
3586
+ muted dash costs every column its floor. `placeholder` plus the class does
3587
+ the same with none of that.
3588
+
3532
3589
  #### The table as a card
3533
3590
 
3534
3591
  The root paints no background, border or radius of its own — it is the
@@ -3578,7 +3635,7 @@ through element names, which match nothing:
3578
3635
  | header cell | `.bb-table__cell-column-header` (+ `--select`, `--actions`) | `columnheader` |
3579
3636
  | body group | `.bb-table__body` | `rowgroup` |
3580
3637
  | data row | `.bb-table__row` (+ `--highlighted`) | `row` |
3581
- | data cell | `.bb-table__cell` (+ `--select`) | `cell` (`gridcell` with `keyboard-navigation`) |
3638
+ | data cell | `.bb-table__cell` (+ `--select`; `--value` / `--placeholder`: what the pipeline produced) | `cell` (`gridcell` with `keyboard-navigation`) |
3582
3639
  | actions cell | `.bb-table__cell--actions` | `cell` (`gridcell` with `keyboard-navigation`) |
3583
3640
  | expand row | `.bb-table-expand__row` > `.bb-table-expand__cell` | `row` > `cell` (`gridcell`) |
3584
3641
  | empty state | `.bb-table-empty__row` > `.bb-table-empty__cell` | `row` > `cell` |
@@ -3721,7 +3778,8 @@ three glyphs: `sort-icon`, `select-icon` (each row's checkbox or radio and
3721
3778
  the select-all) and `drag-icon` (on the chip a column drag carries). The grid
3722
3779
  is `div`s with ARIA roles, so these are parts, not `th` / `td` selectors.
3723
3780
 
3724
- States: `loading` and `empty` (the table); `selected`, `highlighted`,
3781
+ States: `loading` and `empty` (the table); `value` / `placeholder` per data
3782
+ cell (what its pipeline produced, see [Empty cells](#empty-cells-value-and-placeholder)); `selected`, `highlighted`,
3725
3783
  `expanded` and `disabled` resolved per row — a cell takes its row's states,
3726
3784
  a row's `select-icon` its `selected` / `disabled`, and its `expand` its
3727
3785
  `selected` / `highlighted` / `expanded`; `sorted` /
@@ -4176,7 +4234,7 @@ for a preset that fits any row type (never `<any>`), not an imported
4176
4234
 
4177
4235
  | Prop | Type | Default | Required | Description |
4178
4236
  | --- | --- | --- | --- | --- |
4179
- | `pt:<part>`, `pt:<part>:<state>`, `pt` | `PtValue` | | | Passthrough — a class list, or `{ class, style, attrs }`, bound to one named part of the component; the state form applies only while that state is on. Parts: `body`, `caption`, `cell`, `cellColumnHeader`, `dragHandle`, `dragIcon`, `expand`, `firstRow`, `foot`, `head`, `headerRow`, `lastRow`, `root`, `row`, `selectIcon`, `sortIcon`, `table`. States: `disabled`, `empty`, `expanded`, `highlighted`, `indeterminate`, `loading`, `selected`, `sorted`, `sortedDesc`. What each one is: _Passthrough parts and states_ below. `pt` is the object form with the same keys minus the prefix (`{ icon: '…', 'icon:loading': '…' }`). See `guides/passthrough.md`. In `<script>` a `pt` value is typed `BbTableProps<Row>['pt']` (`BbTableProps['pt']` for a preset that fits any row type, never `<any>`) (`PtValue` is not exported): § Typing `pt` in `<script>`. |
4237
+ | `pt:<part>`, `pt:<part>:<state>`, `pt` | `PtValue` | | | Passthrough — a class list, or `{ class, style, attrs }`, bound to one named part of the component; the state form applies only while that state is on. Parts: `body`, `caption`, `cell`, `cellColumnHeader`, `dragHandle`, `dragIcon`, `expand`, `firstRow`, `foot`, `head`, `headerRow`, `lastRow`, `root`, `row`, `selectIcon`, `sortIcon`, `table`. States: `disabled`, `empty`, `expanded`, `highlighted`, `indeterminate`, `loading`, `placeholder`, `selected`, `sorted`, `sortedDesc`, `value`. What each one is: _Passthrough parts and states_ below. `pt` is the object form with the same keys minus the prefix (`{ icon: '…', 'icon:loading': '…' }`). See `guides/passthrough.md`. In `<script>` a `pt` value is typed `BbTableProps<Row>['pt']` (`BbTableProps['pt']` for a preset that fits any row type, never `<any>`) (`PtValue` is not exported): § Typing `pt` in `<script>`. |
4180
4238
  | `accessibleLabel` | `((columns: MappedCell[], item: any) => string) \| undefined` | | | The label of each row's selection checkbox, from `(columns, item)`. Unset: the row's name in the locale's select template (its `rowHeader` cells, else its first column: "Select INV-1041"). The cells arrive in render order: look one up by `key`, never by position. |
4181
4239
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
4182
4240
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
@@ -4263,6 +4321,8 @@ States are listed in precedence order: when two are on at once and their entries
4263
4321
 
4264
4322
  | State | When it is on |
4265
4323
  | --- | --- |
4324
+ | `value` | Per node, on a DATA `cell`: the formatter / placeholder pipeline produced a value (not `null` / `undefined`), slot cells included (`bb-table__cell--value`). Not on the select, drag or actions cells. |
4325
+ | `placeholder` | Per node, on a DATA `cell`: the value was empty and the column's `placeholder` shows instead, whatever its text (`bb-table__cell--placeholder`). A cell with an empty value and no `placeholder` has neither state. |
4266
4326
  | `empty` | Component-wide: there are no rows (`bb-table--empty`). |
4267
4327
  | `sorted` | Per node, on `cellColumnHeader` and `sortIcon`: the column is sorted, either direction (`bb-table__cell-column-header--sorted`). |
4268
4328
  | `sortedDesc` | Per node, on `cellColumnHeader` and `sortIcon`: sorted descending (`bb-table__cell-column-header--sorted-desc`). Ascending is `sorted` alone. |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 3,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.62",
4
+ "version": "3.0.0-beta.63",
5
5
  "upgrade": "v2-to-v3",
6
6
  "guide": "ai/guides/migration/v2-to-v3.md",
7
7
  "legend": {
@@ -72,7 +72,8 @@
72
72
  "3.0.0-beta.59",
73
73
  "3.0.0-beta.60",
74
74
  "3.0.0-beta.61",
75
- "3.0.0-beta.62"
75
+ "3.0.0-beta.62",
76
+ "3.0.0-beta.63"
76
77
  ],
77
78
  "unpublished": [
78
79
  "3.0.0-alpha.2",
@@ -95,7 +96,7 @@
95
96
  ],
96
97
  "summary": {
97
98
  "renames": 220,
98
- "behaviourBreaks": 230,
99
+ "behaviourBreaks": 231,
99
100
  "bySurface": {
100
101
  "prop": 15,
101
102
  "option": 1,
@@ -106,7 +107,7 @@
106
107
  "class": 138
107
108
  },
108
109
  "unreleased": 0,
109
- "added": 414
110
+ "added": 416
110
111
  },
111
112
  "added": {
112
113
  "3.0.0-alpha.1": [
@@ -2085,6 +2086,16 @@
2085
2086
  },
2086
2087
  "table · keep short multi-word values on one line",
2087
2088
  "table · sortable headers describe shift+enter"
2089
+ ],
2090
+ "3.0.0-beta.63": [
2091
+ "table · cells name what the pipeline produced, value or placeholder",
2092
+ {
2093
+ "text": "confirm · every confirm() call opens its own dialog",
2094
+ "components": [
2095
+ "BbHost",
2096
+ "BbPtScope"
2097
+ ]
2098
+ }
2088
2099
  ]
2089
2100
  },
2090
2101
  "breaking": [
@@ -8487,6 +8498,19 @@
8487
8498
  "ruling": "owner 2026-10-16 (Q75.3) + 2026-10-21 (Q76.9 e)",
8488
8499
  "guide": "v2-to-v3.md § 6 item 96 + BbTable.md § Columns, § Column resize",
8489
8500
  "firstReleasedIn": "3.0.0-beta.62"
8501
+ },
8502
+ {
8503
+ "id": "f370",
8504
+ "kind": "behaviour",
8505
+ "summary": "confirm(): every call opens its own dialog; a new one covers the open one instead of resolving it false; it takes the caller's pt scope; new stack option",
8506
+ "components": [
8507
+ "BbConfirm",
8508
+ "BbHost"
8509
+ ],
8510
+ "description": "confirm(): every call opens its own dialog, under the caller's provides. A call while another confirm is open COVERS it (a shared confirm BbDialog stack) instead of resolving it false: the first stays pending and comes back when the newer one closes, so two un-awaited calls ask both, newest first; Escape and the backdrop answer only the top one. close() hides the newest open confirm opened through the same useConfirm() (it used to hide THE confirm); dismissAll() dismisses every open one. The dialog takes the <BbPtScope> and tokens of the component that called useConfirm() (a store's: the host's), so areas need no host of their own for confirms; a component portal injects from the caller's tree. New message option stack (join a dialog's stack: the confirm replaces its view)",
8511
+ "ruling": "owner 2026-10-22 (Q75.9)",
8512
+ "guide": "v2-to-v3.md § 6 item 97 + components/use-confirm.md + components/bb-host.md + BbConfirm.md § Several confirms at once + useConfirm.md",
8513
+ "firstReleasedIn": "3.0.0-beta.63"
8490
8514
  }
8491
8515
  ]
8492
8516
  }