@omnitend/dashboard-for-laravel 0.29.0 → 0.31.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-19T09:16:53.630Z",
2
+ "generated": "2026-07-20T09:28:21.390Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.28.0"
5
+ "version": "0.31.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -102,8 +102,27 @@
102
102
  "name": "DAutocomplete",
103
103
  "category": "base",
104
104
  "filePath": "resources/js/components/base/DAutocomplete.vue",
105
- "description": "Type-safe wrapper around Bootstrap Vue Next BAutocomplete component. Text input with a filtered dropdown of suggestions (typeahead/combobox). Pass `options` and bind `v-model`; supports `multiple` and a custom `filterFunction`.",
106
- "props": [],
105
+ "description": "Type-safe wrapper around Bootstrap Vue Next BAutocomplete component. Text input with a filtered dropdown of suggestions (typeahead/combobox). Pass `options` and bind `v-model`; supports `multiple` and a custom `filterFunction`. `null-option` adds an opt-in, pinned \"no selection\" row (see below) so a searchable single-select can always reach \"none\" in-list (#138).",
106
+ "props": [
107
+ {
108
+ "name": "options",
109
+ "type": "AutocompleteOptions",
110
+ "required": false,
111
+ "description": "Options passed straight through to BAutocomplete (see its `options` prop)."
112
+ },
113
+ {
114
+ "name": "filterFunction",
115
+ "type": "FilterFunction",
116
+ "required": false,
117
+ "description": "A custom filter. Composed with `null-option` rather than replaced: when a\nnull option is present it stays pinned and this runs on the rest."
118
+ },
119
+ {
120
+ "name": "nullOption",
121
+ "type": "union",
122
+ "required": false,
123
+ "description": "Prepend a pinned, never-filtered, selectable \"no selection\" row whose value\nis `null` (#138). `true` → default label \"None\"; a string → that label.\nSelecting it sets the model to `null` (bvn reads that as no selection, so\nthe ✕ hides and the placeholder shows). Off by default — unchanged\nbehaviour. DXTable does NOT use this; it builds its own \"All\" option."
124
+ }
125
+ ],
107
126
  "events": [],
