bitboss-ui 3.0.0-beta.10 → 3.0.0-beta.14

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 (53) hide show
  1. package/dist/ai/BbBaseSelect.md +1 -0
  2. package/dist/ai/BbSelect.md +87 -4
  3. package/dist/ai/BbSelectPopover.md +9 -17
  4. package/dist/ai/BbTable.md +113 -6
  5. package/dist/ai/changelog.json +1 -1
  6. package/dist/ai/components.json +18 -2
  7. package/dist/ai/source/BbBadge.md +52 -13
  8. package/dist/ai/source/BbBaseSelect.md +71 -16
  9. package/dist/ai/source/BbButton.md +39 -23
  10. package/dist/ai/source/BbDropdown.md +5 -1
  11. package/dist/ai/source/BbIcon.md +43 -34
  12. package/dist/ai/source/BbSelect.md +28 -0
  13. package/dist/ai/source/BbSelectPopover.md +23 -3
  14. package/dist/ai/source/BbTable.md +1962 -334
  15. package/dist/ai/source/CommonPopover.md +6 -0
  16. package/dist/ai/source/FlatListBox.md +11 -5
  17. package/dist/ai/source/GroupedListBox.md +7 -5
  18. package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +22 -16
  19. package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +33 -12
  20. package/dist/components/BbBadge/badgeButtonContext.d.ts +21 -8
  21. package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +74 -65
  22. package/dist/components/BbBaseSelect/types.d.ts +22 -0
  23. package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +84 -82
  24. package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +1 -1
  25. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +40 -40
  26. package/dist/components/BbIcon/iconCache.d.ts +33 -0
  27. package/dist/components/BbIcon/iconCache.js +17 -0
  28. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  29. package/dist/components/BbSelect/types.d.ts +24 -0
  30. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +982 -672
  31. package/dist/components/BbTable/BbTableDataRow.d.ts +162 -0
  32. package/dist/components/BbTable/BbTableDataRow.js +136 -0
  33. package/dist/components/BbTable/BbTableExpandRow.d.ts +73 -0
  34. package/dist/components/BbTable/BbTableExpandRow.js +52 -0
  35. package/dist/components/BbTable/types.d.ts +26 -0
  36. package/dist/components/BbTable/utils.d.ts +17 -1
  37. package/dist/components/BbTable/utils.js +23 -18
  38. package/dist/components/CommonPopover/CommonPopover.vue_vue_type_script_setup_true_lang.js +2 -1
  39. package/dist/components/FlatListBox/FlatListBox.vue_vue_type_script_setup_true_lang.js +2 -2
  40. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +2 -2
  41. package/dist/composables/useBaseOptions.js +20 -10
  42. package/dist/directives/bbDropdown.d.ts +5 -0
  43. package/dist/directives/bbDropdown.js +32 -10
  44. package/dist/directives/createPopoverDirective.d.ts +62 -0
  45. package/dist/directives/createPopoverDirective.js +63 -23
  46. package/dist/llms-full.txt +210 -27
  47. package/dist/styles.css +1 -1
  48. package/dist/validated/BbSelect.vue.d.ts +2 -0
  49. package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
  50. package/package.json +1 -1
  51. package/dist/components/BbBadge/BadgeAppendIcon.vue.d.ts +0 -3
  52. package/dist/components/BbBadge/BadgeAppendIcon.vue.js +0 -5
  53. package/dist/components/BbBadge/BadgeAppendIcon.vue_vue_type_script_setup_true_lang.js +0 -22
@@ -76,6 +76,7 @@
76
76
 
77
77
  - `append` — scope: `BaseSelectActivatorSlotProps`
78
78
  - `append-outer` — scope: `BaseSelectOuterSlotProps`
79
+ - `chip` — scope: `BaseSelectChipSlotProps<Item>` — Replaces the default badge rendered for each selection in `multiple` mode.
79
80
  - `footer` — scope: `BaseSelectPanelSlotProps` — Content pinned to the bottom of the panel, below the options list.
80
81
  - `group` — scope: `BaseSelectGroupSlotProps<Item>`
81
82
  - `header` — scope: `BaseSelectPanelSlotProps` — Content pinned to the top of the panel, above the search field.
@@ -1068,10 +1068,12 @@ const role = ref<string | null>(null);
1068
1068
 
1069
1069
  ### Custom option rows
1070
1070
 
