bitboss-ui 3.0.0-beta.60 → 3.0.0-beta.62

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 (102) 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/BbForm.md +2 -1
  5. package/dist/ai/BbSelect.md +6 -2
  6. package/dist/ai/BbSelectPopover.md +1 -1
  7. package/dist/ai/BbTable.md +116 -43
  8. package/dist/ai/BbToast.md +2 -1
  9. package/dist/ai/GroupedListBox.md +1 -1
  10. package/dist/ai/changelog.json +47 -4
  11. package/dist/ai/components.json +7 -7
  12. package/dist/ai/composables/useToast.md +15 -15
  13. package/dist/ai/guides/eslint-rules.md +2 -0
  14. package/dist/ai/guides/inertia-helpers.md +39 -7
  15. package/dist/ai/guides/installation-and-plugin-setup.md +2 -1
  16. package/dist/ai/guides/migration/v2-to-v3.md +14 -0
  17. package/dist/ai/guides/migration/v3-beta-tooling.md +6 -0
  18. package/dist/ai/guides/passthrough.md +2 -2
  19. package/dist/ai/guides/validated-forms.md +3 -2
  20. package/dist/ai/helpers/inertia/manifest.json +18 -10
  21. package/dist/ai/helpers/inertia/useConfirmOnRouteLeave.ts +11 -2
  22. package/dist/ai/helpers/inertia/usePromiseForm.ts +17 -3
  23. package/dist/ai/helpers/inertia/useResourceContext.ts +1 -2
  24. package/dist/ai/manifest/components/BbSelect.json +1 -1
  25. package/dist/ai/manifest/components/BbSelectPopover.json +1 -1
  26. package/dist/ai/manifest/components/BbTable.json +9 -7
  27. package/dist/ai/manifest/components/BbTable.tree.json +4 -1
  28. package/dist/ai/manifest/components/GroupedListBox.json +1 -1
  29. package/dist/ai/manifest/index.json +2 -2
  30. package/dist/ai/manifest/types.advanced.json +1 -1
  31. package/dist/ai/recipes/inertia/onboarding.md +10 -6
  32. package/dist/ai/recipes/inertia/record-form.md +6 -2
  33. package/dist/ai/recipes/inertia/wizard-form.md +9 -5
  34. package/dist/ai/source/BbDatePickerInput.md +6 -1
  35. package/dist/ai/source/BbDialog.md +4 -16
  36. package/dist/ai/source/BbForm.md +7 -2
  37. package/dist/ai/source/BbOffCanvas.md +4 -16
  38. package/dist/ai/source/BbSelect.md +2 -0
  39. package/dist/ai/source/BbSelectPopover.md +2 -0
  40. package/dist/ai/source/BbTable.md +403 -111
  41. package/dist/ai/source/BbTimePickerInput.md +5 -1
  42. package/dist/ai/source/GroupedListBox.md +31 -10
  43. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +211 -210
  44. package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +198 -195
  45. package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +3 -1
  46. package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +308 -305
  47. package/dist/components/BbSelect/types.d.ts +2 -0
  48. package/dist/components/BbSelectPopover/types.d.ts +2 -0
  49. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +1252 -1180
  50. package/dist/components/BbTable/BbTableExpandRow.d.ts +1 -1
  51. package/dist/components/BbTable/BbTableExpandRow.js +29 -25
  52. package/dist/components/BbTable/types.d.ts +37 -15
  53. package/dist/components/BbTable/types.js +1 -0
  54. package/dist/components/BbTable/utils.d.ts +73 -13
  55. package/dist/components/BbTable/utils.js +148 -37
  56. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +108 -106
  57. package/dist/components/GroupedListBox/GroupedListBox.vue_vue_type_script_setup_true_lang.js +116 -111
  58. package/dist/components/GroupedListBox/types.d.ts +1 -0
  59. package/dist/composables/useBodyScrollLock.d.ts +4 -0
  60. package/dist/composables/useBodyScrollLock.js +27 -0
  61. package/dist/composables/useFormBinding.d.ts +5 -0
  62. package/dist/composables/useFormBinding.js +2 -1
  63. package/dist/i18n/locales/cs.json +1 -0
  64. package/dist/i18n/locales/da.json +1 -0
  65. package/dist/i18n/locales/de-CH.json +1 -0
  66. package/dist/i18n/locales/de.json +1 -0
  67. package/dist/i18n/locales/el.json +1 -0
  68. package/dist/i18n/locales/en-GB.json +1 -0
  69. package/dist/i18n/locales/en.json +1 -0
  70. package/dist/i18n/locales/es-MX.json +1 -0
  71. package/dist/i18n/locales/es.json +1 -0
  72. package/dist/i18n/locales/fi.json +1 -0
  73. package/dist/i18n/locales/fr.json +1 -0
  74. package/dist/i18n/locales/hu.json +1 -0
  75. package/dist/i18n/locales/it.json +1 -0
  76. package/dist/i18n/locales/ja.json +1 -0
  77. package/dist/i18n/locales/ko.json +1 -0
  78. package/dist/i18n/locales/nb.json +1 -0
  79. package/dist/i18n/locales/nl.json +1 -0
  80. package/dist/i18n/locales/pl.json +1 -0
  81. package/dist/i18n/locales/pt-BR.json +1 -0
  82. package/dist/i18n/locales/pt-PT.json +1 -0
  83. package/dist/i18n/locales/ro.json +1 -0
  84. package/dist/i18n/locales/ru.json +1 -0
  85. package/dist/i18n/locales/sv.json +1 -0
  86. package/dist/i18n/locales/tr.json +1 -0
  87. package/dist/i18n/locales/uk.json +1 -0
  88. package/dist/i18n/locales/zh-CN.json +1 -0
  89. package/dist/i18n/locales/zh-HK.json +1 -0
  90. package/dist/i18n/locales/zh-TW.json +1 -0
  91. package/dist/llms-full.txt +239 -89
  92. package/dist/llms-medium.txt +2 -1
  93. package/dist/locale-blueprint.json +1 -0
  94. package/dist/styles.css +2 -2
  95. package/dist/types/ptComponentMap.d.ts +1 -1
  96. package/dist/utils/versionCheck.js +1 -1
  97. package/package.json +8 -4
  98. package/scripts/lib/doc-sections.mjs +26 -6
  99. package/scripts/lib/eslint-disable.mjs +2 -0
  100. package/scripts/lib/eslint-plugin.d.ts +2 -0
  101. package/scripts/lib/eslint-plugin.mjs +105 -0
  102. 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({
package/dist/ai/BbForm.md CHANGED
@@ -1617,7 +1617,8 @@ The `id` is also the `<form>`'s own (it follows a BbForm; a plain `<form>`
1617
1617
  submits but has no save state to follow), so a `<BbButton form="id" type="submit">`
1618
1618
  outside it submits it too, through the same validation, and spins while the
1619
1619
  save runs exactly like a submit button inside (`disable-auto-loading` opts
1620
- out): no `:loading` to bind.
1620
+ out): no `:loading` to bind. Server errors (`setErrors`, `field-errors`) then
1621
+ focus the first failing field, as they do after a submit from inside.
1621
1622
 
