@omnitend/dashboard-for-laravel 0.36.0 → 0.38.0

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.
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-07-21T12:50:16.789Z",
2
+ "generated": "2026-07-23T18:28:14.921Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.36.0"
5
+ "version": "0.38.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -211,8 +211,8 @@
211
211
  "name": "variant",
212
212
  "type": "union",
213
213
  "required": false,
214
- "default": "\"primary\"",
215
- "description": "The visual style variant of the button"
214
+ "default": "\"secondary\"",
215
+ "description": "The visual style variant of the button.\n\nDefaults to `'secondary'` (soft, non-emphatic). Under the soft-first\nhouse rule, `primary` is THE single emphatic action per page/modal, so a\nvariant-less button must NOT be emphatic by default — declare\n`variant=\"primary\"` explicitly where you mean the one loud action."
216
216
  },
217
217
  {
218
218
  "name": "size",
@@ -3092,6 +3092,12 @@
3092
3092
  "required": false,
3093
3093
  "default": "\"success\"",
3094
3094
  "description": "On-state colour. `\"success\"` (default) reads the house green-on / red-off\nstyle — an active/enabled/good switch (\"Product is current\", \"Stock is\ntracked\", \"Visible\"). `\"neutral\"` is for semantically-mixed switches\n(\"contains alcohol\", an allergen toggle, \"hidden on web shop\") that\nshouldn't imply good/bad — it uses the brand primary on / grey off (#158)."
3095
+ },
3096
+ {
3097
+ "name": "size",
3098
+ "type": "\"sm\"",
3099
+ "required": false,
3100
+ "description": "`\"sm\"` renders a compact box (~31px vs the default ~35px input height) for\ndense rows that repeat the switch per item (a per-line visibility toggle in\na modal). Omit for the standard size, which matches the `.form-control`\nheight so a switch lines up with inputs beside it."
3095
3101
  }
3096
3102
  ],
3097
3103
  "events": [
@@ -3473,6 +3479,12 @@
3473
3479
  "required": false,
3474
3480
  "default": "false",
3475
3481
  "description": "Apply `table-layout: fixed` to the underlying table. Off by default, so\nexisting tables keep their content-sized columns. When on, column widths\nstop depending on cell content, so they no longer reshuffle when a filter\nnarrows the rows — the table stays put as you filter (#156). Combine with\nper-field `width`/`minWidth` to control the proportions; columns with no\ndeclared width share the remaining space equally."
3482
+ },
3483
+ {
3484
+ "name": "primaryKey",
3485
+ "type": "string",
3486
+ "required": false,
3487
+ "description": "Field on each row whose value uniquely identifies it, forwarded to the\ninner bvn table's `primaryKey` (all three data modes). Without it bvn keys\nclient-side rows by their INDEX, which mis-associates per-row STATEFUL cell\ncomponents when a row is inserted/removed mid-interaction — a debounced\nedit in one row can end up saved against the wrong record after a\nconcurrent delete of a row above. Set it (e.g. `primary-key=\"id\"`) so bvn\nkeys rows by that field's value and each row keeps its identity across\ndata mutations. Non-breaking: omit it and rows key by index exactly as\nbefore."
3476
3488
  }
3477
3489
  ],
3478
3490
  "events": [
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-21T12:50:16.844Z
4
+ > Last updated: 2026-07-23T18:28:14.969Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -98,6 +98,18 @@ $btn-padding-x-lg: 1.5rem; // 24px
98
98
 
99
99
  $input-padding-y: 0.375rem; // compact fields (Bootstrap's default; 0.625rem read oversized)
100
100
  $input-padding-x: 1rem;
101
+
102
+ // Match the sm/lg INPUT vertical padding to the sm/lg BUTTON padding above
103
+ // (PO18). Bootstrap sizes every child of an `.input-group-sm`/`-lg` — buttons
104
+ // INCLUDED — with the INPUT's sized padding, so leaving these at Bootstrap's
105
+ // defaults (0.25rem / 0.5rem) made a sized input-group sit ~8px / ~4px shorter
106
+ // than a `.btn-sm`/`.btn-lg` beside it. Aligning them makes sized controls share
107
+ // one height by construction. NB: the BASE `$input-padding-y` (0.375rem) stays
108
+ // deliberately more compact than `$btn-padding-y` (0.625rem) — see the comment
109
+ // above ("0.625rem read oversized"); only the SIZED variants are equalised,
110
+ // because those are the ones whose padding Bootstrap leaks onto grouped buttons.
111
+ $input-padding-y-sm: 0.5rem; // == $btn-padding-y-sm (was Bootstrap's 0.25rem)
112
+ $input-padding-y-lg: 0.75rem; // == $btn-padding-y-lg (was Bootstrap's 0.5rem)
101
113
  $input-border-radius: $border-radius;
102
114
  $input-border-color: #d1d5db; // Gray-300
103
115
  $input-focus-border-color: $primary;
@@ -652,8 +664,52 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
652
664
  }