1071
- The `#option` slot replaces the default row rendering — use it for avatars,
1072
- secondary text or metadata. It exposes the raw `item`, the resolved `text` and
1073
- `value`, plus `selected` / `focused` / `disabled` / `loading` / `hasErrors`
1074
- flags and the row `index`. Rows are fixed-height (the list is virtualized): if
1071
+ The `#option` slot replaces the row's **text** — use it for avatars, secondary
1072
+ text or metadata. It exposes the raw `item`, the resolved `text` and `value`,
1073
+ plus `selected` / `focused` / `disabled` / `loading` / `hasErrors` flags and the
1074
+ row `index`. The selection check is not yours to redraw: it renders outside the
1075
+ slot, so a custom row keeps it (`selected` is there for rows that want to show
1076
+ the state some _other_ way, not for drawing a second check). Rows are fixed-height (the list is virtualized): if
1075
1077
  your markup is taller than the default 28px (24px compact; 44px / 36px inside
1076
1078
  the adaptive sheet), set `item-height` to match — and `header-height` for
1077
1079
  grouped headers (defaults 32px / 24px compact; 36px / 32px in the sheet). The
@@ -1148,6 +1150,86 @@ const assignee = ref<string | null>(null);
1148
1150
  </script>
1149
1151
  ```
1150
1152
 
1153
+ ### Custom chips
1154
+
1155
+ In `multiple` mode each selection renders as a badge. The `#chip` slot replaces
1156
+ that badge outright — a colored label, an avatar plus a name, anything — and
1157
+ receives `{ item, text, value, disabled, deselect }`. `item` is the raw entry
1158
+ from `items` (where a per-selection color or avatar lives), `disabled` is `true`
1159
+ while the control is `disabled` or `readonly` (render no remove affordance —
1160
+ `deselect` refuses anyway), and `deselect` drops just this value from the model.
1161
+
1162
+ Custom chips need none of the library's class names: the chips row supplies the
1163
+ spacing and keeps them from shrinking. The slot is not called in `comma` mode
1164
+ (there are no chips to replace) nor for the collapsed "+N" summary that appears
1165
+ once the chips outgrow the field — keep chips compact, or raise
1166
+ `max-selected-labels`.
1167
+
1168
+ **Labels as colored chips**
1169
+
1170
+ ```vue
1171
+ <template>
1172
+ <div class="grid max-w-sm gap-3 rounded-[var(--bb-radius)] border p-3">
1173
+ <BbSelect
1174
+ v-model="tags"
1175
+ clearable
1176
+ item-text="label"
1177
+ item-value="value"
1178
+ :items="tagItems"
1179
+ label="Labels"
1180
+ multiple
1181
+ placeholder="Add labels"
1182
+ >
1183
+ <template #option="{ item, text }">
1184
+ <span class="flex w-full items-center gap-2">
1185
+ <span
1186
+ class="size-2 shrink-0 rounded-full"
1187
+ :style="{ background: item.color }"
1188
+ ></span>
1189
+ <span class="min-w-0 flex-1 truncate">{{ text }}</span>
1190
+ </span>
1191
+ </template>
1192
+ <!-- The chip slot replaces the default badge for each selection. It
1193
+ hands over the raw `item` (here: the label's own color), the resolved
1194
+ `text` / `value`, a `disabled` flag that is true while the control is
1195
+ disabled or readonly, and `deselect` — which drops just this value. -->
1196
+ <template #chip="{ item, text, disabled, deselect }">
1197
+ <BbBadge
1198
+ :clearable="!disabled"
1199
+ size="lg"
1200
+ :style="{
1201
+ '--bg': `color-mix(in oklab, ${item.color} 15%, transparent)`,
1202
+ '--color': 'var(--bb-text)',
1203
+ }"
1204
+ @click:clear="deselect"
1205
+ >
1206
+ <span
1207
+ class="size-2 rounded-full"
1208
+ :style="{ background: item.color }"
1209
+ ></span>
1210
+ {{ text }}
1211
+ </BbBadge>
1212
+ </template>
1213
+ </BbSelect>
1214
+ </div>
1215
+ </template>
1216
+ <script setup lang="ts">
1217
+ import { ref } from 'vue';
1218
+ import { BbBadge, BbSelect } from 'bitboss-ui';
1219
+
1220
+ type Label = { label: string; value: string; color: string };
1221
+
1222
+ const tagItems: Label[] = [
1223
+ { label: 'Bug', value: 'bug', color: '#ef4444' },
1224
+ { label: 'Enhancement', value: 'enhancement', color: '#0ea5e9' },
1225
+ { label: 'Documentation', value: 'documentation', color: '#8b5cf6' },
1226
+ { label: 'Good first issue', value: 'good-first-issue', color: '#22c55e' },
1227
+ ];
1228
+
1229
+ const tags = ref<string[]>(['bug']);
1230
+ </script>
1231
+ ```
1232
+
1151
1233
  ### Slots around the input