1622
1623
  ```vue
1623
1624
  <script setup lang="ts">
@@ -1617,7 +1617,11 @@ const createLabel = (text: string) => {
1617
1617
  Set `group-by` to a field path (or a function) to bucket options under headers.
1618
1618
  The `#group` slot styles the header and receives
1619
1619
  `{ text, item, index, length, disabled }` — `length` is the number of options
1620
- in the group, `index` the list index of its first option.
1620
+ in the group, `index` the list index of its first option. A row whose
1621
+ `group-by` gives `null` or `undefined` has no group: those rows are listed
1622
+ without a header, and `#group` is not called for them (a known row is listed
1623
+ the same way). Rows in the middle of the list with no header read as part of
1624
+ the group above, so give them a group of their own (`row.team ?? 'Other'`).
1621
1625
 
1622
1626
  A grouped list virtualizes **whole groups, not rows**: a group mounts only
1623
1627
  when it scrolls into range, but all of its rows mount together. Many
@@ -2346,7 +2350,7 @@ States are listed in precedence order: when two are on at once and their entries
2346
2350
  - `description` — scope: `BbSelectDescriptionSlotProps` `{ text: string; hasErrors: boolean; hasWarnings: boolean }` — Replaces the description text. Provided alone (no `description` prop) it still renders the description region and wires it into `aria-describedby`.
2347
2351
  - `empty` — scope: `{ query: string; focus: () => void; error: unknown; }` — Content shown when no options match the query or the list is empty. `error` is what the latest failed load threw, untouched (`null` when none).
2348
2352
  - `footer` — scope: `BbSelectPanelSlotProps` `{ close: () => Promise<void>; clear: () => void; focus: () => void; query: string; selectedOptions: BbSelectResolvedOption[] }` — Content pinned to the bottom of the options panel, below the options list. Rendered in both the desktop popover and the adaptive sheet.
2349
- - `group` — scope: `BbSelectGroupSlotProps<T>` `{ text: string; item: T; index: number; length: number; disabled: boolean }` — Replaces the default group header rendered above grouped options.
2353
+ - `group` — scope: `BbSelectGroupSlotProps<T>` `{ text: string; item: T; index: number; length: number; disabled: boolean }` — Replaces the default group header rendered above grouped options. Not called for rows with no group (`group-by` gives `null`/`undefined`, known rows): they are listed without a header.
2350
2354
  - `header` — scope: `BbSelectPanelSlotProps` `{ close: () => Promise<void>; clear: () => void; focus: () => void; query: string; selectedOptions: BbSelectResolvedOption[] }` — Content pinned to the top of the options panel, above the search field. Rendered in both the desktop popover and the adaptive sheet.