653
665
 
654
666
  // DXTable header titles: muted by default (#157) so the table's CONTENT is
655
- // the loud layer. Override to re-louden (e.g. `var(--bs-body-color)`).
656
- --dx-table-header-color: var(--bs-secondary-color);
667
+ // the loud layer. Lightened again for S5b — #157's `--bs-secondary-color`
668
+ // (rgba(33,37,41,.75), ~5.9:1 on white) still read near-black next to legacy
669
+ // omnitend's light-slate headers. `#7c8293` (legacy's `$text-secondary`) is
670
+ // the signed-off value (~3.6:1 on white — fine for the semibold/large header
671
+ // text it colours, below the 4.5:1 body-text bar). Still on the same token so
672
+ // a consumer can re-louden (e.g. `var(--bs-body-color)`).
673
+ --dx-table-header-color: #7c8293;
674
+ }
675
+
676
+ // ----------------------------------------------------------------------------
677
+ // Tables — header alignment + filter-row affordance
678
+ // ----------------------------------------------------------------------------
679
+
680
+ // B17: keep header labels vertically centred regardless of whether a column
681
+ // carries sort icons. Bootstrap sets `.table > thead { vertical-align: bottom }`,
682
+ // which floors a plain (non-sortable) column's label while a sortable
683
+ // neighbour's taller two-arrow sort stack holds ITS label higher — one header
684
+ // row, mixed baselines. Centring the whole thead makes vertical alignment
685
+ // independent of the sort indicator's presence. Scoped to `.table > thead` so a
686
+ // consumer who genuinely wants bottom-aligned headers can still opt back in on a
687
+ // specific table.
688
+ .table > thead {
689
+ vertical-align: middle;
690
+ }
691
+
692
+ // PL1: a magnifier glyph on DXTable's text/number filter inputs, so the filter
693
+ // row reads as "this searches" at a glance (legacy lt-table carried one).
694
+ //
695
+ // It MUST be a background-image, not a `::before`/`::after` — an `<input>` is a
696
+ // replaced element and cannot host pseudo-elements. The glyph is an inline
697
+ // bootstrap-icons "search" SVG data-URI, tinted `#7c8293` (the same muted slate
698
+ // as the header token above), sized and inset with `padding-left` so text
699
+ // clears it.
700
+ //
701
+ // Direct-child selector (`> th > input.form-control`) is deliberate: DXTable's
702
+ // text/number/date filters render a bare `.form-control` straight inside the
703
+ // `<th>`, whereas the `select` filter's DAutocomplete nests its input inside
704
+ // `.d-autocomplete .input-group .b-autocomplete-input-wrapper` — so it is not a
705
+ // direct child and correctly gets NO glyph (it has its own chevron affordance).
706
+ // `:not([type="date"])` further excludes the date filter (its own picker icon).
707
+ .filter-row > th > input.form-control:not([type="date"]) {
708
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%237c8293' viewBox='0 0 16 16'%3e%3cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3e%3c/svg%3e");
709
+ background-repeat: no-repeat;
710
+ background-position: left 0.65rem center;
711
+ background-size: 0.85em;
712
+ padding-left: 1.9rem;
657
713
  }
658
714
 
659
715
  // primary is near-black navy, so Bootstrap's darken-on-hover is imperceptible —
@@ -679,6 +735,35 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
679
735
  }
680
736
  }
681
737
 