1152
1234
 
1153
1235
  Beyond the panel slots, the field itself is extensible:
@@ -1424,6 +1506,7 @@ Platform notes:
1424
1506
 
1425
1507
  - `append` — scope: `{ query: string; focus: () => void; close: () => Promise<void>; }` — Content appended inside the input field, after the value area.
1426
1508
  - `append-outer` — scope: `{ query: string; close: () => Promise<void>; }` — Content appended outside the input field wrapper.
1509
+ - `chip` — scope: `BbSelectChipSlotProps<T>` — Replaces the default badge rendered for each selection in `multiple` mode. Not called in `comma` mode, nor for the collapsed "+N" summary.
1427
1510
  - `footer` — scope: `BbSelectPanelSlotProps` — Content pinned to the bottom of the options panel, below the options list. Rendered in both the desktop popover and the adaptive sheet.
1428
1511
  - `group` — scope: `BbSelectGroupSlotProps<T>` — Replaces the default group header rendered above grouped options.
1429
1512
  - `header` — scope: `BbSelectPanelSlotProps` — Content pinned to the top of the options panel, above the search field. Rendered in both the desktop popover and the adaptive sheet.
@@ -216,9 +216,11 @@ const reviewers = ref<string[]>(['ada', 'grace']);
216
216
 
217
217
  ### Custom option rows
218
218
 
219
- The `#option` slot replaces each row — exposing the raw `item`, resolved `text` /
220
- `value`, the row `index`, and `selected` / `focused` / `disabled` / `loading`
221
- flags — so options can carry avatars, badges or secondary metadata. Rows are
219
+ The `#option` slot replaces each row's **text** — exposing the raw `item`,
220
+ resolved `text` / `value`, the row `index`, and `selected` / `focused` /
221
+ `disabled` / `loading` flags — so options can carry avatars, badges or secondary
222
+ metadata. The selection check renders outside the slot and survives any custom
223
+ markup, so there is no need to redraw it from `selected`. Rows are
222
224
  fixed-height and virtualized: if your markup is taller than the default 28px
223
225
  (24px compact; 44px / 36px inside the adaptive sheet), set `item-height` to
224
226
  match — and `header-height` for grouped headers (32px / 24px compact; 36px /
@@ -258,16 +260,12 @@ height wins everywhere.
258
260
  }}</BbBadgeButton>
259
261
  </BbBadge>
260
262
  </template>
261
- <template #option="{ item, text, selected }">
263
+ <!-- The slot replaces the row's TEXT only: the selection check keeps
264
+ rendering outside it, so custom rows never have to redraw it. -->
265
+ <template #option="{ item, text }">
262
266
  <span class="flex w-full items-center gap-2">
263
267
  <BbAvatar :alt="text" size="32">{{ item.initials }}</BbAvatar>
264
268
  <span class="min-w-0 flex-1 truncate">{{ text }}</span>
265
- <BbIcon
266
- v-if="selected"
267
- class="shrink-0 text-[color:var(--bb-text-muted)]"
268
- icon="lucide:check"
269
- :size="14"
270
- />
271
269
  </span>
272
270
  </template>
273
271
  </BbSelectPopover>
@@ -275,13 +273,7 @@ height wins everywhere.
275
273
  </template>
276
274
  <script setup lang="ts">
277
275
  import { computed, ref } from 'vue';
278
- import {
279
- BbAvatar,
280
- BbBadge,
281
- BbBadgeButton,
282
- BbIcon,
283
- BbSelectPopover,
284
- } from 'bitboss-ui';
276
+ import { BbAvatar, BbBadge, BbBadgeButton, BbSelectPopover } from 'bitboss-ui';
285
277
 
286
278
  type Person = { label: string; value: string; initials: string };
287
279
 
@@ -31,12 +31,14 @@ that state to your data source (client-side or via `dependencies`-driven
31
31
  refetch). This keeps it equally at home in a client-only screen and a
32
32
  server-paginated one.
33
33
 