2351
2355
  - `label` — scope: `BbSelectLabelSlotProps` `{ showAsterisk?: boolean; text: string; hasErrors: boolean; hasWarnings: boolean }` — Replaces the default label text rendered above the input.
2352
2356
  - `loading` — scope: `{ query: string; }` — Content shown inside the options dropdown while items are loading.
@@ -1916,7 +1916,7 @@ States are listed in precedence order: when two are on at once and their entries
1916
1916
  - `activator` — scope: `BbSelectPopoverActivatorSlotProps` `{ clear: () => void; close: () => Promise<void>; focus: () => void; disabled: boolean; loading: boolean; open: () => Promise<void>; props: { ref: (reference: unknown) => void; }; query: string; readonly: boolean; selectedOptions: BbSelectResolvedOption<any>[]; shown: boolean; text: string; toggle: () => Promise<void> }` — Replaces the default trigger element that opens the select popover.
1917
1917
  - `empty` — scope: `{ query: string; focus: () => void; error: unknown; }` — Content shown when no options match the query or the list is empty. `error` is what the latest failed load threw, untouched (`null` when none).
1918
1918
  - `footer` — scope: `BbSelectPopoverPanelSlotProps` `{ close: () => Promise<void>; clear: () => void; focus: () => void; query: string; selectedOptions: BbSelectResolvedOption<any>[] }` — Content pinned to the bottom of the panel, below the options list. Rendered in both the desktop popover and the adaptive sheet.
1919
- - `group` — scope: `BbSelectPopoverGroupSlotProps<Item>` `{ disabled: boolean; index: number; item: Item; length: number; text: string }` — Replaces the default group header rendered above grouped options.
1919
+ - `group` — scope: `BbSelectPopoverGroupSlotProps<Item>` `{ disabled: boolean; index: number; item: Item; length: number; text: string }` — Replaces the default group header rendered above grouped options. Not called for rows with no group (`group-by` gives `null`/`undefined`, known rows): they are listed without a header.
1920
1920
  - `header` — scope: `BbSelectPopoverPanelSlotProps` `{ close: () => Promise<void>; clear: () => void; focus: () => void; query: string; selectedOptions: BbSelectResolvedOption<any>[] }` — Content pinned to the top of the panel, above the search field. Rendered in both the desktop popover and the adaptive sheet.
1921
1921
  - `loading` — scope: `{ query: string; }` — Content shown inside the options dropdown while items are loading.
1922
1922
  - `option` — scope: `BbSelectPopoverOptionSlotProps<Item>` `{ disabled: boolean; focused: boolean; index: number; item: Item; loading: boolean; selected: boolean; text: string; value: any; description: string }` — Replaces the default rendering of each option row in the dropdown.
@@ -88,11 +88,21 @@ needs is declared here, not in markup:
88
88
  (the table is a CSS grid — the tracks are the width API, there is no
89
89
  `table-layout`). An unfrozen text column also has a **floor estimated from
90
90
  what it shows**: short values (IDs, emails, dates) ask for one line, prose