738
+ // ----------------------------------------------------------------------------
739
+ // Dropdowns — chevron caret (B14 / PO5)
740
+ // ----------------------------------------------------------------------------
741
+ // Bootstrap's `.dropdown-toggle::after` is a CSS border-triangle that sits high
742
+ // against Poppins' line box and reads crude next to the rest of the theme.
743
+ // Replace it with the bootstrap-icons chevron glyph, optically centred with a
744
+ // little breathing room from the label. The font is already available: the
745
+ // theme `@import`s `bootstrap-icons/font/bootstrap-icons` (declaring the
746
+ // `@font-face`), and #77's post-build extraction ships that woff2 with the
747
+ // stylesheet — so this needs no new dependency and no data-URI. Values signed
748
+ // off after one padding iteration in the greendragon interim; the glyph metrics
749
+ // are tunable via the variables below.
750
+ $dx-dropdown-caret-size: 0.75em; // glyph size relative to the label
751
+ $dx-dropdown-caret-gap: 0.4em; // space between label and chevron
752
+ $dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
753
+ .dropdown-toggle::after {
754
+ border: none; // drop Bootstrap's border-triangle
755
+ font-family: "bootstrap-icons";
756
+ content: "\F282"; // bi-chevron-down
757
+ font-size: $dx-dropdown-caret-size;
758
+ line-height: 1;
759
+ vertical-align: $dx-dropdown-caret-shift;
760
+ margin-left: $dx-dropdown-caret-gap;
761
+ }
762
+ .dropup .dropdown-toggle::after {
763
+ border: none;
764
+ content: "\F286"; // bi-chevron-up
765
+ }
766
+
682
767
  // ----------------------------------------------------------------------------
683
768
  // Switches — green when ON, red when OFF, by default (#158)
684
769
  // ----------------------------------------------------------------------------
@@ -719,6 +804,21 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
719
804
  --dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral pill in both states
720
805
  }
721
806
 