108
127
  "slots": [
109
128
  {
@@ -2114,7 +2133,14 @@
2114
2133
  "type": "boolean",
2115
2134
  "required": false,
2116
2135
  "default": "false",
2117
- "description": "Wrap the form in a card for a visual boundary (mirrors DXTable's\n`card` prop). Tabbed forms render the tab nav as a BS5\ncard-header-tabs instead of double-wrapping. Off by default since\nDXForm is commonly embedded in a page card or modal already."
2136
+ "description": "Wrap the form in a card for a visual boundary (mirrors DXTable's\n`card` prop). Off by default since DXForm is commonly embedded in a\npage card or modal already. Tabbed forms are wrapped by default\nregardless — see `cardTabs`; setting `card` also forces the tabbed\ncard on even when `cardTabs` is disabled."
2137
+ },
2138
+ {
2139
+ "name": "cardTabs",
2140
+ "type": "boolean",
2141
+ "required": false,
2142
+ "default": "true",
2143
+ "description": "Wrap a TABBED form's content in a card panel so the active tab reads\nas a finished panel connected to the tab strip (the standard\nBootstrap card-with-tabs pattern) rather than floating on the bare\npage background (#159). On by default. Set `false` for bare tabs\n(e.g. inside a modal that already provides a boundary). Ignored for\nflat (non-tabbed) forms — use `card` for those."
2118
2144
  },
2119
2145
  {
2120
2146
  "name": "layout",
@@ -2610,7 +2636,7 @@
2610
2636
  "name": "DXSwitch",
2611
2637
  "category": "extended",
2612
2638
  "filePath": "resources/js/components/extended/DXSwitch.vue",
2613
- "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 (a neutral box when off, a filled primary box when on). 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.",
2639
+ "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.",
2614
2640
  "props": [
2615
2641
  {
2616
2642
  "name": "modelValue",
@@ -2636,6 +2662,13 @@
2636
2662
  "type": "string",
2637
2663
  "required": false,
2638
2664
  "description": "Text shown when off — overrides `label` in the off state (e.g. \"Product is not current\")."
2665
+ },
2666
+ {
2667
+ "name": "onVariant",
2668
+ "type": "union",
2669
+ "required": false,
2670
+ "default": "\"success\"",
2671
+ "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)."
2639
2672
  }
2640
2673
  ],
2641
2674
  "events": [
@@ -2779,6 +2812,13 @@
2779
2812
  "default": "true",
2780
2813
  "description": "Show per-page selector"
2781
2814
  },
2815
+ {
2816
+ "name": "showCount",
2817
+ "type": "boolean",
2818
+ "required": false,
2819
+ "default": "true",
2820
+ "description": "Show the footer's item-count caption (\"N items.\" / \"X to Y out of Z\nitems.\"). Defaults to `true`. Set `false` to suppress just the caption,\nindependent of the pager — a page ported from a plain `<b-table>` that\nhides the pager with `:show-pagination=\"false\"` would otherwise gain a\ncaption it never had (#127)."
2821
+ },
2782
2822
  {
2783
2823
  "name": "perPageOptions",
2784
2824
  "type": "Array",
@@ -2797,7 +2837,7 @@
2797
2837
  "name": "perPage",
2798
2838
  "type": "number",
2799
2839
  "required": false,
2800
- "description": "Items per page (for provider mode, v-model support)"
2840
+ "description": "Items per page. With a `v-model:per-page` it is controlled state; a bare\n`:per-page=\"20\"` is the INITIAL page size (see the initial-value vs\ncontrolled note above). When passed explicitly it takes PRECEDENCE over any\nper-page preference persisted in localStorage — `:per-page=\"20\"` always\nstarts at 20, never a size the user once picked on another table (#124).\nOmit it to let a stored preference (URL-keyed tables only) or the default\napply."
2801
2841
  },
2802
2842
  {
2803
2843
  "name": "striped",
@@ -2953,6 +2993,13 @@
2953
2993
  "type": "boolean",
2954
2994
  "required": false,
2955
2995
  "description": "Enable client-side filtering, sorting, and pagination on items array"
2996
+ },
2997
+ {
2998
+ "name": "fixedLayout",
2999
+ "type": "boolean",
3000
+ "required": false,
3001
+ "default": "false",
3002
+ "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."
2956
3003
  }
2957
3004
  ],
2958
3005
  "events": [
@@ -3392,6 +3439,12 @@
3392
3439
  "required": true,
3393
3440
  "description": "Whether to render the per-page selector."
3394
3441
  },
3442
+ {
3443
+ "name": "showCount",
3444
+ "type": "boolean",
3445
+ "required": true,
3446
+ "description": "Whether to render the item-count caption (the \"X out of Z items\" line)."
3447
+ },
3395
3448
  {
3396
3449
  "name": "singularItemName",
3397
3450
  "type": "string",
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-19T09:16:53.689Z
4
+ > Last updated: 2026-07-20T09:28:21.457Z
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.29.0",
3
+ "version": "0.31.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",
@@ -16,9 +16,9 @@
16
16
 
17
17
  // Base $theme-colors carry each hue's READABLE-ON-LIGHT identity (its "emphasis"
18
18
  // shade) — this is what `.text-*`, `.link-*`, `.border-*` and outline buttons
19
- // use, so every one must clear AA on white. Bold solid FILLS (primary/danger
20
- // buttons) and SOFT tints (soft buttons, badges, alerts) are applied as explicit
21
- // overrides after the Bootstrap import, from the $dx-variants map. See
19
+ // use, so every one must clear AA on white. The one bold solid FILL (the primary
20
+ // button) and SOFT tints (soft buttons incl. danger, badges, alerts) are applied
21
+ // as explicit overrides after the Bootstrap import, from the $dx-variants map. See
22
22
  // plans/2026-07-18-semantic-colour-system.md.
23
23
  $primary: #151e2d; // Omni Tend brand navy (16.73:1 on white)
24
24
  $secondary: #475569; // Slate-600 (7.58:1 on white)
@@ -168,16 +168,22 @@ $badge-padding-x: 0.625rem;
168
168
  $badge-font-weight: 500;
169
169
 
170
170
  // The ONE source of truth for the semantic colour system. Each variant carries
171
- // its SOLID fill + same-hue text (primary/danger buttons, solid fills), its SOFT
171
+ // its SOLID fill + same-hue text (the primary button, solid fills), its SOFT
172
172
  // tint pair (soft buttons, badges, alerts, status), its EMPHASIS shade (outline
173
173
  // buttons, coloured links, .text-*), and whether its button is solid or soft.
174
174
  // The overrides after the Bootstrap import loop over this map. All pairs are
175
175
  // WCAG AA verified. See plans/2026-07-18-semantic-colour-system.md.
176
+ //
177
+ // Only PRIMARY is a bold solid button — one loud action (the brand). Everything
178
+ // else, INCLUDING danger, is soft: a soft red "Delete" reads as danger without
179
+ // a heavy fill, and one solid button per screen keeps the hierarchy clear. (Was
180
+ // primary+danger solid; danger's soft was also an off-hue plum — it is now a
181
+ // same-hue light red, matching the danger emphasis and the switch OFF.)
176
182
  $dx-variants: (
177
183
  "primary": (solid-bg: #151e2d, solid-text: #e9f0f8, soft-bg: #e9f0f8, soft-text: #151e2d, emphasis: #151e2d, button: "solid"),
178
184
  "secondary": (solid-bg: #475569, solid-text: #e6ebf2, soft-bg: #e6ebf2, soft-text: #29374a, emphasis: #475569, button: "soft"),
179
185
  "success": (solid-bg: #84cc16, solid-text: #203b0e, soft-bg: #cdf9b2, soft-text: #203b0e, emphasis: #4d7c0f, button: "soft"),
180
- "danger": (solid-bg: #dc2626, solid-text: #ffffff, soft-bg: #f5dff1, soft-text: #59194a, emphasis: #dc2626, button: "solid"),
186
+ "danger": (solid-bg: #dc2626, solid-text: #ffffff, soft-bg: #f8d4d4, soft-text: #7a1a1a, emphasis: #dc2626, button: "soft"),
181
187
  "warning": (solid-bg: #f59e0b, solid-text: #512d05, soft-bg: #fce5c4, soft-text: #512d05, emphasis: #b45309, button: "soft"),
182
188
  "info": (solid-bg: #2563eb, solid-text: #eef4ff, soft-bg: #deebff, soft-text: #12376c, emphasis: #2563eb, button: "soft"),
183
189
  );
@@ -456,10 +462,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
456
462
  // ----------------------------------------------------------------------------
457
463
  // One loop over $dx-variants applies the whole system on top of Bootstrap:
458
464
  //
459
- // - Buttons: primary/danger are SOLID (fill from the base colour; we only
465
+ // - Buttons: only PRIMARY is SOLID (fill from the base colour; we only
460
466
  // override the label to the same-hue tint, letting Bootstrap shade hover/
461
- // active). secondary/success/warning/info are SOFT (light tint bg + dark
462
- // text, fully overridden incl. hover/active).
467
+ // active). secondary/success/danger/warning/info are SOFT (light tint bg +
468
+ // dark text, fully overridden incl. hover/active).
463
469
  // - Outline buttons: the emphasis shade for border/label, the soft tint on hover.
464
470
  // - Badges: `.badge.text-bg-*` re-tinted soft — needs `!important` because
465
471
  // Bootstrap's `.text-bg-*` helper sets its colours `!important` by design.
@@ -516,8 +522,15 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
516
522
  --bs-btn-active-border-color: #{$emphasis};
517
523
  }
518
524
 
519
- // Badge — soft tint. `!important` beats Bootstrap's `.text-bg-*` helper.
520
- .badge.text-bg-#{$name} {
525
+ // `.text-bg-*` — soft tint, everywhere (not just `.badge`). Soft-first is the
526
+ // default across the board: a `.badge`, an `.input-group-text`, a chip or a
527
+ // small status bar built with `.text-bg-success` all get the same soft green
528
+ // as the badge, so a stock indicator can match a "current" badge without
529
+ // hardcoding the hue. `!important` beats Bootstrap's `.text-bg-*` helper
530
+ // (which sets its colours `!important` by design). `.toast` is excluded: it
531
+ // has its own soft handling (a fainter 50%-white mix via `--bs-toast-bg` for
532
+ // its large surface) and themed toasts don't carry `.text-bg-*` anyway.
533
+ .text-bg-#{$name}:not(.toast) {
521
534
  background-color: #{$soft-bg} !important;
522
535
  color: #{$soft-text} !important;
523
536
  }
@@ -534,9 +547,23 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
534
547
  // colour IS its emphasis shade, so Bootstrap already renders them legible.
535
548
  }
536
549
 
550
+ // Publish each variant's soft/emphasis shades as CSS variables so components
551
+ // (e.g. DXSwitch's on-box) and consumers can reference the exact soft-first
552
+ // tints on any element — Bootstrap's own `--bs-*-bg-subtle` are a different,
553
+ // greyer family derived from the dark emphasis base, so they don't match the
554
+ // soft badge/alert green. These are the same values the $dx-variants loop paints.
555
+ :root {
556
+ @each $name, $v in $dx-variants {
557
+ --dx-#{$name}-soft-bg: #{map.get($v, "soft-bg")};
558
+ --dx-#{$name}-soft-text: #{map.get($v, "soft-text")};
559
+ --dx-#{$name}-solid-bg: #{map.get($v, "solid-bg")};
560
+ --dx-#{$name}-emphasis: #{map.get($v, "emphasis")};
561
+ }
562
+ }
563
+
537
564
  // primary is near-black navy, so Bootstrap's darken-on-hover is imperceptible —
538
- // the main action button would have no visible hover feedback. Lighten instead.
539
- // (danger's red darkens visibly, so the generic solid path is fine for it.)
565
+ // the main (and now only) solid button would have no visible hover feedback.
566
+ // Lighten instead. (Other variants are soft; their hover is handled in the loop.)
540
567
  .btn-primary {
541
568
  --bs-btn-hover-bg: #{color.scale($primary, $lightness: 20%)};
542
569
  --bs-btn-hover-border-color: #{color.scale($primary, $lightness: 20%)};
@@ -557,6 +584,93 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
557
584
  }
558
585
  }
559
586
 
587
+ // ----------------------------------------------------------------------------
588
+ // Switches — green when ON, red when OFF, by default (#158)
589
+ // ----------------------------------------------------------------------------
590
+ // House style (James): an active switch — "Product is current", "Stock is
591
+ // tracked", "Visible" — should read success GREEN when on and RED when off,
592
+ // OUT OF THE BOX, not only when a consumer opts in. Bootstrap's default is a
593
+ // primary (navy) track when on / grey when off. We flip the default here.
594
+ //
595
+ // Semantically-MIXED switches ("contains alcohol", an allergen toggle, "hidden
596
+ // on web shop") shouldn't imply good/bad — those opt OUT with `.switch-neutral`
597
+ // on the `.form-switch`, which restores Bootstrap's grey-off / primary-on.
598
+ //
599
+ // Applies to any bare `.form-switch` (DFormCheckbox `switch`); DXSwitch is the
600
+ // "filled box" form where the whole control (not the toggle) carries the colour
601
+ // and the pill stays a neutral affordance — it reads the same tokens below.
602
+ $dx-switch-on: map.get(map.get($dx-variants, "success"), "solid-bg"); // vivid lime
603
+ $dx-switch-off: map.get(map.get($dx-variants, "danger"), "solid-bg"); // vivid red
604
+ $dx-switch-thumb-white: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
605
+ // A red thumb reads better than white on the light-red OFF track.
606
+ $dx-switch-thumb-red: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23dc2626'/%3e%3c/svg%3e");
607
+
608
+ // Switch fill tokens. The ON fill is the SAME soft green as the success
609
+ // badge/button (its `soft-bg`), so a switch and a success badge read as one
610
+ // green — not a separate, punchier lime. OFF is a LIGHT red (a light tint of the
611
+ // danger solid, NOT the plum `danger` soft — a switch off wants to read "red /
612
+ // stop", which the plum status tint doesn't). Derived from the base tokens so
613
+ // one edit moves them. Used by the DXSwitch filled box; the OFF track reuses them.
614
+ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cdf9b2
615
+ :root {
616
+ --dx-switch-on-bg: #{$dx-switch-on-soft};
617
+ --dx-switch-on-well: #{color.mix($dx-switch-on-soft, #fff, 55%)};
618
+ --dx-switch-on-line: #{color.mix($dx-switch-on-soft, $dx-switch-on, 55%)};
619
+ --dx-switch-on-ink: #{map.get(map.get($dx-variants, "success"), "soft-text")};
620
+ --dx-switch-off-bg: #{color.mix($dx-switch-off, #fff, 20%)};
621
+ --dx-switch-off-well: #{color.mix($dx-switch-off, #fff, 10%)};
622
+ --dx-switch-off-line: #{color.mix($dx-switch-off, #fff, 45%)};
623
+ --dx-switch-off-ink: #{color.mix($dx-switch-off, #000, 30%)};
624
+ --dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral pill in both states
625
+ }
626
+
627
+ .form-switch .form-check-input {
628
+ // OFF: light-red track, red thumb (left).
629
+ background-color: var(--dx-switch-off-bg);
630
+ border-color: var(--dx-switch-off-line);
631
+ --bs-form-switch-bg: #{$dx-switch-thumb-red};
632
+
633
+ &:checked {
634
+ // ON: vivid green track, white thumb (right — Bootstrap positions it).
635
+ background-color: #{$dx-switch-on};
636
+ border-color: #{$dx-switch-on};
637
+ --bs-form-switch-bg: #{$dx-switch-thumb-white};
638
+ }
639
+
640
+ // Neutral grey focus ring (the track carries the state colour, so the brand
641
+ // navy ring would clash); keep the state thumb rather than Bootstrap's navy.
642
+ &:focus {
643
+ --bs-form-switch-bg: #{$dx-switch-thumb-red};
644
+ border-color: var(--dx-switch-off-line);
645
+ box-shadow: 0 0 0 0.25rem rgba($dark, 0.15);
646
+ }
647
+
648
+ &:checked:focus {
649
+ border-color: #{$dx-switch-on};
650
+ --bs-form-switch-bg: #{$dx-switch-thumb-white};
651
+ }
652
+ }
653
+
654
+ // Neutral opt-out for mixed-semantics switches: restore Bootstrap's grey-off /
655
+ // primary-on (no good/bad valence).
656
+ .form-switch.switch-neutral .form-check-input {
657
+ background-color: var(--bs-body-bg);
658
+ border-color: var(--bs-border-color);
659
+ // Bootstrap's default faint-grey OFF thumb.
660
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
661
+
662
+ &:checked {
663
+ background-color: var(--bs-primary);
664
+ border-color: var(--bs-primary);
665
+ --bs-form-switch-bg: #{$dx-switch-thumb-white};
666
+ }
667
+
668
+ &:focus {
669
+ border-color: var(--bs-primary);
670
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
671
+ }
672
+ }
673
+
560
674
  // ----------------------------------------------------------------------------
561
675
  // Chart palette (data-viz)
562
676
  // ----------------------------------------------------------------------------
@@ -4,10 +4,38 @@
4
4
  * Text input with a filtered dropdown of suggestions (typeahead/combobox).
5
5
  * Pass `options` and bind `v-model`; supports `multiple` and a custom
6
6
  * `filterFunction`.
7
+ *
8
+ * `null-option` adds an opt-in, pinned "no selection" row (see below) so a
9
+ * searchable single-select can always reach "none" in-list (#138).
7
10
  */
8
11
  <script setup lang="ts">
9
12
  import { computed, useAttrs } from "vue";
10
13
  import { BAutocomplete } from "bootstrap-vue-next";
14
+ import type { SelectOption } from "bootstrap-vue-next";
15
+
16
+ // bvn's own `options` prop type — kept identical so this stays a pass-through.
17
+ type AutocompleteOptions = readonly (object | string | number | boolean)[];
18
+ type FilterFunction = (options: SelectOption[], search: string) => SelectOption[];
19
+
20
+ interface Props {
21
+ /** Options passed straight through to BAutocomplete (see its `options` prop). */
22
+ options?: AutocompleteOptions;
23
+ /**
24
+ * A custom filter. Composed with `null-option` rather than replaced: when a
25
+ * null option is present it stays pinned and this runs on the rest.
26
+ */
27
+ filterFunction?: FilterFunction;
28
+ /**
29
+ * Prepend a pinned, never-filtered, selectable "no selection" row whose value
30
+ * is `null` (#138). `true` → default label "None"; a string → that label.
31
+ * Selecting it sets the model to `null` (bvn reads that as no selection, so
32
+ * the ✕ hides and the placeholder shows). Off by default — unchanged
33
+ * behaviour. DXTable does NOT use this; it builds its own "All" option.
34
+ */
35
+ nullOption?: boolean | string;
36
+ }
37
+
38
+ const props = defineProps<Props>();
11
39
 
12
40
  defineOptions({
13
41
  inheritAttrs: false,
@@ -15,6 +43,68 @@ defineOptions({
15
43
 
16
44
  const attrs = useAttrs();
17
45
 
46
+ const attr = (name: string, hyphenated: string) =>
47
+ (attrs as Record<string, unknown>)[name] ?? (attrs as Record<string, unknown>)[hyphenated];
48
+
49
+ const DEFAULT_NULL_OPTION_LABEL = "None";
50
+
51
+ /*
52
+ * The null option is a raw option that bvn will normalise through the SAME
53
+ * `value-field`/`text-field` the consumer configured (default `value`/`text`).
54
+ * Build it with those effective field names so it normalises to
55
+ * `{ value: null, text: label }` — otherwise a consumer using e.g.
56
+ * `value-field="id" text-field="label"` would normalise our `{ value, text }`
57
+ * to `{ value: undefined, text: undefined }`, and the row would be filtered out
58
+ * and select as `undefined` instead of `null` (#138 review).
59
+ */
60
+ const nullOption = computed<Record<string, unknown> | null>(() => {
61
+ const raw = props.nullOption;
62
+ if (raw === false || raw === undefined) return null;
63
+ const text = typeof raw === "string" ? raw : DEFAULT_NULL_OPTION_LABEL;
64
+ const valueField = (attr("valueField", "value-field") as string | undefined) ?? "value";
65
+ const textField = (attr("textField", "text-field") as string | undefined) ?? "text";
66
+ return { [valueField]: null, [textField]: text };
67
+ });
68
+
69
+ const consumerOptions = computed<AutocompleteOptions>(() =>
70
+ Array.isArray(props.options) ? props.options : [],
71
+ );
72
+
73
+ const computedOptions = computed<AutocompleteOptions>(() => {
74
+ const pinned = nullOption.value;
75
+ if (!pinned) return props.options ?? [];
76
+ return [pinned, ...consumerOptions.value];
77
+ });
78
+
79
+ const isNullOption = (option: SelectOption) => option?.value === null;
80
+
81
+ /*
82
+ * When a null option is present it must stay pinned (top of the list, immune to
83
+ * the search term); the consumer's `filterFunction` — or bvn's default
84
+ * substring-on-text match — applies only to the rest, so the two compose rather
85
+ * than clobber. With no null option we forward the consumer's filter unchanged
86
+ * (possibly undefined, letting bvn use its own default).
87
+ */
88
+ const effectiveFilterFunction = computed<FilterFunction | undefined>(() => {
89
+ const pinned = nullOption.value;
90
+ const consumerFilter = props.filterFunction;
91
+ if (!pinned) return consumerFilter;
92
+
93
+ return (options, search) => {
94
+ const rest = options.filter((option) => !isNullOption(option));
95
+ const filteredRest = consumerFilter
96
+ ? consumerFilter(rest, search)
97
+ : search
98
+ ? rest.filter((option) =>
99
+ String(option.text ?? "")
100
+ .toLowerCase()
101
+ .includes(search.toLowerCase()),
102
+ )
103
+ : rest;
104
+ return [...options.filter(isNullOption), ...filteredRest];
105
+ };
106
+ });
107
+
18
108
  /*
19
109
  * Diverges from bvn: the clear (✕) is hidden when there is nothing to clear.
20
110
  *
@@ -28,9 +118,6 @@ const attrs = useAttrs();
28
118
  * A consumer's explicit `no-clear-button` still wins; this only ever hides the
29
119
  * button, never forces it on.
30
120
  */
31
- const attr = (name: string, hyphenated: string) =>
32
- (attrs as Record<string, unknown>)[name] ?? (attrs as Record<string, unknown>)[hyphenated];
33
-
34
121
  const hasValue = computed(() => {
35
122
  const value = attr("modelValue", "model-value");
36
123
  if (Array.isArray(value)) return value.length > 0;
@@ -52,7 +139,12 @@ const noClearButton = computed(
52
139
  (#53). A plain element root makes it deterministic.
53
140
  -->
54
141
  <div class="d-autocomplete">
55
- <BAutocomplete v-bind="$attrs" :no-clear-button="noClearButton">
142
+ <BAutocomplete
143
+ v-bind="$attrs"
144
+ :options="computedOptions"
145
+ :filter-function="effectiveFilterFunction"
146
+ :no-clear-button="noClearButton"
147
+ >
56
148
  <!-- Dynamically pass through all named slots with their props -->
57
149
  <template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
58
150
  <slot :name="name" v-bind="slotProps" />
@@ -159,6 +159,7 @@
159
159
  <DXSwitch
160
160
  v-else
161
161
  v-model="switchModel"
162
+ :on-variant="field.switchVariant"
162
163
  :disabled="isDisabled || isReadonly || isPlaintext"
163
164
  v-bind="controlPropsWithGuards"
164
165
  >
@@ -201,6 +202,7 @@
201
202
  <DXSwitch
202
203
  v-else
203
204
  v-model="switchModel"
205
+ :on-variant="field.switchVariant"
204
206
  :disabled="isDisabled || isReadonly || isPlaintext"
205
207
  v-bind="controlPropsWithGuards"
206
208
  >
@@ -36,8 +36,8 @@
36
36
  `.card-body` wrapper — DTabs already provides `.card-header`/
37
37
  `.card-body` internally via its own `card` prop, so wrapping
38
38
  that in another `.card-body` would double up. -->
39
- <component :is="card ? DCard : 'div'" v-if="hasTabs" v-bind="card ? { noBody: true } : {}">
40
- <DTabs v-model:index="activeTab" :card="card">
39
+ <component :is="tabsInCard ? DCard : 'div'" v-if="hasTabs" v-bind="tabsInCard ? { noBody: true } : {}">
40
+ <DTabs v-model:index="activeTab" :card="tabsInCard">
41
41
  <DTab
42
42
  v-for="(tab, index) in visibleTabs"
43
43
  :key="tab.key"
@@ -213,12 +213,23 @@ interface Props {
213
213
 
214
214
  /**
215
215
  * Wrap the form in a card for a visual boundary (mirrors DXTable's
216
- * `card` prop). Tabbed forms render the tab nav as a BS5
217
- * card-header-tabs instead of double-wrapping. Off by default since
218
- * DXForm is commonly embedded in a page card or modal already.
216
+ * `card` prop). Off by default since DXForm is commonly embedded in a
217
+ * page card or modal already. Tabbed forms are wrapped by default
218
+ * regardless — see `cardTabs`; setting `card` also forces the tabbed
219
+ * card on even when `cardTabs` is disabled.
219
220
  */
220
221
  card?: boolean;
221
222
 
223
+ /**
224
+ * Wrap a TABBED form's content in a card panel so the active tab reads
225
+ * as a finished panel connected to the tab strip (the standard
226
+ * Bootstrap card-with-tabs pattern) rather than floating on the bare
227
+ * page background (#159). On by default. Set `false` for bare tabs
228
+ * (e.g. inside a modal that already provides a boundary). Ignored for
229
+ * flat (non-tabbed) forms — use `card` for those.
230
+ */
231
+ cardTabs?: boolean;
232
+
222
233
  /**
223
234
  * Form-wide field layout: "vertical" (default, label above input) or
224
235
  * "horizontal" (label left, input right). Overridable per-field via
@@ -241,6 +252,7 @@ const props = withDefaults(defineProps<Props>(), {
241
252
  showSubmit: true,
242
253
  autoErrorTab: true,
243
254
  card: false,
255
+ cardTabs: true,
244
256
  layout: "vertical",
245
257
  });
246
258
 
@@ -318,6 +330,14 @@ const hasTabs = computed(
318
330
  () => !!props.tabs && props.tabs.length > 0,
319
331
  );
320
332
 
333
+ // Tabbed forms render inside a card panel by default (#159) so the tab
334
+ // content reads as a finished panel rather than floating on the page.
335
+ // `cardTabs` is the tabbed-only toggle (on by default); `card` (which also
336
+ // wraps flat forms) still forces it on. Flat forms are unaffected.
337
+ const tabsInCard = computed(
338
+ () => hasTabs.value && (props.card || props.cardTabs),
339
+ );
340
+
321
341
  function visibleFieldsFor(tab: FormTab): FieldDefinition[] {
322
342
  return tab.fieldKeys
323
343
  .map((key) => fieldByKey.value[key])