91
- for about three, fitted to the room the sized columns leave — short values
92
- give way first and wrap, prose keeps at least half its width, and only then
93
- does the table scroll sideways. The header is always part of that floor, so
94
- a heading never clips. No floor on `fixed` or `virtual` tables, slot
95
- columns, or under a custom `#thead`. A grid keyword or function is the track as written:
91
+ for about three, fitted to the room the sized columns leave. Values a line
92
+ break would harm stay whole: a short name or status ("Poltrona 10", "In
93
+ attesa di verifica"), an amount with its currency, a range or pair ("0,00 € /
94
+ 450,00 €"), a date or code ("16/10/2026", a grouped IBAN). When room runs
95
+ short, longer values wrap first, then prose narrows, then a long heading of
96
+ several words takes two lines; a table that scrolls sideways anyway keeps
97
+ all of those whole. Only a table that would otherwise _start_ scrolling
98
+ wraps them, and long-form text never runs wider than a readable measure.
99
+ You rarely need `whitespace-nowrap` in a `td-class`. The header is always
100
+ part of that floor, so a heading never clips. (On a server-rendered page
101
+ the server cannot know the width: a table whose headings need two lines
102
+ takes them, once, when the page hydrates.) The floors read every row up
103
+ to 2,000; past that, the first 1,000 and a sample across the rest (a
104
+ `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:
96
106
  `'auto'`, `'2fr'`, `'minmax(0, 1fr)'` (a share that may shrink below its
97
107
  content), `'fit-content(200px)'`. When _every_ column declares a length, the
98
108
  lengths become floors and the surplus is shared in proportion to their
@@ -354,7 +364,9 @@ not sure — write the name through the function instead of by hand:
354
364
  `#[slotKey('Qualità')]`, with `import { slotKey } from 'bitboss-ui'`; the same
355
365
  goes for slot names built in code (columns from config, a dynamic `<template>`
356
366
  loop). A hand copy that differs by one character makes the slot silently never
357
- match; `bitboss-ui check` catches it when `columns` is in the same file. Keep the column in `columns`
367
+ match; `bitboss-ui check` catches it when `columns` is in the same file, and
368
+ warns on a loop over keys it cannot see (`#[column.key]` over a prop) until
369
+ the name goes through `slotKey`. Keep the column in `columns`
358
370
  even when you fully slot it: label, alignment and width still come from there.
359
371
 
360
372
  Body cell scope: `content` (the formatted value), `item` (raw row), `value`
@@ -692,7 +704,9 @@ empty the table shows its empty row — localized default text, overridable via
692
704
  `empty-text` or fully via the `#empty` slot. That slot fills the table's own
693
705
  full-width empty-state cell (already centred, already spanning every column),
694
706
  so pass the content — a message, a "clear filters" button — not a row of your
695
- own.
707
+ own. On a table that scrolls sideways (wide columns at phone width) the
708
+ content centres in the visible part, not on the whole grid, and stays there
709
+ as the table scrolls.
696
710
 
697
711
  `loading-text` is the loading row's `aria-live` announcement. Like
698
712
  `empty-text` it has a **localized default** (`common.loadingText` —
@@ -1096,13 +1110,19 @@ scriptable from anywhere via `useBbTableContext`
1096
1110
  Mark columns `sortable` and bind `v-model:sort` — an ordered
1097
1111
  `BbTableSortEntry[]` (`[columnKey, 'asc' | 'desc']`). A click anywhere on a
1098
1112
  sortable header (its whitespace included — AG Grid parity; the built-in
1099
- button is the keyboard/AT control) cycles the column unsorted → asc → desc →
1100
- removed; a direction flip updates the entry in
1101
- place, so the column keeps its priority in a multi-column sort and other
1102
- entries are never touched. The array order **is** the priority: first entry
1103
- sorts first. The table only reports the model; **apply it to your data**
1104
- (client-side, as below) or forward it to the server as `orderBy` and refetch
1105
- via `dependencies`.
1113
+ button is the keyboard/AT control), or Enter on it, sorts by **that column
1114
+ alone**, cycling it unsorted → asc → desc → removed (`[]`).
1115
+
1116
+ **Shift+click** (or Shift+Enter) adds the column instead: it joins the end of
1117
+ the model, a direction flip updates its entry in place (it keeps its priority)
1118
+ and its third click removes it alone; the other entries are never touched. Set
1119
+ `multi-sort` when a plain click should add too. The array order **is** the
1120
+ priority: first entry sorts first. A several-entry model you write yourself
1121
+ is shown as is in either mode; the next plain click replaces it.
1122
+
1123
+ The table only reports the model; **apply it to your data** (client-side, as
1124
+ below) or forward it to the server as `orderBy` and refetch via
1125
+ `dependencies`.
1106
1126
 
1107
1127
  The affordance is one arrow: nothing at rest, a faint arrow previewed while
1108
1128
  the header is hovered or focused, solid once the column is sorted (up for
@@ -1114,9 +1134,9 @@ third click.
1114
1134
  ```vue
1115
1135
  <template>
1116
1136
  <div class="flex max-w-2xl flex-col gap-1.5">
1117
- <!-- Mark columns `sortable` and bind `v-model:sort`. The header buttons
1118
- cycle asc → desc → off; the table only REPORTS the model — you sort
1119
- the data. -->
1137
+ <!-- Mark columns `sortable` and bind `v-model:sort`. A click sorts by
1138
+ that column (asc → desc → off), Shift+click adds it; the table only
1139
+ REPORTS the model — you sort the data. -->
1120
1140
  <div
1121
1141
  class="overflow-hidden rounded-(--bb-radius) border border-(--bb-border)"
1122
1142
  >
@@ -1189,7 +1209,9 @@ const sortedInvoices = computed(() => {
1189
1209
 
1190
1210
  A custom `#<key>:header` slot replaces the built-in sort button, but its scope
1191
1211
  hands you `sortOrder` and `toggleSort` so your header can keep sorting alive
1192
- (render your own button and call `toggleSort`). To merely add something next
1212
+ (render your own button with `@click="toggleSort"`: given the event, Shift adds
1213
+ the column like on the built-in header; `toggleSort()` with no event sorts by
1214
+ that column alone). To merely add something next
1193
1215
  to the control — a resize handle, a filter icon — use
1194
1216
  `#<key>:header:prepend/append` instead and keep the built-in button (see
1195
1217
  "Custom cells and headers"). The column-level `sorted`
@@ -1211,7 +1233,8 @@ it **renders** in — an array of column keys, the same split as `sortable` +
1211
1233
  Grid style, a chip with the label follows), release to commit. The same
1212
1234
  header row is also one keyboard tab stop (the AG Grid grammar): ←/→ move
1213
1235
  focus between headers, **Shift+←/→** move the column one slot
1214
- (Shift+Home/End to the ends), Enter sorts a sortable header.
1236
+ (Shift+Home/End to the ends), Enter sorts a sortable header (Shift+Enter
1237
+ adds it to the sort).
1215
1238
  Each committed move writes the model once and is announced.
1216
1239
 
1217
1240
  The model's contract is what makes a column panel trivial:
@@ -2014,6 +2037,33 @@ fixed first track).
2014
2037
  The actions-column toggle remains valid — prefer it when expansion reveals
2015
2038
  _detail about the row_ rather than _children of it_.
2016
2039
 
2040
+ #### Styling the detail area
2041
+
2042
+ The cell that holds `#expand` (it spans every column) is the `expand`
2043
+ passthrough part. It has no padding of its own, so the slot's content sits
2044
+ flush; give the area a surface or an inset there:
2045
+
2046
+ ```vue
2047
+ <BbTable
2048
+ v-model:expanded-items="open"
2049
+ :columns="columns"
2050
+ :items="invoices"
2051
+ item-value="id"
2052
+ pt:expand="bg-(--bb-surface-hover) px-4 py-3"
2053
+ pt:expand:highlighted="bg-(--bb-muted)"
2054
+ >
2055
+ <template #expand="{ item }">…</template>
2056
+ </BbTable>
2057
+ ```
2058
+
2059
+ It takes the data row's `selected`, `highlighted` and `expanded` (always
2060
+ on), and, like `pt:row`, a
2061
+ per-item function: `:pt:expand="({ item }) => item.overdue && 'bg-red-50'"`.
2062
+ It is not a `cell`, so `pt:cell` never reaches it: a table that moves its row
2063
+ rules off the cells (`pt:cell="border-b-0"`, a top rule on `pt:row`) takes
2064
+ the expand cell's off too (`pt:expand="border-b-0"`), or the line under each
2065
+ open row is drawn twice.
2066
+
2017
2067
  ### Reordering rows