34
- **`BbTable` ships no client-side virtualization or windowing** every row you
35
- hand it in `items` mounts to the DOM, always. A few hundred rows is fine for a
36
- client-only screen. Past roughly 1,000 rows, don't ship the whole dataset to
37
- the browser: page it — feed `BbTable` the current slice as `items` and pass
38
- `page` / `per-page` / `total-items` (see § Pagination) so the server, not the
39
- DOM, holds the rest.
34
+ **Every row in `items` mounts to the DOM unless you say `virtual`.** A few
35
+ hundred rows is fine for a client-only screen. Past roughly 1,000 rows,
36
+ either page it feed `BbTable` the current slice as `items` and pass `page`
37
+ / `per-page` / `total-items` (see § Pagination) so the server, not the DOM,
38
+ holds the rest or, when the whole dataset legitimately lives in the
39
+ browser (local filtering, a spreadsheet-shaped tool), add `virtual` and let
40
+ the table window its rows (see § Virtual rows). The two compose: a large
41
+ page can be windowed.
40
42
 
41
43
  ### Columns: the full grammar
42
44
 
@@ -2490,6 +2492,107 @@ your wrapper actually sits on before sprinkling `min-width: 0` everywhere.)
2490
2492
  </div>
2491
2493
  ```
2492
2494
 
2495
+ ### Virtual rows
2496
+
2497
+ `virtual` windows the body: only the rows in the vertical scrollport (plus a
2498
+ few on each side) are mounted, and two spacer rows hold the scrollport at the
2499
+ full list height, so 5,000 or 50,000 rows scroll at the cost of a few dozen.
2500
+ Everything else is the same table — the rows stay in flow in the grid, so the
2501
+ stuck header, `fixed-columns`, nested tables, selection, highlight and the
2502
+ keyboard grammar work untouched. One prop, no knobs.
2503
+
2504
+ - **Give it a scroller.** Bound the table's height (`max-height` / `height`
2505
+ on the table, or a flex item with `min-height: 0`) and the table itself
2506
+ scrolls its rows; left unbounded it grows with the page and the page (or
2507
+ the nearest scrolling ancestor) is the scroller. `virtual` implies a stuck
2508
+ header — a windowed body without column names is a scroller, not a table.
2509
+ - **Columns are sized once.** A windowed body only ever holds a few rows, so
2510
+ `auto` tracks would move as rows come and go. The first rendered batch is
2511
+ measured and its widths become floors (`minmax(px, 1fr)`): they still share
2512
+ surplus, later rows never widen them — and the cells of those columns are
2513
+ single-line and **clip with an ellipsis** (like a resized column) so a
2514
+ longer value that shows up later never spills into its neighbour. Want a
2515
+ column to wrap (prose, notes)? Declare a `width` on it: a declared width
2516
+ wins as always and keeps wrapping. A resize overrides as always, and an
2517
+ empty→non-empty transition (a refetch) re-measures from the new first
2518
+ batch.
2519
+ - **A fast wheel never shows blank space.** A band of skeleton rows is kept
2520
+ rendered beyond the window on both sides (cheap: empty cells at the
2521
+ measured row height), so a wheel that the compositor scrolls ahead of the
2522
+ main thread reveals skeletons, not blank spacer, and they turn into real
2523
+ rows as the window reaches them. While the scroll is fast, rows never
2524
+ mounted before stay skeletons and are swapped for real rows as soon as
2525
+ it slows; rows you have already seen stay real. A moderate scroll never
2526
+ sees a skeleton.
2527
+ - **Dynamic heights are fine.** Every mounted row is measured (wrapping
2528
+ cells, a "show more", an expand row that opens a nested table three levels
2529
+ deep), measurements are cached by row so a row seen once stays right, and a
2530
+ height change _above_ the viewport moves the scroll offset with it so what
2531
+ you are reading does not jump.
2532
+ - **Focus is never lost to an unmount.** The row (or expand row) that owns
2533
+ focus stays mounted however far it scrolls — a control inside a nested
2534
+ table included — and is released on blur. Arrow keys, Home/End and
2535
+ PageUp/PageDown work across the whole list (the target row is scrolled in,
2536
+ mounted, then focused).
2537
+ - **Nested tables are not windowed.** A nested table inside an expand row is
2538
+ one tall row to the virtualizer; give a huge nested dataset its own page.
2539
+ `virtual` is ignored on a table that inherits its widths, and under a
2540
+ `#tbody` slot.
2541
+ - **Rows keep their absolute position for AT** (`aria-rowcount` /
2542
+ `aria-rowindex`), page or no page — a virtual table is one page unless you
2543
+ say otherwise.
2544
+
2545
+ **5,000 rows in a 320px scroller**
2546
+
2547
+ ```vue
2548
+ <template>
2549
+ <!-- Bounding the table's height is what makes it the scroller of its own
2550
+ rows; `virtual` windows them and implies the stuck header. -->
2551
+ <div
2552
+ class="max-w-2xl overflow-hidden rounded-[var(--bb-radius)] border border-[color:var(--bb-border)]"
2553
+ >
2554
+ <BbTable
2555
+ caption="Ledger"
2556
+ class="max-h-80"
2557
+ :columns="columns"
2558
+ compact
2559
+ :fixed-columns="[0]"
2560
+ item-value="id"
2561
+ :items="rows"
2562
+ virtual
2563
+ />
2564
+ </div>
2565
+ </template>
2566
+ <script setup lang="ts">
2567
+ import { BbTable } from 'bitboss-ui';
2568
+ import type { BbTableColumn } from 'bitboss-ui';
2569
+
2570
+ type Entry = {
2571
+ id: number;
2572
+ number: string;
2573
+ client: string;
2574
+ issued: string;
2575
+ amount: string;
2576
+ };
2577
+
2578
+ const columns: Array<BbTableColumn<Entry>> = [
2579
+ { key: 'number', label: 'Invoice' },
2580
+ { key: 'client', label: 'Client' },
2581
+ { key: 'issued', label: 'Issued' },
2582
+ { key: 'amount', label: 'Amount', align: 'right' },
2583
+ ];
2584
+
2585
+ const clients = ['Northwind Traders', 'Globex', 'Initech', 'Umbrella', 'Hooli'];
2586
+ const rows: Entry[] = Array.from({ length: 5000 }, (_, i) => ({
2587
+ id: i,
2588
+ number: `INV-${10000 + i}`,
2589
+ client: clients[i % clients.length],
2590
+ issued: `2026-${String(1 + (i % 12)).padStart(2, '0')}-${String(1 + (i % 28)).padStart(2, '0')}`,
2591
+ amount: `€${((i * 37) % 9000) + 100}`,
2592
+ }));
2593
+ </script>
2594
+ ```
2595
+
2493
2596
  ### Narrow screens: hide columns, don't scroll everything
