@omnitend/dashboard-for-laravel 0.41.0 → 0.41.1

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-08-27T14:46:45.385Z",
2
+ "generated": "2026-09-27T10:14:02.393Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.41.0"
5
+ "version": "0.41.1"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -246,6 +246,13 @@
246
246
  "required": false,
247
247
  "description": "Leading Bootstrap Icons glyph name, rendered as `<i class=\"bi bi-{icon}\">`.\ne.g. `icon=\"save\"`. Hidden while the loading spinner is visible.\n\nDiverges from bvn: `BButton`'s native `icon` is a `boolean` (icon-button\nstyling mode). We redefine it as a glyph-name `string`, so bvn's boolean\nicon mode is unreachable through `DButton`. See DIVERGENCES.md."
248
248
  },
249
+ {
250
+ "name": "iconOnly",
251
+ "type": "boolean",
252
+ "required": false,
253
+ "default": "false",
254
+ "description": "Square icon control at the same height as a normal button of this size.\nUse `icon` or an icon in the default slot, and provide `aria-label`.\nTakes precedence over `block`. While loading, the spinner replaces the\nicon/slot without changing dimensions; `loadingText` is not shown."
255
+ },
249
256
  {
250
257
  "name": "block",
251
258
  "type": "boolean",
@@ -3072,7 +3079,7 @@
3072
3079
  "name": "DXSwitch",
3073
3080
  "category": "extended",
3074
3081
  "filePath": "resources/js/components/extended/DXSwitch.vue",
3075
- "description": "DXSwitch — a \"filled box\" toggle switch. An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized like a `.form-control` with the label on the left and the toggle on the right, colour-coding its state. By default (`onVariant=\"success\"`) it reads the house green-on / red-off style — a neutral box + red toggle when off, a soft-green box + green toggle when on. `onVariant=\"neutral\"` (brand primary on / grey off) is for semantically-mixed switches that shouldn't imply good/bad (#158). Used standalone or, internally, by `DXField`'s `switch` field type so a form switch and a lone switch share one look. Usage: ```vue <DXSwitch v-model=\"isCurrent\" label=\"Product is current\" /> ``` For a form field, prefer a `DXField`/`DXForm` `type: 'switch'`; reach for `DXSwitch` directly when you want the styled toggle outside a form. Pass a `label` prop for simple text, or the default slot for richer label content.",
3082
+ "description": "DXSwitch — a \"filled box\" toggle switch. An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized like a `.form-control` with the label on the left and the toggle on the right, colour-coding its state. By default (`onVariant=\"success\"`) it reads the house green-on / red-off style: a light-red box with a grey toggle when off, a soft-green box with a muted green-grey toggle when on. `onVariant=\"neutral\"` (brand primary on / grey off) is for semantically-mixed switches that shouldn't imply good/bad (#158). Used standalone or, internally, by `DXField`'s `switch` field type so a form switch and a lone switch share one look. Usage: ```vue <DXSwitch v-model=\"isCurrent\" label=\"Product is current\" /> ``` For a form field, prefer a `DXField`/`DXForm` `type: 'switch'`; reach for `DXSwitch` directly when you want the styled toggle outside a form. Pass a `label` prop for simple text, or the default slot for richer label content.",
3076
3083
  "props": [
3077
3084
  {
3078
3085
  "name": "modelValue",
@@ -3596,7 +3603,7 @@
3596
3603
  },
3597
3604
  {
3598
3605
  "name": "thead-top",
3599
- "description": "A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.",
3606
+ "description": "Rows above the column headers, such as grouped-column banners or pinned totals. Give it `<tr>`s.",
3600
3607
  "bindings": [
3601
3608
  {
3602
3609
  "title": "binding",
@@ -3616,6 +3623,20 @@
3616
3623
  }
3617
3624
  ]
3618
3625
  },
3626
+ {
3627
+ "name": "thead-sub",
3628
+ "description": "",
3629
+ "bindings": [
3630
+ {
3631
+ "name": "field",
3632
+ "title": "binding"
3633
+ },
3634
+ {
3635
+ "name": "v-bind",
3636
+ "title": "binding"
3637
+ }
3638
+ ]
3639
+ },
3619
3640
  {
3620
3641
  "name": "`head-end(${field.key})`",
3621
3642
  "description": "head-end(<fieldKey>) Additive content at the end of a column's own header — a period total above a numeric column, a small badge — keeping DXTable's sort indicator and field hint. Not a `head()` override.",
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-08-27T14:46:45.443Z
4
+ > Last updated: 2026-09-27T10:14:02.445Z
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.41.0",
3
+ "version": "0.41.1",
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",
@@ -100,20 +100,12 @@ $btn-padding-x-lg: 1.5rem; // 24px
100
100
  // Forms
101
101
  // ----------------------------------------------------------------------------
102
102
 
103
- $input-padding-y: 0.375rem; // compact fields (Bootstrap's default; 0.625rem read oversized)
103
+ // Inputs, selects, input-group labels and buttons share one height at each
104
+ // size. Otherwise adding a button stretches a default input group by 8px.
105
+ $input-padding-y: $btn-padding-y;
104
106
  $input-padding-x: 1rem;
105
-
106
- // Match the sm/lg INPUT vertical padding to the sm/lg BUTTON padding above
107
- // (PO18). Bootstrap sizes every child of an `.input-group-sm`/`-lg` — buttons
108
- // INCLUDED — with the INPUT's sized padding, so leaving these at Bootstrap's
109
- // defaults (0.25rem / 0.5rem) made a sized input-group sit ~8px / ~4px shorter
110
- // than a `.btn-sm`/`.btn-lg` beside it. Aligning them makes sized controls share
111
- // one height by construction. NB: the BASE `$input-padding-y` (0.375rem) stays
112
- // deliberately more compact than `$btn-padding-y` (0.625rem) — see the comment
113
- // above ("0.625rem read oversized"); only the SIZED variants are equalised,
114
- // because those are the ones whose padding Bootstrap leaks onto grouped buttons.
115
- $input-padding-y-sm: 0.5rem; // == $btn-padding-y-sm (was Bootstrap's 0.25rem)
116
- $input-padding-y-lg: 0.75rem; // == $btn-padding-y-lg (was Bootstrap's 0.5rem)
107
+ $input-padding-y-sm: $btn-padding-y-sm;
108
+ $input-padding-y-lg: $btn-padding-y-lg;
117
109
  $input-border-radius: $border-radius;
118
110
  $input-border-color: #d1d5db; // Gray-300
119
111
  $input-focus-border-color: $primary;
@@ -288,6 +280,7 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
288
280
  // and expose it as a var so such components can reference the single source
289
281
  // of truth instead of duplicating the formula.
290
282
  :root {
283
+ --d-input-border-color: #{$input-border-color};
291
284
  // $input-border-width resolves to a CSS var() (Bootstrap 5.3+), which Sass
292
285
  // can't multiply directly — fold the *2 into the calc() itself instead.
293
286
  --dx-input-height: calc(
@@ -295,6 +288,16 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
295
288
  );
296
289
  }
297
290
 
291
+ // Bootstrap sizes grouped buttons with padding properties, but leaves their
292
+ // padding variables unchanged. Keep computed icon-only dimensions in sync.
293
+ .input-group-sm > .btn {
294
+ --bs-btn-padding-y: #{$input-padding-y-sm};
295
+ }
296
+
297
+ .input-group-lg > .btn {
298
+ --bs-btn-padding-y: #{$input-padding-y-lg};
299
+ }
300
+
298
301
  // Horizontal ("label-left") forms: a compact settings-form rhythm, with the
299
302
  // label + control top-aligned so a checkbox/switch sits on the label's first
300
303
  // line rather than floating above it (the label column carries a top offset to
@@ -305,12 +308,8 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
305
308
  align-items: flex-start;
306
309
  }
307
310
 
308
- .col-form-label {
309
- padding-top: 0.4rem;
310
- }
311
-
312
311
  .form-check {
313
- margin-top: 0.4rem;
312
+ margin-top: calc(#{$input-padding-y} + #{$input-border-width});
314
313
  }
315
314
 
316
315
  .mb-3 {
@@ -689,6 +688,12 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
689
688
  // independent of the sort indicator's presence. Scoped to `.table > thead` so a
690
689
  // consumer who genuinely wants bottom-aligned headers can still opt back in on a
691
690
  // specific table.
691
+ // Body cells inherit this alignment; explicit .align-top remains available
692
+ // on a table, row or cell for long-form content that needs top alignment.
693
+ .table {
694
+ vertical-align: middle;
695
+ }
696
+
692
697
  .table > thead {
693
698
  vertical-align: middle;
694
699
  }
@@ -702,13 +707,9 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
702
707
  // as the header token above), sized and inset with `padding-left` so text
703
708
  // clears it.
704
709
  //
705
- // Direct-child selector (`> th > input.form-control`) is deliberate: DXTable's
706
- // text/number/date filters render a bare `.form-control` straight inside the
707
- // `<th>`, whereas the `select` filter's DAutocomplete nests its input inside
708
- // `.d-autocomplete .input-group .b-autocomplete-input-wrapper` — so it is not a
709
- // direct child and correctly gets NO glyph (it has its own chevron affordance).
710
- // `:not([type="date"])` further excludes the date filter (its own picker icon).
711
- .filter-row > th > input.form-control:not([type="date"]) {
710
+ // Select filters keep their chevron without a second icon or extra text inset.
711
+ // Date inputs keep their own picker affordance.
712
+ .dx-table-filter-cell > input.form-control:not([type="date"]) {
712
713
  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");
713
714
  background-repeat: no-repeat;
714
715
  background-position: left 0.65rem center;
@@ -805,7 +806,8 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
805
806
  --dx-switch-off-well: #{color.mix($dx-switch-off, #fff, 10%)};
806
807
  --dx-switch-off-line: #{color.mix($dx-switch-off, #fff, 45%)};
807
808
  --dx-switch-off-ink: #{color.mix($dx-switch-off, #000, 30%)};
808
- --dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral pill in both states
809
+ --dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral OFF pill
810
+ --dx-switch-on-pill: #{color.mix(map.get(map.get($dx-variants, "success"), "soft-text"), $gray-400, 50%)};
809
811
  }
810
812
 
811
813
  // Optically centre the check/radio/switch box against its label's FIRST line.
@@ -1046,3 +1048,8 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
1046
1048
  margin-left: 0;
1047
1049
  }
1048
1050
  }
1051
+
1052
+ // Keep the compact filter-cell spacing when BTable supplies the subheader row.
1053
+ thead tr:has(.dx-table-filter-cell) > td {
1054
+ padding: 0.5rem;
1055
+ }
@@ -203,13 +203,13 @@ const noClearButton = computed(
203
203
  */
204
204
  .d-autocomplete :deep(.b-autocomplete-trigger) {
205
205
  --bs-btn-bg: var(--bs-body-bg);
206
- --bs-btn-border-color: var(--bs-border-color);
206
+ --bs-btn-border-color: var(--d-input-border-color, var(--bs-border-color));
207
207
  --bs-btn-color: var(--bs-secondary-color);
208
208
  --bs-btn-hover-bg: var(--bs-tertiary-bg);
209
- --bs-btn-hover-border-color: var(--bs-border-color);
209
+ --bs-btn-hover-border-color: var(--d-input-border-color, var(--bs-border-color));
210
210
  --bs-btn-hover-color: var(--bs-body-color);
211
211
  --bs-btn-active-bg: var(--bs-secondary-bg);
212
- --bs-btn-active-border-color: var(--bs-border-color);
212
+ --bs-btn-active-border-color: var(--d-input-border-color, var(--bs-border-color));
213
213
  --bs-btn-active-color: var(--bs-body-color);
214
214
  flex: 0 0 auto;
215
215
  }
@@ -2,11 +2,12 @@
2
2
  /**
3
3
  * DButton - A type-safe wrapper around Bootstrap Vue Next's BButton component
4
4
  *
5
- * Adds three base-level button behaviours on top of `variant`/`size`:
5
+ * Adds base-level button behaviours on top of `variant`/`size`:
6
6
  * - `loading` — disables the button and shows a trailing spinner while an async
7
7
  * action is in flight, with anti-flash timing (no spinner for sub-`spinnerDelay`
8
8
  * actions; a `minSpinnerTime` floor once shown so it never strobes).
9
9
  * - `icon` — a leading Bootstrap Icons glyph (e.g. `icon="save"` → `bi bi-save`).
10
+ * - `iconOnly` — square icon control at the standard height for its size.
10
11
  * - `block` — full-width button (the BS5 replacement for the dropped `.btn-block`).
11
12
  *
12
13
  * @component
@@ -15,6 +16,7 @@
15
16
  * <DButton variant="primary" size="lg">Click Me</DButton>
16
17
  * <DButton icon="save" variant="primary">Save</DButton>
17
18
  * <DButton :loading="isSaving" loading-text="Saving…">Save</DButton>
19
+ * <DButton icon="trash" icon-only aria-label="Delete" />
18
20
  * <DButton block>Continue</DButton>
19
21
  * ```
20
22
  */
@@ -88,6 +90,15 @@ interface Props {
88
90
  */
89
91
  icon?: string;
90
92
 
93
+ /**
94
+ * Square icon control at the same height as a normal button of this size.
95
+ * Use `icon` or an icon in the default slot, and provide `aria-label`.
96
+ * Takes precedence over `block`. While loading, the spinner replaces the
97
+ * icon/slot without changing dimensions; `loadingText` is not shown.
98
+ * @default false
99
+ */
100
+ iconOnly?: boolean;
101
+
91
102
  /**
92
103
  * Full-width button (BS5 `w-100` idiom, replacing the dropped `.btn-block`).
93
104
  * @default false
@@ -114,6 +125,7 @@ const props = withDefaults(defineProps<Props>(), {
114
125
  disabled: false,
115
126
  loading: false,
116
127
  block: false,
128
+ iconOnly: false,
117
129
  spinnerDelay: 500,
118
130
  minSpinnerTime: 100,
119
131
  });
@@ -189,7 +201,7 @@ const passthroughSlotNames = computed(() =>
189
201
  // `:deep()`-on-a-B-root trap. Only add the flex row when there's an adornment
190
202
  // to space out.
191
203
  const hasAdornment = computed(
192
- () => Boolean(props.icon) || showSpinner.value,
204
+ () => props.iconOnly || Boolean(props.icon) || showSpinner.value,
193
205
  );
194
206
  </script>
195
207
 
@@ -201,7 +213,7 @@ const hasAdornment = computed(
201
213
  :aria-busy="loading || showSpinner || undefined"
202
214
  :class="[
203
215
  { 'd-inline-flex align-items-center justify-content-center gap-2': hasAdornment },
204
- { 'w-100': block },
216
+ { 'w-100': block && !iconOnly, 'd-button--icon-only': iconOnly },
205
217
  ]"
206
218
  v-bind="$attrs"
207
219
  >
@@ -210,8 +222,8 @@ const hasAdornment = computed(
210
222
  :class="['bi', `bi-${icon}`]"
211
223
  aria-hidden="true"
212
224
  ></i>
213
- <template v-if="showSpinner && loadingText">{{ loadingText }}</template>
214
- <slot v-else />
225
+ <template v-if="showSpinner && loadingText && !iconOnly">{{ loadingText }}</template>
226
+ <slot v-else-if="!iconOnly || !showSpinner" />
215
227
  <DSpinner v-if="showSpinner" small />
216
228
 
217
229
  <!-- Forward any other named slots untouched, with their slot props. -->
@@ -224,3 +236,25 @@ const hasAdornment = computed(
224
236
  </template>
225
237
  </BButton>
226
238
  </template>
239
+
240
+ <style scoped>
241
+ .d-button--icon-only {
242
+ --d-button-icon-size: calc(
243
+ 1em * var(--bs-btn-line-height) + 2 * var(--bs-btn-padding-y) +
244
+ 2 * var(--bs-btn-border-width)
245
+ );
246
+ width: var(--d-button-icon-size);
247
+ height: var(--d-button-icon-size);
248
+ padding: 0;
249
+ flex-shrink: 0;
250
+ }
251
+
252
+ .input-group > .d-button--icon-only {
253
+ padding: 0;
254
+ }
255
+
256
+ .d-button--icon-only > .bi {
257
+ font-size: 1.25em;
258
+ line-height: 1;
259
+ }
260
+ </style>
@@ -5,8 +5,8 @@
5
5
  An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized
6
6
  like a `.form-control` with the label on the left and the toggle on the right,
7
7
  colour-coding its state. By default (`onVariant="success"`) it reads the house
8
- green-on / red-off style — a neutral box + red toggle when off, a soft-green
9
- box + green toggle when on. `onVariant="neutral"` (brand primary on / grey off)
8
+ green-on / red-off style: a light-red box with a grey toggle when off, a
9
+ soft-green box with a muted green-grey toggle when on. `onVariant="neutral"` (brand primary on / grey off)
10
10
  is for semantically-mixed switches that shouldn't imply good/bad (#158). Used
11
11
  standalone or, internally, by `DXField`'s `switch` field type so a form switch
12
12
  and a lone switch share one look.
@@ -168,8 +168,8 @@ defineOptions({
168
168
  cursor: pointer;
169
169
  }
170
170
 
171
- /* Filled box (#158): the WHOLE control carries the colour and the pill stays a
172
- neutral grey affordance — the box IS the state, so it reads strongest at a
171
+ /* Filled box (#158): the WHOLE control carries the colour and the pill uses a
172
+ muted state tint — the box IS the state, so it reads strongest at a
173
173
  glance (the house style). `success` (default) is green ON / light-red OFF;
174
174
  `neutral` keeps a brand primary panel for mixed-semantics switches. Pair with
175
175
  `textWhenTrue`/`textWhenFalse` so the LABEL also names each state. */
@@ -192,9 +192,12 @@ defineOptions({
192
192
  font-weight: 500;
193
193
  }
194
194
 
195
- /* Neutral grey pill in BOTH states (white thumb, sliding) — overrides the global
196
- green-on/red-off toggle colours within the box, so only the box carries the
197
- state. Explicit `:checked`/`:focus` selectors so they out-specify the global
195
+ .dx-switch--success.dx-switch--on {
196
+ --dx-switch-pill: var(--dx-switch-on-pill);
197
+ }
198
+
199
+ /* Muted pill with a white sliding thumb: green-grey ON, neutral grey OFF.
200
+ The stronger box colour and position continue to communicate the state. Explicit `:checked`/`:focus` selectors so they out-specify the global
198
201
  `.form-switch .form-check-input:checked` rule. */
199
202
  .dx-switch--success :deep(.form-check-input),
200
203
  .dx-switch--success :deep(.form-check-input:checked),
@@ -72,8 +72,7 @@
72
72
  supplied their own `table-colgroup` (forwarded generically) —
73
73
  so a table with no widths gets no colgroup and renders exactly
74
74
  as before. `<col>` widths are authoritative under
75
- `table-layout: fixed`, where the filter row (not the header)
76
- is the width-determining first row. -->
75
+ `table-layout: fixed`, regardless of any consumer header banners. -->
77
76
  <template
78
77
  v-if="fieldsHaveWidths && !$slots['table-colgroup']"
79
78
  #table-colgroup
@@ -85,92 +84,90 @@
85
84
  />
86
85
  </template>
87
86
 
88
- <!-- DXTable owns `thead-top` (its filter row lives there), so a
89
- consumer's own thead-top content is COMPOSED above it rather
90
- than being dropped: a grouped column-header banner or a pinned
91
- totals row sits above the headers where it belongs (#120). -->
92
- <template v-if="hasFilters || $slots['thead-top']" #thead-top="theadScope">
87
+ <template v-if="$slots['thead-top']" #thead-top="theadScope">
93
88
  <!--
94
- @slot A row rendered ABOVE the column headers — a grouped-column banner (a `<th colspan>` spanning several columns), or a pinned totals row. Renders above DXTable's own filter row. Give it `<tr>`s.
89
+ @slot Rows above the column headers, such as grouped-column banners or pinned totals. Give it `<tr>`s.
95
90
  @binding {object} columns The number of columns in the table.
96
91
  @binding {object} fields The table's fields.
97
92
  -->
98
93
  <slot name="thead-top" v-bind="theadScope" />
94
+ </template>
99
95
 
100
- <tr v-if="hasFilters" class="filter-row">
101
- <th v-for="field in fields" :key="`filter-${field.key}`" class="p-2">
102
- <!-- Text Filter -->
103
- <DFormInput
104
- v-if="field.filter === 'text'"
105
- :model-value="effectiveFilters[filterKeyFor(field)] || ''"
106
- :placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
107
- size="sm"
108
- @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
109
- />
110
-
111
- <!-- Multi-value Select Filter (#51): several values at once; the filters entry is an array -->
112
- <DAutocomplete
113
- v-else-if="field.filter === 'select' && field.filterMultiple"
114
- :model-value="multiFilterValueFor(field)"
115
- :options="getFieldFilterOptions(field)"
116
- :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
117
- size="sm"
118
- multiple
119
- open-on-focus
120
- @update:model-value="handleMultiSelectFilterChange(field, $event)"
121
- />
122
-
123
- <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
124
- <DAutocomplete
125
- v-else-if="field.filter === 'select'"
126
- :model-value="(effectiveFilters[filterKeyFor(field)] as string) || ''"
127
- :options="getFieldFilterOptions(field)"
128
- :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
129
- size="sm"
130
- open-on-focus
131
- @update:model-value="handleSelectFilterChange(field, $event)"
132
- />
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
-
151
- <!-- Number Filter -->
152
- <DFormInput
153
- v-else-if="field.filter === 'number'"
154
- :model-value="effectiveFilters[filterKeyFor(field)] || ''"
155
- :placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
156
- type="number"
157
- size="sm"
158
- @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
159
- />
160
-
161
- <!-- Date Filter -->
162
- <DFormInput
163
- v-else-if="field.filter === 'date'"
164
- :model-value="effectiveFilters[filterKeyFor(field)] || ''"
165
- type="date"
166
- size="sm"
167
- @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
168
- />
96
+ <!-- The table supplies the below-header row and one cell per field. -->
97
+ <template v-if="hasFilters || $slots['thead-sub']" #thead-sub="{ field, ...filterScope }">
98
+ <div v-if="hasFilters" class="dx-table-filter-cell">
99
+ <!-- Text Filter -->
100
+ <DFormInput
101
+ v-if="field.filter === 'text'"
102
+ :model-value="effectiveFilters[filterKeyFor(field)] || ''"
103
+ :placeholder="field.filterPlaceholder || `Search ${field.label || field.key}...`"
104
+ size="sm"
105
+ @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
106
+ />
107
+
108
+ <!-- Multi-value Select Filter (#51): several values at once; the filters entry is an array -->
109
+ <DAutocomplete
110
+ v-else-if="field.filter === 'select' && field.filterMultiple"
111
+ :model-value="multiFilterValueFor(field)"
112
+ :options="getFieldFilterOptions(field)"
113
+ :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
114
+ size="sm"
115
+ multiple
116
+ open-on-focus
117
+ @update:model-value="handleMultiSelectFilterChange(field, $event)"
118
+ />
119
+
120
+ <!-- Select Filter: typeahead — browse the full list on focus, or type to narrow; clear (✕) resets to "no filter" -->
121
+ <DAutocomplete
122
+ v-else-if="field.filter === 'select'"
123
+ :model-value="(effectiveFilters[filterKeyFor(field)] as string) || ''"
124
+ :options="getFieldFilterOptions(field)"
125
+ :placeholder="field.filterPlaceholder || filterAllLabelFor(field)"
126
+ size="sm"
127
+ open-on-focus
128
+ @update:model-value="handleSelectFilterChange(field, $event)"
129
+ />
130
+
131
+ <!-- Native Select Filter (S3b): a plain <select> for consumers who
132
+ want OS-native behaviour (full-height menu, native keyboard/scroll)
133
+ instead of the DAutocomplete typeahead. Shares the select filter's
134
+ option list (getFieldFilterOptions) and change handler
135
+ (handleSelectFilterChange) EXACTLY — so it behaves identically to
136
+ filter:"select" in every way but the control. Single-select only:
137
+ `filterMultiple` is ignored here. The model maps "no filter" to the
138
+ FILTER_ALL_VALUE sentinel (see nativeSelectFilterValue) so the "All …"
139
+ option shows selected in a native control. -->
140
+ <DFormSelect
141
+ v-else-if="field.filter === 'select-native'"
142
+ :model-value="nativeSelectFilterValue(field)"
143
+ :options="getFieldFilterOptions(field) as unknown as Record<string, unknown>[]"
144
+ size="sm"
145
+ @update:model-value="handleSelectFilterChange(field, $event)"
146
+ />
147
+
148
+ <!-- Number Filter -->
149
+ <DFormInput
150
+ v-else-if="field.filter === 'number'"
151
+ :model-value="effectiveFilters[filterKeyFor(field)] || ''"
152
+ :placeholder="field.filterPlaceholder || `Filter ${field.label || field.key}...`"
153
+ type="number"
154
+ size="sm"
155
+ @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
156
+ />
157
+
158
+ <!-- Date Filter -->
159
+ <DFormInput
160
+ v-else-if="field.filter === 'date'"
161
+ :model-value="effectiveFilters[filterKeyFor(field)] || ''"
162
+ type="date"
163
+ size="sm"
164
+ @update:model-value="handleFilterChange(filterKeyFor(field), $event as string)"
165
+ />
169
166
 
170
- <!-- No filter for this column -->
171
- <div v-else></div>
172
- </th>
173
- </tr>
167
+ <!-- No filter for this column -->
168
+ <div v-else></div>
169
+ </div>
170
+ <slot name="thead-sub" v-bind="{ ...filterScope, field }" />
174
171
  </template>
175
172
 
176
173
  <!-- Custom headers for all fields -->
@@ -1318,9 +1315,8 @@ const dottedCellFields = (slots: Record<string, unknown>): TableField[] =>
1318
1315
  /*
1319
1316
  * Per-column widths (#156). A field's `width`/`minWidth` is applied through a
1320
1317
  * `<colgroup>` `<col>`, NOT the header `<th>`: with `table-layout: fixed` the
1321
- * column widths are taken from the FIRST row of cells, and DXTable's inline
1322
- * filter row (`thead-top`) is that first row — so a width on the header `<th>`
1323
- * would be ignored whenever filters are present. A `<col>` width is
1318
+ * column widths are taken from the FIRST row of cells, which may be a
1319
+ * consumer banner rather than the column labels. A `<col>` width is
1324
1320
  * authoritative regardless of row order, in both fixed and auto layout.
1325
1321
  */
1326
1322
  const sizeToCss = (value: string | number): string =>
@@ -2262,7 +2258,7 @@ const refresh = () => {
2262
2258
  *
2263
2259
  * - `head(<key>)` — DXTable draws its own column headers (sort indicators,
2264
2260
  * field hints). Forwarding a consumer's would silently drop those.
2265
- * - `thead-top` — DXTable renders the inline filter row there.
2261
+ * - `thead-top` / `thead-sub` — composed with the below-header filter cells.
2266
2262
  *
2267
2263
  * `head-end(<key>)` is DXTable's OWN additive header slot (#99) and is likewise
2268
2264
  * not forwarded: the inner table has no such slot, and the prefixes below are
@@ -2312,11 +2308,10 @@ const isTableSlot = (name: string) =>
2312
2308
  * invisible (#114). Called from the template, it re-evaluates every render.
2313
2309
  *
2314
2310
  * It also has to EXCLUDE the slots DXTable renders itself: declaring `#thead-top`
2315
- * here (even rendering nothing into it) would override DXTable's own thead-top
2316
- * template, silently dropping the filter row and the consumer's banner (#120).
2311
+ * or `#thead-sub` here would override the composed header/filter slots.
2317
2312
  */
2318
2313
  const forwardableSlotNames = (slots: Record<string, unknown>): string[] =>
2319
- Object.keys(slots).filter((name) => isTableSlot(name));
2314
+ Object.keys(slots).filter((name) => name !== 'thead-sub' && isTableSlot(name));
2320
2315
 
2321
2316
  const tableSlotSignature = (slots: Record<string, unknown>): string => {
2322
2317
  // DXTable's OWN dotted-cell slots count too (#121). bvn captures its cell