2018
2068
 
2019
2069
  `reorderable-rows` adds a slim drag handle at the start of every row: the user drags
@@ -2214,10 +2264,12 @@ keeps until that column's `width` changes:
2214
2264
  - A resized width is always px; `width` still accepts `'20%'` / `'12rem'`
2215
2265
  for what you declare yourself.
2216
2266
 
2217
- Headers never wrap. A column you have not sized is never narrower than its
2218
- own heading — the label is the table's size hint, so an empty or slim column
2219
- with a long heading still reads on a narrow screen (the table overflows
2220
- instead). Once a track is a length (a declared `width`, a user resize) the
2267
+ Headers stay on one line. A column you have not sized is never narrower
2268
+ than its own heading — the label is the table's size hint, so an empty or
2269
+ slim column with a long heading still reads on a narrow screen. The one
2270
+ exception: a long heading of several words ("Numero e data emissione") takes
2271
+ two lines when the alternative is the table scrolling sideways; short
2272
+ headings never do, and no `pt` class is needed for it. Once a track is a length (a declared `width`, a user resize) the
2221
2273
  label cannot widen it: squeezed, it ellipsizes and the sort arrow stays; at
2222
2274
  the narrowest only the arrow (and the handle) survive, and the column is
2223
2275
  still resizable. The data cells of a resized column clip with an ellipsis
@@ -3312,7 +3364,7 @@ const columns = [
3312
3364
  ];