2494
2597
 
2495
2598
  An ops table with ten columns is unusable on a phone as a horizontal scroller —
@@ -2551,6 +2654,9 @@ through element names, which match nothing:
2551
2654
  | focused header | `.bb-table-header[data-column-key]:focus-visible` (`reorderable` only; inset ring, the header row is one roving tab stop) | `columnheader` |
2552
2655
  | dragged column | `.bb-table-header--dragging` on the header; `.bb-table__head--dragging` on the head group during a drag | — |
2553
2656
  | clipped header | `.bb-table-header--clip` (the track is a length or an equal split, so the label ellipsizes instead of flooring the column) | `columnheader` |
2657
+ | virtual table | `.bb-table--virtual` on the container (`overflow: auto`, the scroller once its height is bounded) | — |
2658
+ | spacer row | `.bb-table-spacer__row` (a `virtual` body's stand-in for unmounted rows, `aria-hidden`; never style its height) | — (`aria-hidden`) |
2659
+ | skeleton row | `.bb-table-placeholder__row` > `.bb-table-placeholder__cell` (a `virtual` body's skeleton band and fast-scroll stand-in) | `row` > `cell` (`aria-hidden`) |
2554
2660
  | drag chip | `.bb-table-column-ghost` (a `popover="manual"` in the top layer; paints `--bb-panel`, not the table's `--bg`) | — (`aria-hidden`) |
2555
2661
  | caption | `.bb-table-caption` (before the root, `aria-labelledby`) | — |
2556
2662
 
@@ -2817,6 +2923,7 @@ reader.
2817
2923
  | `totalItems` | `string \| number \| undefined` | | | Total number of items there is. Also flows into the shared table context, where a `BbPagination` paired through `id` derives its page count from it. When omitted, an array `items` table infers it from `items.length`; an explicit value (or a… |
2818
2924
  | `totalPages` | `string \| number \| undefined` | | | Seeds the page count shown by a `BbPagination` paired through `id`. A shortcut for when the server reports a page count rather than a row count: the table renders nothing from it, it only flows into the shared table context. A known total a… |
2819
2925
  | `unselectedItems` | `any[] \| undefined` | `[]` | | Items that are not selected. |
2926
+ | `virtual` | `boolean \| undefined` | | | Windows the body: only the rows in (and just around) the vertical scrollport are mounted, so a client-side dataset of any size scrolls at a constant cost. The table itself is the scroller as soon as you bound its height (`max-height` / `hei… |
2820
2927
 
2821
2928
  ## Events
2822
2929
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.10",
4
+ "version": "3.0.0-beta.14",
5
5
  "upgrade": "v2-to-v3",
6
6
  "guide": "ai/guides/migration/v2-to-v3.md",
7
7
  "summary": {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.10",
5
- "generatedAt": "2026-08-27T13:52:59.543Z",
4
+ "version": "3.0.0-beta.14",
5
+ "generatedAt": "2026-08-28T14:01:44.030Z",
6
6
  "composables": [
7
7
  {
8
8
  "name": "useBbConfig",
@@ -3506,6 +3506,11 @@
3506
3506
  "name": "append-outer",
3507
3507
  "type": "BaseSelectOuterSlotProps"
3508
3508
  },
3509
+ {
3510
+ "name": "chip",
3511
+ "type": "BaseSelectChipSlotProps<Item>",
3512
+ "description": "Replaces the default badge rendered for each selection in `multiple` mode."
3513
+ },
3509
3514
  {
3510
3515
  "name": "footer",
3511
3516
  "type": "BaseSelectPanelSlotProps",
@@ -10923,6 +10928,11 @@
10923
10928
  "type": "{ query: string; close: () => Promise<void>; }",
10924
10929
  "description": "Content appended outside the input field wrapper."
10925
10930
  },
10931
+ {
10932
+ "name": "chip",
10933
+ "type": "BbSelectChipSlotProps<T>",
10934
+ "description": "Replaces the default badge rendered for each selection in `multiple` mode.\nNot called in `comma` mode, nor for the collapsed \"+N\" summary."
10935
+ },
10926
10936
  {
10927
10937
  "name": "footer",
10928
10938
  "type": "BbSelectPanelSlotProps",
@@ -12894,6 +12904,12 @@
12894
12904
  "required": false,
12895
12905
  "default": "[]",
12896
12906
  "description": "Items that are not selected."
12907
+ },
12908
+ {
12909
+ "name": "virtual",
12910
+ "type": "boolean | undefined",
12911
+ "required": false,
12912
+ "description": "Windows the body: only the rows in (and just around) the vertical\nscrollport are mounted, so a client-side dataset of any size scrolls at\na constant cost. The table itself is the scroller as soon as you bound\nits height (`max-height` / `height`, or a flex item with\n`min-height: 0`); left unbounded, the nearest scrolling ancestor or the\npage scrolls it. The header sticks to that scroller. Column widths are\nmeasured once from the first rendered batch and then hold — later rows\nnever widen them. Expand rows and nested tables stay fully rendered\ninside their row (a nested table is never windowed); the row that owns\nfocus stays mounted even when scrolled out of view. Ignored by a table\nthat inherits its widths from a parent, and under a `#tbody` slot."
12897
12913
  }
12898
12914
  ],
12899
12915
  "events": [
@@ -19,7 +19,17 @@
19
19
  The append icon joins it here so the two can share the hover swap; on a
20
20
  non-clearable badge it renders inside the body instead. -->
21
21
  <span v-if="props.clearable" class="bb-badge__trailing">
22
- <BadgeAppendIcon v-if="!props.icon" />
22
+ <template v-if="!props.icon">
23
+ <BbSpinner
24
+ v-if="props.loading && spinnerTrails()"
25
+ class="bb-badge__append-icon"
26
+ />
27
+ <BbIcon
28
+ v-else-if="props['append:icon']"
29
+ class="bb-badge__append-icon"
30
+ :icon="props['append:icon']"
31
+ />
32
+ </template>
23
33
  <button
24
34
  :id="`${badgeId}__clear`"
25
35
  :aria-label="props.clearableLabel || t('badge.clearLabel').value"
@@ -45,7 +55,7 @@ import { useLogger } from '@/composables/useLogger';
45
55
  import { removedPropsFor } from '@/deprecation/removedProps';
46
56
  import { warnRemovedAttrs } from '@/utilities/functions/warnRemovedAttrs';
47
57
  import BbIcon from '../BbIcon/BbIcon.vue';
48
- import BadgeAppendIcon from './BadgeAppendIcon.vue';
58
+ import BbSpinner from '../BbSpinner/BbSpinner.vue';
49
59
  import BadgeBodyContent from './BadgeBodyContent.vue';
50
60
  import BbBadgeButton from './BbBadgeButton.vue';
51
61
  import { provideBadgeHost } from './badgeButtonContext';
@@ -80,9 +90,7 @@ const normalizedSize = computed(() => {
80
90
  });
81
91
 
82
92
  /** The spinner overlays the append slot only when the append icon is the sole icon. */
83
- const spinnerTrails = computed(
84
- () => !!props['append:icon'] && !props['prepend:icon']
85
- );
93
+ const spinnerTrails = () => !!props['append:icon'] && !props['prepend:icon'];
86
94
 
87
95
  /* ── BbBadgeButton detection ─────────────────────────────── */
88
96
 
@@ -106,15 +114,46 @@ const scanForButton = (nodes: unknown): boolean =>
106
114
  const detectButton = () => scanForButton(slots.default?.({}));
107
115
 
108
116
  // Published to the subtree: BadgeBodyContent — ours or the one inside a
109
- // slotted BbBadgeButton — renders the leading side from this state.
117
+ // slotted BbBadgeButton — renders the leading side from this state. Plain
118
+ // getters, not `computed()`: see `HostValue`. Every entry is a bare prop read,
119
+ // so there is nothing for a computed to memoize — and seven reactive effects
120
+ // per badge is a real per-row tax in a table cell that carries three of them.
110
121
  provideBadgeHost({
111
- icon: computed(() => props.icon),
112
- prependIcon: computed(() => props['prepend:icon']),
113
- appendIcon: computed(() => props['append:icon']),
114
- loading: computed(() => !!props.loading),
115
- spinnerLeads: computed(() => !!props.loading && !spinnerTrails.value),
116
- spinnerTrails: computed(() => !!props.loading && spinnerTrails.value),
117
- clearable: computed(() => !!props.clearable),
122
+ icon: {
123
+ get value() {
124
+ return props.icon;
125
+ },
126
+ },
127
+ prependIcon: {
128
+ get value() {
129
+ return props['prepend:icon'];
130
+ },
131
+ },
132
+ appendIcon: {
133
+ get value() {
134
+ return props['append:icon'];
135
+ },
136
+ },
137
+ loading: {
138
+ get value() {
139
+ return !!props.loading;
140
+ },
141
+ },
142
+ spinnerLeads: {
143
+ get value() {
144
+ return !!props.loading && !spinnerTrails();
145
+ },
146
+ },
147
+ spinnerTrails: {
148
+ get value() {
149
+ return !!props.loading && spinnerTrails();
150
+ },
151
+ },
152
+ clearable: {
153
+ get value() {
154
+ return !!props.clearable;
155
+ },
156
+ },
118
157
  });
119
158
 
120
159
  const badgeClasses = computed(() => [
@@ -115,7 +115,11 @@
115
115
  v-if="usesOverflowCollapse || !isCollapsed"
116
116
  ref="chipsContainerRef"
117
117
  class="bb-base-select__chips-row"
118
- :class="{ 'bb-base-select__chips-row--hidden': isCollapsed }"
118
+ :class="{
119
+ 'bb-base-select__chips-row--hidden': isCollapsed,
120
+ 'bb-base-select__chips-row--chips':
121
+ !comma && !!selectedOptions.length,
122
+ }"
119
123
  >
120
124
  <template v-if="comma">
121
125
  <span
@@ -127,19 +131,40 @@
127
131
  </span>
128
132
  </template>
129
133
  <!-- One badge per selection: removable (clearable) when
130
- deselection is allowed, non-removable otherwise. -->
134
+ deselection is allowed, non-removable otherwise. The `chip`
135
+ slot replaces the badge outright (the badge below is its
136
+ fallback), so a consumer can render their own chip — a colored
137
+ tag, an avatar row — without reaching for `prepend`. No wrapper
138
+ element around it: the row itself supplies the spacing (see
139
+ `--chips` in index.css), so custom content needs none of our
140
+ class names to sit correctly. -->
131
141
  <template v-else>
132
- <BbBadge
142
+ <!-- `v-for` on a `<template>`, not on the `<slot>` itself: a
143
+ `key` bound on a slot outlet is passed through as a slot
144
+ PROP. The template renders no element of its own. -->
145
+ <template
133
146
  v-for="item in selectedOptions"
134
147
  :key="hash(item.value)"
135
- class="bb-base-select__chip"
136
- :clearable="canDeselectChips"
137
- size="lg"
138
- variant="secondary"
139
- @click:clear.stop="onChipUnselected(item)"
140
148
  >
141
- {{ item.text }}
142
- </BbBadge>
149
+ <slot
150
+ :deselect="() => onChipUnselected(item)"
151
+ :disabled="!canDeselectChips"
152
+ :item="item.item"
153
+ name="chip"
154
+ :text="item.text"
155
+ :value="item.value"
156
+ >
157
+ <BbBadge
158
+ class="bb-base-select__chip"
159
+ :clearable="canDeselectChips"
160
+ size="lg"
161
+ variant="secondary"
162
+ @click:clear.stop="onChipUnselected(item)"
163
+ >
164
+ {{ item.text }}
165
+ </BbBadge>
166
+ </slot>
167
+ </template>
143
168
  </template>
144
169
  </span>
145
170
  <span v-if="isCollapsed" class="bb-base-select__max-reached">
@@ -769,6 +794,27 @@ export type BaseSelectPanelSlotProps = {
769
794
  selectedOptions: BbSelectResolvedOption[];
770
795
  };
771
796
 
797
+ /**
798
+ * Props exposed by the `chip` slot — called once per selected value in
799
+ * `multiple` mode, in place of the default badge. Not called in `comma` mode
800
+ * (there are no chips there) nor for the collapsed "+N" summary.
801
+ */
802
+ export type BaseSelectChipSlotProps<Item = any> = {
803
+ /** The selected item, exactly as it was supplied in `items`. */
804
+ item: Item;
805
+ /** The item's resolved display text (what the default badge prints). */
806
+ text: string;
807
+ /** The item's resolved value — what sits in the v-model. */
808
+ value: any;
809
+ /**
810
+ * `true` while the control is disabled or readonly: the chip must not offer
811
+ * a way to remove itself, and `deselect` is a no-op.
812
+ */
813
+ disabled: boolean;
814
+ /** Drops this value from the selection. No-op while `disabled`. */
815
+ deselect: () => void;
816
+ };
817
+
772
818
  export type BaseSelectSlots<Item = any> = {
773
819
  /** Content pinned to the top of the panel, above the search field. */
774
820
  header?: (props: BaseSelectPanelSlotProps) => any;
@@ -784,6 +830,8 @@ export type BaseSelectSlots<Item = any> = {
784
830
  'no-data'?: (props: { query: string; focus: () => void }) => any;
785
831
  option?: (props: BaseSelectOptionSlotProps<Item>) => any;
786
832
  group?: (props: BaseSelectGroupSlotProps<Item>) => any;
833
+ /** Replaces the default badge rendered for each selection in `multiple` mode. */
834
+ chip?: (props: BaseSelectChipSlotProps<Item>) => any;
787
835
  };
788
836
  ```
789
837
 
@@ -885,12 +933,19 @@ export type BaseSelectSlots<Item = any> = {
885
933
  pointer-events: none;
886
934
  visibility: hidden;
887
935
  }
888
- }
889
936
 
890
- &__chip {
891
- flex-shrink: 0;
892
- &:not(:last-child) {
893
- margin-right: 0.25rem;
937
+ /* Chips mode with at least one selection — never comma mode, which
938
+ separates with its own `,` and wants no gap. The `chip` slot lets a
939
+ consumer render their own element in place of our badge, so the layout
940
+ a chip needs (no shrinking, the gap between chips, and the negative
941
+ inset coupling below) lives HERE, on the row, rather than on a
942
+ `__chip` class custom content would have to know to apply. */
943
+ &--chips {
944
+ gap: 0.25rem;
945
+
946
+ > * {
947
+ flex-shrink: 0;
948
+ }
894
949
  }
895
950
  }
896
951
 
@@ -927,7 +982,7 @@ export type BaseSelectSlots<Item = any> = {
927
982
  }
928
983
  /* If there is no prepend icon, add a negative margin to the chips row so it smoothly couples with the border */
929
984
  &:not(:has(.common-input-wrapper__prepend-icon)) {
930
- .bb-base-select__chips-row:has(.bb-base-select__chip) {
985
+ .bb-base-select__chips-row--chips {
931
986
  margin-left: calc(
932
987
  var(--bb-input-px) * var(--compensation-multiplier) * -1
933
988
  );