807
+ // Optically centre the check/radio/switch box against its label's FIRST line.
808
+ // Bootstrap's `.25em` top margin is mathematically right for a 1em box in a
809
+ // 1.5em line box, but Poppins seats its glyphs low in the line box, so the box
810
+ // reads visibly HIGH beside the text (same optical-metrics issue as the B14
811
+ // dropdown caret). `0.325em` lands the box centre on Poppins' visual centre —
812
+ // judged against a rendered filmstrip (0.25/0.30/0.325/0.35em) with Poppins
813
+ // loaded, not from the maths. A top MARGIN (not flex-centring) is deliberate:
814
+ // it aligns to the first line, so a multi-line label keeps the box on line one
815
+ // rather than floating to the block's vertical centre. DXSwitch is unaffected —
816
+ // it flex-centres and zeroes this margin on its inner input at higher
817
+ // specificity (`.dx-switch :deep(.form-check-input) { margin: 0 }`).
818
+ .form-check-input {
819
+ margin-top: 0.325em;
820
+ }
821
+
722
822
  .form-switch .form-check-input {
723
823
  // OFF: light-red track, red thumb (left).
724
824
  background-color: var(--dx-switch-off-bg);
@@ -851,6 +951,11 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
851
951
  .b-autocomplete-content {
852
952
  min-width: max-content; // grow to the longest option (floors at the trigger width)
853
953
  max-width: min(90vw, 32rem); // but don't run away
954
+ // S3b.1: bvn caps this popup at 300px, showing ~7 of a ~30-option list behind
955
+ // an inner scrollbar — it "doesn't drop all the way down the page like a
956
+ // native select". Let it fall most of the way down the viewport (but never
957
+ // taller than 42rem) so the list reads like a real dropdown.
958
+ max-height: min(70vh, 42rem);
854
959
  }
855
960
 
856
961
  // ----------------------------------------------------------------------------
@@ -37,8 +37,13 @@ type LinkVariant =
37
37
 
38
38
  interface Props {
39
39
  /**
40
- * The visual style variant of the button
41
- * @default 'primary'
40
+ * The visual style variant of the button.
41
+ *
42
+ * Defaults to `'secondary'` (soft, non-emphatic). Under the soft-first
43
+ * house rule, `primary` is THE single emphatic action per page/modal, so a
44
+ * variant-less button must NOT be emphatic by default — declare
45
+ * `variant="primary"` explicitly where you mean the one loud action.
46
+ * @default 'secondary'
42
47
  */
43
48
  variant?: ButtonVariant | LinkVariant | null;
44
49
 
@@ -105,7 +110,7 @@ interface Props {
105
110
  }
106
111
 
107
112
  const props = withDefaults(defineProps<Props>(), {
108
- variant: "primary",
113
+ variant: "secondary",
109
114
  disabled: false,
110
115
  loading: false,
111
116
  block: false,
@@ -21,7 +21,7 @@
21
21
  `label` prop for simple text, or the default slot for richer label content.
22
22
  -->
23
23
  <template>
24
- <div class="dx-switch" :class="[`dx-switch--${onVariant}`, { 'dx-switch--on': isOn }]">
24
+ <div class="dx-switch" :class="[`dx-switch--${onVariant}`, { 'dx-switch--on': isOn, 'dx-switch--sm': size === 'sm' }]">
25
25
  <!-- For the neutral variant, tag the inner `.form-switch` with
26
26
  `switch-neutral` so the global theme restores grey-off / primary-on for
27
27
  the toggle itself (the box styling below handles the wrapper). -->
@@ -63,6 +63,13 @@ interface Props {
63
63
  * shouldn't imply good/bad — it uses the brand primary on / grey off (#158).
64
64
  */
65
65
  onVariant?: "success" | "neutral";
66
+ /**
67
+ * `"sm"` renders a compact box (~31px vs the default ~35px input height) for
68
+ * dense rows that repeat the switch per item (a per-line visibility toggle in
69
+ * a modal). Omit for the standard size, which matches the `.form-control`
70
+ * height so a switch lines up with inputs beside it.
71
+ */
72
+ size?: "sm";
66
73
  }
67
74
 
68
75
  const props = withDefaults(defineProps<Props>(), {
@@ -131,6 +138,17 @@ defineOptions({
131
138
  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
132
139
  }
133
140
 
141
+ /* Compact size (#… size="sm"): a shorter box for dense rows that repeat the
142
+ switch per item. `min-height: auto` drops the input-height floor so the box
143
+ shrinks to its padded content (~31px vs ~35px); the smaller font pulls the
144
+ label in to match. The fixed-rem toggle itself is unchanged — only the box
145
+ chrome tightens. */
146
+ .dx-switch--sm :deep(.form-check) {
147
+ font-size: 0.875rem;
148
+ min-height: auto;
149
+ padding: 0.25rem 0.625rem;
150
+ }
151
+
134
152
  /* Label on the left, toggle on the right (DOM order is input then label). */
135
153
  .dx-switch :deep(.form-check-label) {
136
154
  order: 1;
@@ -131,6 +131,23 @@
131
131
  @update:model-value="handleSelectFilterChange(field, $event)"
132
132
  />
133
133
 
134
+ <!-- Native Select Filter (S3b): a plain <select> for consumers who
135
+ want OS-native behaviour (full-height menu, native keyboard/scroll)
136
+ instead of the DAutocomplete typeahead. Shares the select filter's
137
+ option list (getFieldFilterOptions) and change handler
138
+ (handleSelectFilterChange) EXACTLY — so it behaves identically to
139
+ filter:"select" in every way but the control. Single-select only:
140
+ `filterMultiple` is ignored here. The model maps "no filter" to the
141
+ FILTER_ALL_VALUE sentinel (see nativeSelectFilterValue) so the "All …"
142
+ option shows selected in a native control. -->
143
+ <DFormSelect
144
+ v-else-if="field.filter === 'select-native'"
145
+ :model-value="nativeSelectFilterValue(field)"
146
+ :options="getFieldFilterOptions(field) as unknown as Record<string, unknown>[]"
147
+ size="sm"
148
+ @update:model-value="handleSelectFilterChange(field, $event)"
149
+ />
150
+
134
151
  <!-- Number Filter -->
135
152
  <DFormInput
136
153
  v-else-if="field.filter === 'number'"
@@ -310,11 +327,17 @@ import DXTableShell from "./DXTableShell.vue";
310
327
  import DSpinner from "../base/DSpinner.vue";
311
328
  import DTable from "../base/DTable.vue";
312
329
  import DFormInput from "../base/DFormInput.vue";
330
+ import DFormSelect from "../base/DFormSelect.vue";
313
331
  import DAutocomplete from "../base/DAutocomplete.vue";
314
332
  import DButton from "../base/DButton.vue";
315
333
  import DXTablePagination from "./DXTablePagination.vue";
316
334
  import DXTableEditorModal from "./DXTableEditorModal.vue";
317
- export type FilterType = 'text' | 'select' | 'number' | 'date' | false;
335
+ // 'select' — DAutocomplete typeahead (browse-on-focus + type-to-narrow).
336
+ // 'select-native' — a plain native <select> (DFormSelect). Same options, value
337
+ // semantics and client-side matching as 'select'; differs ONLY in the control,
338
+ // for consumers who want OS-native menu behaviour (full-height menu, native
339
+ // keyboard/scroll). Single-select only — `filterMultiple` is ignored for it.
340
+ export type FilterType = 'text' | 'select' | 'select-native' | 'number' | 'date' | false;
318
341
 
319
342
  export interface FilterOption {
320
343
  value: string;
@@ -384,6 +407,12 @@ export interface TableField {
384
407
  filterAllText?: string;
385
408
  sortable?: boolean;
386
409
  hint?: string;
410
+ /**
411
+ * The inline filter control for this column, or `false`/omit for none. See
412
+ * `FilterType`. `'select'` (typeahead) and `'select-native'` (plain
413
+ * `<select>`) share the same options and value semantics — pick the native
414
+ * one when OS-native menu behaviour matters more than type-to-narrow.
415
+ */
387
416
  filter?: FilterType;
388
417
  filterOptions?: FilterOption[];
389
418
  filterPlaceholder?: string;
@@ -769,6 +798,19 @@ export interface Props<TItem = any> {
769
798
  * declared width share the remaining space equally.
770
799
  */
771
800
  fixedLayout?: boolean;
801
+
802
+ /**
803
+ * Field on each row whose value uniquely identifies it, forwarded to the
804
+ * inner bvn table's `primaryKey` (all three data modes). Without it bvn keys
805
+ * client-side rows by their INDEX, which mis-associates per-row STATEFUL cell
806
+ * components when a row is inserted/removed mid-interaction — a debounced
807
+ * edit in one row can end up saved against the wrong record after a
808
+ * concurrent delete of a row above. Set it (e.g. `primary-key="id"`) so bvn
809
+ * keys rows by that field's value and each row keeps its identity across
810
+ * data mutations. Non-breaking: omit it and rows key by index exactly as
811
+ * before.
812
+ */
813
+ primaryKey?: string;
772
814
  }
773
815
 
774
816
  const props = withDefaults(defineProps<Props<T>>(), {
@@ -1102,7 +1144,9 @@ const clientSideFilteredItems = computed<T[]>(() => {
1102
1144
  // Case-insensitive contains search
1103
1145
  return String(itemValue).toLowerCase().includes(filterValue);
1104
1146
 
1147
+ // Both select controls match identically (exact string match).
1105
1148
  case 'select':
1149
+ case 'select-native':
1106
1150
  // Exact match
1107
1151
  return String(itemValue) === candidate;
1108
1152
 
@@ -1272,6 +1316,15 @@ const fieldsHaveWidths = computed(() => props.fields.some(hasDeclaredWidth));
1272
1316
  // key: a display column can filter on a different server param via `filterKey`.
1273
1317
  const filterKeyFor = (field: TableField): string => field.filterKey ?? field.key;
1274
1318
 
1319
+ // Both select-style filters — 'select' (DAutocomplete typeahead) and
1320
+ // 'select-native' (plain <select>) — share the SAME option list, value
1321
+ // semantics and client-side matching; they differ only in the rendered control.
1322
+ // This predicate keeps the two in lockstep everywhere the option-derivation and
1323
+ // server-value logic runs, so a `select-native` column gets identical options to
1324
+ // what `select` would offer.
1325
+ const isSelectFilter = (field: TableField): boolean =>
1326
+ field.filter === 'select' || field.filter === 'select-native';
1327
+
1275
1328
  // The value that means "has no value" for a select filter (#106).
1276
1329
  const filterNullValueFor = (field: TableField): string => field.filterNullValue ?? 'null';
1277
1330
 
@@ -1286,6 +1339,17 @@ const handleSelectFilterChange = (field: TableField, value: unknown) => {
1286
1339
  handleFilterChange(filterKeyFor(field), selected === FILTER_ALL_VALUE ? '' : selected);
1287
1340
  };
1288
1341
 
1342
+ // The model value for a `select-native` control. Unlike DAutocomplete (which
1343
+ // falls back to '' and shows its placeholder), a native <select> selects its
1344
+ // FIRST option when the bound value matches none — which would desync the
1345
+ // control from the model. So the "no filter" state must map to FILTER_ALL_VALUE,
1346
+ // the value carried by the "All …" option, so that option shows as selected.
1347
+ // This is the read-side mirror of the write-side translation
1348
+ // `handleSelectFilterChange` already does (FILTER_ALL_VALUE → '') — the sentinel
1349
+ // is handled in one conceptual place, not reinvented.
1350
+ const nativeSelectFilterValue = (field: TableField): string =>
1351
+ (effectiveFilters.value[filterKeyFor(field)] as string) || FILTER_ALL_VALUE;
1352
+
1289
1353
  // Multi-value select filter (#51): the autocomplete's array model maps
1290
1354
  // straight onto the filters entry; an emptied selection removes the entry.
1291
1355
  const multiFilterValueFor = (field: TableField): string[] => {
@@ -1361,7 +1425,7 @@ const derivedFilterOptions = computed<Record<string, FilterOption[]>>(() => {
1361
1425
  const optionsByFieldKey: Record<string, FilterOption[]> = {};
1362
1426
 
1363
1427
  for (const field of props.fields) {
1364
- if (field.filter !== 'select') continue;
1428
+ if (!isSelectFilter(field)) continue;
1365
1429
  if (field.deriveFilterOptions === false) continue;
1366
1430
  // Explicit options win outright (see `getFieldFilterOptions`), so don't
1367
1431
  // spend a pass over every row producing a list nothing will read.
@@ -1434,8 +1498,10 @@ const getFieldFilterOptions = (field: TableField): FilterOption[] => {
1434
1498
  //
1435
1499
  // A `filterMultiple` column omits it (#51): "no filter" inside a
1436
1500
  // multi-select reads as a selectable value, and clearing is already
1437
- // discoverable there — remove the chips, or the ✕.
1438
- if (!field.filterMultiple) {
1501
+ // discoverable there — remove the chips, or the ✕. `select-native` is the
1502
+ // exception: it is ALWAYS single-select (it ignores `filterMultiple`), so it
1503
+ // still needs the "All" reset option even when `filterMultiple` is set.
1504
+ if (!field.filterMultiple || field.filter === 'select-native') {
1439
1505
  options.push({ value: FILTER_ALL_VALUE, text: filterAllLabelFor(field) });
1440
1506
  }
1441
1507
 
@@ -1750,7 +1816,7 @@ const shouldShowPerPageSelector = computed(() => {
1750
1816
  // mutually exclusive, so a client-side derived column can never appear here.
1751
1817
  const fieldsNeedingFilterValues = computed(() => {
1752
1818
  return props.fields
1753
- .filter(field => field.filter === 'select' && (!field.filterOptions || field.filterOptions.length === 0))
1819
+ .filter(field => isSelectFilter(field) && (!field.filterOptions || field.filterOptions.length === 0))
1754
1820
  .map(field => filterKeyFor(field));
1755
1821
  });
1756
1822
 
@@ -2243,6 +2309,13 @@ const tablePassthroughProps = computed(() => ({
2243
2309
  ? `No ${pluralItemName.value} match your filters`
2244
2310
  : `No ${pluralItemName.value} found`),
2245
2311
  expandedItems: props.expandedItems,
2312
+ // Row identity (#B16). Forwarded here rather than in `tableModeBindings`
2313
+ // because it is mode-INDEPENDENT — it must key rows by value in provider,
2314
+ // client-side AND inertia mode alike. `undefined` when unset, so bvn falls
2315
+ // back to index keys (its default) and nothing changes for tables that don't
2316
+ // opt in. Reaches BTable through DTable's `v-bind="$attrs"` (its prop is
2317
+ // `primaryKey`); DTable needs no change.
2318
+ primaryKey: props.primaryKey,
2246
2319
  }));
2247
2320
  // Computed: Singular and plural item names
2248
2321
  const singularItemName = computed(() => props.itemName);