3313
3365
  ```
3314
3366
 
3315
- Three rules:
3367
+ Four rules:
3316
3368
 
3317
3369
  - **The pin is the column's.** Under `order` / `reorderable` it travels with
3318
3370
  the cell, and a `hidden` pinned column pins nothing. The offset is still
@@ -3327,6 +3379,15 @@ Three rules:
3327
3379
  - **Nested tables pin against the ancestor scrollport.** A child that
3328
3380
  inherits the parent's tracks has no scrollport of its own, so both tables'
3329
3381
  pins land on the same visible edge (see Nested tables).
3382
+ - **Pinned cells sit at `z-index: 2`.** Body and header cells alike; under
3383
+ `fixed-headers` the stuck header band and its pinned headers sit at `3`,
3384
+ and so do the `fixed-first` / `fixed-last` rows; column resize handles sit
3385
+ at `1`. Cell content that sets its own `z-index` (a positioned badge with
3386
+ `z-10`, say) paints over the pinned column as it scrolls under it: keep
3387
+ such content at `z-index: 1` or lower, or put `isolate` on its wrapper so
3388
+ its layers stay inside it. The table root opens no stacking context of
3389
+ its own, so these values also count against the page: a sticky app bar
3390
+ that a scrolled table must pass under needs `z-index: 4` or more.
3330
3391
 
3331
3392
  ### Virtual rows
3332
3393
 
@@ -3652,21 +3713,24 @@ object form, a style and attributes — optionally only while a state is on.
3652
3713
  Parts: `root` (the container), `caption` (the visible caption), `table` (the
3653
3714
  grid), `head` / `body` / `foot` (the row groups), `header-row`,
3654
3715
  `cell-column-header` (every header cell — the select and actions ones
3655
- included, a broadcast), `row` (every DATA row, a broadcast) and `cell` (every
3656
- data cell — the select and actions cells included, a broadcast), and three
3657
- glyphs: `sort-icon`, `select-icon` (each row's checkbox or radio and the
3658
- select-all) and `drag-icon` (on the chip a column drag carries). The grid is
3659
- `div`s with ARIA roles, so these are parts, not `th` / `td` selectors.
3716
+ included, a broadcast), `row` (every DATA row, a broadcast), `cell` (every
3717
+ data cell — the select and actions cells included, a broadcast), `expand`
3718
+ (the cell holding an open row's `#expand`), `first-row` / `last-row` (the
3719
+ edge rows), `drag-handle` (each row's grip under `reorderable-rows`), and
3720
+ three glyphs: `sort-icon`, `select-icon` (each row's checkbox or radio and
3721
+ the select-all) and `drag-icon` (on the chip a column drag carries). The grid
3722
+ is `div`s with ARIA roles, so these are parts, not `th` / `td` selectors.
3660
3723
 
3661
3724
  States: `loading` and `empty` (the table); `selected`, `highlighted`,
3662
3725
  `expanded` and `disabled` resolved per row — a cell takes its row's states,
3663
- and a row's `select-icon` its `selected` / `disabled`; `sorted` /
3726
+ a row's `select-icon` its `selected` / `disabled`, and its `expand` its
3727
+ `selected` / `highlighted` / `expanded`; `sorted` /
3664
3728
  `sorted-desc` per header, on its cell and its `sort-icon`; `indeterminate` on
3665
3729
  the select-all glyph. **`disabled` means the row cannot be selected**
3666
3730
  (`selectable` rejects it, or the table is `disabled`): the row still clicks,
3667
- highlights and expands, and a table without selection never has it. Expand
3668
- rows, the empty row and the skeleton are not parts — `pt:row:expanded` and
3669
- `pt:body:empty` cover the usual asks.
3731
+ highlights and expands, and a table without selection never has it. The
3732
+ empty row and the skeleton are not parts — `pt:body:empty` covers the usual
3733
+ ask.
3670
3734
 
3671
3735
  The selectable invoices table above, restyled through those parts: muted
3672
3736
  type on `pt:head`, weight on `pt:row:selected` (Globex is checked), a muted
@@ -3843,8 +3907,9 @@ A value that depends on the ITEM — a danger wash on overdue rows — is a
3843
3907
  **per-item function** on `pt:row`: `({ item, highlighted }) => …`, called
3844
3908
  with the row's item and every state as a boolean for it, its result merged after the
3845
3909
  state entries (so the overdue wash below steps aside while the row is
3846
- highlighted). `rowClass` still works and is computed once per row rather than
3847
- per render; `pt:cell` takes no function (`tdClass` is the per-cell hook).
3910
+ highlighted). `pt:expand` takes one the same way. `rowClass` still works and
3911
+ is computed once per row rather than per render; `pt:cell` takes no function
3912
+ (`tdClass` is the per-cell hook).
3848
3913
 
3849
3914
  **pt:row as a function — overdue invoices washed by status**
3850
3915
 
@@ -4048,7 +4113,13 @@ for a preset that fits any row type (never `<any>`), not an imported
4048
4113
  - Set `item-value` so selection, expansion and highlight track rows by stable
4049
4114
  identity rather than object reference.
4050
4115
  - Sortable headers set `aria-sort` and render real buttons; custom header
4051
- slots should keep a button calling `toggleSort`.
4116
+ slots should keep a button calling `toggleSort`. Enter sorts by the column
4117
+ alone and **Shift+Enter adds it** to a multi-column sort. The sortable
4118
+ header's accessible description says so (on the sort button, or on the
4119
+ header cell when the header row is a keyboard surface, after the
4120
+ reorder / resize instructions); with `multi-sort` there is no hint, since a
4121
+ plain Enter adds. A custom `#<key>:header` button carries no hint: describe
4122
+ it yourself if it should.
4052
4123
  - **The role follows the keyboard.** A plain table is `role="table"` with
4053
4124
  `cell`s. `keyboard-navigation` makes it `role="grid"` (the APG data grid:
4054
4125
  one tab stop, cell focus, `gridcell`s, `aria-selected` on selectable rows
@@ -4105,7 +4176,7 @@ for a preset that fits any row type (never `<any>`), not an imported
4105
4176
 
4106
4177
  | Prop | Type | Default | Required | Description |
4107
4178
  | --- | --- | --- | --- | --- |
4108
- | `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`, `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>`. |
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>`. |
4109
4180
  | `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. |
4110
4181
  | `actionsText` | `string \| undefined` | | | Label used in the header of the actions column. |
4111
4182
  | `align` | `"left" \| "center" \| "right" \| undefined` | `"left"` | | Text alignment of the columns. |
@@ -4142,6 +4213,7 @@ for a preset that fits any row type (never `<any>`), not an imported
4142
4213
  | `max` | `number \| undefined` | `Infinity` | | Maximum number of items that can be selected. |
4143
4214
  | `modelValue` | `any` | `[]` | | Used by v-model. Can be any serializable type. |
4144
4215
  | `multiple` | `boolean \| undefined` | `true` | | Allows the selection of multiple items. |
4216
+ | `multiSort` | `boolean \| undefined` | `false` | | A plain click on a sortable header adds the column to the sort, as Shift+click always does. Off, a plain click makes the sort that column alone. |
4145
4217
  | `name` | `string \| undefined` | | | Defines the name of the input. |
4146
4218
  | `order` | `string[] \| undefined` | `[]` | | Used by `v-model:order`: the column keys in render order. Columns it omits follow in declaration order; unknown keys stay put. A `reorderable` move writes it back complete. Works unbound, like `sort`; never holds `select` / `actions`. |
4147
4219
  | `page` | `string \| number \| undefined` | | | Current page (from 1), for the rows' `aria-rowindex` and a `BbPagination` paired through `id`. Doesn't slice `items`: pass the current page's rows. Numeric strings are coerced. |
@@ -4177,8 +4249,9 @@ for a preset that fits any row type (never `<any>`), not an imported
4177
4249
  | `cellColumnHeader` | Every column header cell (`role="columnheader"`), the select and actions headers included — a broadcast resolved per header. Not a `<th>`: the grid is `div`s with ARIA roles. |
4178
4250
  | `sortIcon` | A sortable column's sort arrow, resolved per header. |
4179
4251
  | `body` | The body row group. |
4180
- | `row` | Every DATA row — a broadcast resolved per row. Expand rows and the skeleton are not parts. Also takes a per-item function, called with the row's raw item, once per rendered row: keep it cheap. |
4252
+ | `row` | Every DATA row — a broadcast resolved per row. The skeleton is not a part, and an expand row is styled through `expand`. Also takes a per-item function, called with the row's raw item, once per rendered row: keep it cheap. |
4181
4253
  | `cell` | Every data cell, the select and actions cells included — a broadcast resolved with its row's states (`role="cell"`, `gridcell` under `keyboardNavigation`) — and the cells of the `firstRow` / `lastRow`, with no row state. Takes no function: use `tdClass` for a per-cell hook. |
4254
+ | `expand` | The cell under an open row that holds the `#expand` slot and spans every column — resolved with its data row's `selected`, `highlighted` and `expanded` (always on: the cell exists only while its row is open). Not a `cell`, so `pt:cell` never reaches it: a rule taken off the cells (`pt:cell="border-b-0"`) needs `pt:expand="border-b-0"` too. Also takes a per-item function, called with the open row's raw item. |
4182
4255
  | `firstRow` | The row of `#<key>:first` cells, before the data rows. |
4183
4256
  | `lastRow` | The row of `#<key>:last` cells, after the data rows (a totals row: `pt:lastRow="font-semibold"`). |
4184
4257
  | `selectIcon` | The selection glyph: each row's checkbox (radio with single selection) and the select-all checkbox — resolved per node. Not rendered when the `select` / `select:header` slots replace them. |
@@ -4193,11 +4266,11 @@ States are listed in precedence order: when two are on at once and their entries
4193
4266
  | `empty` | Component-wide: there are no rows (`bb-table--empty`). |
4194
4267
  | `sorted` | Per node, on `cellColumnHeader` and `sortIcon`: the column is sorted, either direction (`bb-table__cell-column-header--sorted`). |
4195
4268
  | `sortedDesc` | Per node, on `cellColumnHeader` and `sortIcon`: sorted descending (`bb-table__cell-column-header--sorted-desc`). Ascending is `sorted` alone. |
4196
- | `selected` | Per node, on `row`, its cells and its `selectIcon`: a checked row; on the select-all glyph, every row is selected. |
4269
+ | `selected` | Per node, on `row`, its cells, its `expand` and its `selectIcon`: a checked row; on the select-all glyph, every row is selected. |
4197
4270
  | `indeterminate` | Per node, on the select-all `selectIcon`: some rows are selected, not all. |
4198
4271
  | `disabled` | Per node, on `row`, its cells and its `selectIcon`: the row cannot be SELECTED — `selectable` rejects it, or the table is `disabled` (`bb-table__row--disabled`). It still clicks, highlights and expands. Never on a table without selection. The select-all glyph takes it from the table's `disabled`. |
4199
- | `expanded` | Per node, on `row` and its cells: the row's expand row is open (`bb-table__row--expanded`). |
4200
- | `highlighted` | Per node, on `row` and its cells: the highlighted row. |
4272
+ | `expanded` | Per node, on `row`, its cells and its `expand`: the row's expand row is open (`bb-table__row--expanded`). |
4273
+ | `highlighted` | Per node, on `row`, its cells and its `expand`: the highlighted row. |
4201
4274
  | `loading` | Component-wide: the table is `loading`. |
4202
4275
 
4203
4276
  ## Events
@@ -4224,8 +4297,8 @@ States are listed in precedence order: when two are on at once and their entries
4224
4297
  - `actions:header` — scope: `{ text: string; }` — Replaces the default actions header cell content.
4225
4298
  - `empty` — scope: `{ error: unknown; }` — Content shown when there are no rows and nothing is loading, inside the table's own full-width cell: pass content, never a row. `error` is what the latest failed load threw (`null` when none).
4226
4299
  - `expand` — scope: `{ expanded: boolean; expandProps?: ExpandControllerProps \| undefined; toggleExpanded: () => void; highlighted?: boolean \| undefined; toggleHighlighted?: (() =…` — Full-width expandable content row rendered below each main row when expanded.
4227
- - `header:append` — scope: `BbTableHeaderAffixSlotProps<Item>` `{ classes?: Classes; columnKey: string; items?: Item[]; label?: string; sortable?: boolean; sortOrder?: "asc" \| "desc" \| null; toggleSort?: (() => void); hideColumn?: (() => void) }` — Content after every data column's header content (the label + sort control, or its own `<key>:header` replacement). A column's `<key>:header:append` wins over this for that column; they never stack. Branch on `columnKey` for per-column output.
4228
- - `header:prepend` — scope: `BbTableHeaderAffixSlotProps<Item>` `{ classes?: Classes; columnKey: string; items?: Item[]; label?: string; sortable?: boolean; sortOrder?: "asc" \| "desc" \| null; toggleSort?: (() => void); hideColumn?: (() => void) }` — Content before every data column's header content (the label + sort control, or its own `<key>:header` replacement). A column's `<key>:header:prepend` wins over this for that column; they never stack. Branch on `columnKey` for per-column output.
4300
+ - `header:append` — scope: `BbTableHeaderAffixSlotProps<Item>` `{ classes?: Classes; columnKey: string; items?: Item[]; label?: string; sortable?: boolean; sortOrder?: "asc" \| "desc" \| null; toggleSort?: ((event?: MouseEvent \| KeyboardEvent \| undefined) => void); hideColumn?: (() => void) }` — Content after every data column's header content (the label + sort control, or its own `<key>:header` replacement). A column's `<key>:header:append` wins over this for that column; they never stack. Branch on `columnKey` for per-column output.
4301
+ - `header:prepend` — scope: `BbTableHeaderAffixSlotProps<Item>` `{ classes?: Classes; columnKey: string; items?: Item[]; label?: string; sortable?: boolean; sortOrder?: "asc" \| "desc" \| null; toggleSort?: ((event?: MouseEvent \| KeyboardEvent \| undefined) => void); hideColumn?: (() => void) }` — Content before every data column's header content (the label + sort control, or its own `<key>:header` replacement). A column's `<key>:header:prepend` wins over this for that column; they never stack. Branch on `columnKey` for per-column output.
4229
4302
  - `loading` — scope: `{ items: Item[]; }` — Content shown while table rows are loading (replaces the skeleton rows).
4230
4303
  - `select` — scope: `{ item: Item; value: any; checked: boolean; disabled: boolean; inputName: string; readonly: boolean; toggleSelected?: (() => void) \| undefined; }` — Replaces the default checkbox/radio in the selection cell for each row.
4231
4304
  - `select:header` — scope: `{ multiple: boolean; selectAll: boolean; text: string; }` — Replaces the default selection header cell content (checkbox or radio label).
@@ -191,7 +191,8 @@ Three things worth internalising:
191
191
 
192
192
  #### Variant icons
193
193
 
194
- The object form of the plugin's `toastVariants` sets each variant's icon for
194
+ A default icon per variant, set once: the object form of the plugin's
195
+ `toastVariants` option sets each variant's icon for
195
196
  the whole app. A toast without its own `icon` uses its variant's; its own
196
197
  `icon` wins; `hideIcon: true` hides either (and does nothing when there is no
197
198
  icon). The loading spinner still shows with `hideIcon`: it is state, not
@@ -53,7 +53,7 @@
53
53
  ## Slots
54
54
 
55
55
  - `empty` — scope: `object` — Content shown when no options are available (replaces the default "no data" text).
56
- - `group` — scope: `GroupedListBoxGroupSlotProps<Item>` `{ text: string; item: Item; index: number; length: number; disabled: boolean }` — Replaces the default group header rendered above each group of options.
56
+ - `group` — scope: `GroupedListBoxGroupSlotProps<Item>` `{ text: string; item: Item; index: number; length: number; disabled: boolean }` — Replaces the default group header rendered above each group of options. A group whose `data` is `null`/`undefined` has no header, so no call.
57
57
  - `loading` — scope: `object` — Content shown while items are loading (replaces the default loading text).
58
58
  - `option` — scope: `GroupedListBoxOptionSlotProps<Item>` `{ disabled: boolean; focused: boolean; index: number; item: Item; selected: boolean; text: string; value: any; description: string }` — Replaces the default rendering of each option row within a group.
59
59
  - `options:append` — scope: `object` — Content appended inside the options list, after the last group.