@omnitend/dashboard-for-laravel 0.28.0 → 0.30.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.
Files changed (40) hide show
  1. package/dist/charts.d.ts +19 -0
  2. package/dist/charts.js +225 -0
  3. package/dist/charts.js.map +1 -0
  4. package/dist/charts.umd.cjs +2 -0
  5. package/dist/charts.umd.cjs.map +1 -0
  6. package/dist/components/base/DAlert.vue.d.ts +6 -1
  7. package/dist/components/base/DAutocomplete.vue.d.ts +21 -1
  8. package/dist/components/base/DFormCheckboxGroup.vue.d.ts +12 -0
  9. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +2 -2
  10. package/dist/components/extended/DXField.vue.d.ts +4 -4
  11. package/dist/components/extended/DXForm.vue.d.ts +15 -4
  12. package/dist/components/extended/DXTable.vue.d.ts +39 -1
  13. package/dist/components/extended/DXTablePagination.vue.d.ts +2 -0
  14. package/dist/dashboard-for-laravel.js +8826 -8937
  15. package/dist/dashboard-for-laravel.js.map +1 -1
  16. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  17. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  18. package/dist/index.d.ts +1 -3
  19. package/dist/style.css +1 -1
  20. package/dist/types/index.d.ts +1 -1
  21. package/docs/public/api-reference.json +84 -9
  22. package/docs/public/docs-map.md +1 -1
  23. package/docs/public/llms.txt +3 -2
  24. package/package.json +11 -5
  25. package/resources/css/theme.scss +21 -7
  26. package/resources/js/charts.ts +19 -0
  27. package/resources/js/components/base/DAlert.vue +19 -1
  28. package/resources/js/components/base/DAutocomplete.vue +96 -4
  29. package/resources/js/components/base/DFormCheckboxGroup.vue +22 -0
  30. package/resources/js/components/charts/DXBarChart.vue +0 -9
  31. package/resources/js/components/charts/DXDoughnutChart.vue +0 -9
  32. package/resources/js/components/charts/DXLineChart.vue +0 -9
  33. package/resources/js/components/extended/DXField.vue +12 -0
  34. package/resources/js/components/extended/DXForm.vue +25 -5
  35. package/resources/js/components/extended/DXTable.vue +139 -19
  36. package/resources/js/components/extended/DXTableEditorModal.vue +4 -0
  37. package/resources/js/components/extended/DXTablePagination.vue +5 -2
  38. package/resources/js/composables/useForm.ts +30 -3
  39. package/resources/js/index.ts +4 -3
  40. package/resources/js/types/index.ts +1 -0
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-07-18T13:55:43.374Z",
2
+ "generated": "2026-07-20T07:09:59.465Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.28.0"
5
+ "version": "0.30.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -53,7 +53,15 @@
53
53
  "category": "base",
54
54
  "filePath": "resources/js/components/base/DAlert.vue",
55
55
  "description": "",
56
- "props": [],
56
+ "props": [
57
+ {
58
+ "name": "modelValue",
59
+ "type": "union",
60
+ "required": false,
61
+ "default": "true",
62
+ "description": ""
63
+ }
64
+ ],
57
65
  "events": [],
58
66
  "slots": [
59
67
  {
@@ -94,8 +102,27 @@
94
102
  "name": "DAutocomplete",
95
103
  "category": "base",
96
104
  "filePath": "resources/js/components/base/DAutocomplete.vue",
97
- "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`.",
98
- "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
+ ],
99
126
  "events": [],
100
127
  "slots": [
101
128
  {
@@ -481,6 +508,27 @@
481
508
  ],
482
509
  "methods": []
483
510
  },
511
+ {
512
+ "name": "DFormCheckboxGroup",
513
+ "category": "base",
514
+ "filePath": "resources/js/components/base/DFormCheckboxGroup.vue",
515
+ "description": "Type-safe wrapper around Bootstrap Vue Next BFormCheckboxGroup component. Renders a group of checkboxes from an `options` array (or slot content); the `v-model` is the ARRAY of checked option values (\"pick any of N\").",
516
+ "props": [],
517
+ "events": [],
518
+ "slots": [
519
+ {
520
+ "name": "name",
521
+ "description": "",
522
+ "bindings": [
523
+ {
524
+ "name": "name",
525
+ "title": "binding"
526
+ }
527
+ ]
528
+ }
529
+ ],
530
+ "methods": []
531
+ },
484
532
  {
485
533
  "name": "DFormGroup",
486
534
  "category": "base",
@@ -2085,7 +2133,14 @@
2085
2133
  "type": "boolean",
2086
2134
  "required": false,
2087
2135
  "default": "false",
2088
- "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."
2089
2144
  },
2090
2145
  {
2091
2146
  "name": "layout",
@@ -2750,6 +2805,13 @@
2750
2805
  "default": "true",
2751
2806
  "description": "Show per-page selector"
2752
2807
  },
2808
+ {
2809
+ "name": "showCount",
2810
+ "type": "boolean",
2811
+ "required": false,
2812
+ "default": "true",
2813
+ "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)."
2814
+ },
2753
2815
  {
2754
2816
  "name": "perPageOptions",
2755
2817
  "type": "Array",
@@ -2768,7 +2830,7 @@
2768
2830
  "name": "perPage",
2769
2831
  "type": "number",
2770
2832
  "required": false,
2771
- "description": "Items per page (for provider mode, v-model support)"
2833
+ "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."
2772
2834
  },
2773
2835
  {
2774
2836
  "name": "striped",
@@ -2924,6 +2986,13 @@
2924
2986
  "type": "boolean",
2925
2987
  "required": false,
2926
2988
  "description": "Enable client-side filtering, sorting, and pagination on items array"
2989
+ },
2990
+ {
2991
+ "name": "fixedLayout",
2992
+ "type": "boolean",
2993
+ "required": false,
2994
+ "default": "false",
2995
+ "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."
2927
2996
  }
2928
2997
  ],
2929
2998
  "events": [
@@ -3363,6 +3432,12 @@
3363
3432
  "required": true,
3364
3433
  "description": "Whether to render the per-page selector."
3365
3434
  },
3435
+ {
3436
+ "name": "showCount",
3437
+ "type": "boolean",
3438
+ "required": true,
3439
+ "description": "Whether to render the item-count caption (the \"X out of Z items\" line)."
3440
+ },
3366
3441
  {
3367
3442
  "name": "singularItemName",
3368
3443
  "type": "string",
@@ -3481,8 +3556,8 @@
3481
3556
  ]
3482
3557
  },
3483
3558
  "stats": {
3484
- "totalComponents": 75,
3485
- "baseComponents": 56,
3559
+ "totalComponents": 76,
3560
+ "baseComponents": 57,
3486
3561
  "extendedComponents": 19
3487
3562
  }
3488
3563
  }
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-18T13:55:43.432Z
4
+ > Last updated: 2026-07-20T07:09:59.531Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Vue 3 dashboard components for Laravel with Bootstrap Vue Next
4
4
  >
5
- > A dual-package library (NPM + Composer) providing 68 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
5
+ > A dual-package library (NPM + Composer) providing 69 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
6
6
 
7
7
  ## Installation
8
8
 
@@ -19,7 +19,7 @@
19
19
  - [Theming](/guide/theming): Customize appearance with CSS variables
20
20
  - [TypeScript](/guide/typescript): TypeScript types and best practices
21
21
 
22
- ## Base Components (56 components)
22
+ ## Base Components (57 components)
23
23
 
24
24
  Lightweight type-safe wrappers around Bootstrap Vue Next components providing API stability and consistent theming.
25
25
 
@@ -43,6 +43,7 @@ Lightweight type-safe wrappers around Bootstrap Vue Next components providing AP
43
43
  - [DDropdownItem](/components/base/ddropdownitem): Bootstrap Vue Next DropdownItem wrapper
44
44
  - [DForm](/components/base/dform): Bootstrap Vue Next Form wrapper
45
45
  - [DFormCheckbox](/components/base/dformcheckbox): Bootstrap Vue Next FormCheckbox wrapper
46
+ - [DFormCheckboxGroup](/components/base/dformcheckboxgroup): Bootstrap Vue Next FormCheckboxGroup wrapper
46
47
  - [DFormGroup](/components/base/dformgroup): Bootstrap Vue Next FormGroup wrapper
47
48
  - [DFormInput](/components/base/dforminput): Bootstrap Vue Next FormInput wrapper
48
49
  - [DFormInvalidFeedback](/components/base/dforminvalidfeedback): Bootstrap Vue Next FormInvalidFeedback wrapper
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.28.0",
3
+ "version": "0.30.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",
@@ -15,6 +15,11 @@
15
15
  "import": "./dist/dashboard-for-laravel.js",
16
16
  "require": "./dist/dashboard-for-laravel.umd.cjs"
17
17
  },
18
+ "./charts": {
19
+ "types": "./dist/charts.d.ts",
20
+ "import": "./dist/charts.js",
21
+ "require": "./dist/charts.umd.cjs"
22
+ },
18
23
  "./style.css": "./dist/style.css",
19
24
  "./theme.css": "./dist/style.css",
20
25
  "./theme.scss": "./resources/css/theme.scss"
@@ -29,13 +34,14 @@
29
34
  ],
30
35
  "scripts": {
31
36
  "dev": "vite build --watch",
32
- "build": "vite build && node scripts/extract-icon-font.mjs && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
37
+ "build:lib": "vite build && vite build --config vite.config.charts.ts && node scripts/extract-icon-font.mjs",
38
+ "build": "npm run build:lib && vue-tsc --declaration --emitDeclarationOnly --outDir dist",
33
39
  "typecheck": "vue-tsc --noEmit",
34
- "pretest": "vite build",
40
+ "pretest": "npm run build:lib",
35
41
  "test": "vitest",
36
- "pretest:ui": "vite build",
42
+ "pretest:ui": "npm run build:lib",
37
43
  "test:ui": "vitest --ui",
38
- "pretest:headless": "vite build",
44
+ "pretest:headless": "npm run build:lib",
39
45
  "test:headless": "vitest --run --browser.headless",
40
46
  "docs:dev": "astro dev",
41
47
  "docs:build": "npm run docs:generate:ai && astro build && node scripts/check-docs-base-links.mjs",
@@ -436,14 +436,17 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
436
436
 
437
437
  // Semantic variants: subtle tinted surface, driven through the CSS variable
438
438
  // the base `.toast` rule (above, and Bootstrap's own) already reads.
439
- @each $name, $color in (
440
- "success": $success,
441
- "danger": $danger,
442
- "warning": $warning,
443
- "info": $info
444
- ) {
439
+ //
440
+ // Sourced from each variant's `soft-bg` in $dx-variants (#143) — the same
441
+ // vivid-derived soft family badges/alerts use — so the toast hue tracks the
442
+ // palette and editing a `soft-bg` moves toasts too. Previously these mixed the
443
+ // dark *emphasis* base colours (greyer success/warning) and danger's saturated
444
+ // red (a pink, not the soft system's plum). Mixed 50% with white so a large
445
+ // toast surface stays fainter than a small badge while keeping the hue.
446
+ @each $name in ("success", "danger", "warning", "info") {
447
+ $variant: map.get($dx-variants, $name);
445
448
  &.toast-#{$name} {
446
- --bs-toast-bg: #{color.mix($color, $white, 8%)};
449
+ --bs-toast-bg: #{color.mix(map.get($variant, "soft-bg"), $white, 50%)};
447
450
  }
448
451
  }
449
452
  }
@@ -581,6 +584,17 @@ $dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d9770
581
584
  }
582
585
  }
583
586
 
587
+ // The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped
588
+ // from the `@omnitend/dashboard-for-laravel/charts` entry, #142). Global rather
589
+ // than a component scoped style so the charts entry emits NO CSS — chart
590
+ // consumers already import this stylesheet for the theme + palette variables.
591
+ .dx-chart {
592
+ position: relative;
593
+ width: 100%;
594
+ height: 100%;
595
+ min-height: 240px;
596
+ }
597
+
584
598
  // ----------------------------------------------------------------------------
585
599
  // Autocomplete
586
600
  // ----------------------------------------------------------------------------
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Chart entry point — `@omnitend/dashboard-for-laravel/charts` (#142).
3
+ *
4
+ * The chart components are the ONLY ones that need `chart.js` / `vue-chartjs`,
5
+ * so they live behind this separate subpath entry. The main entry
6
+ * (`@omnitend/dashboard-for-laravel`) never references the chart libraries, so
7
+ * they are genuinely OPTIONAL peers: a consumer that renders no charts installs
8
+ * neither and their build no longer fails resolving them.
9
+ *
10
+ * import { DXBarChart } from '@omnitend/dashboard-for-laravel/charts';
11
+ *
12
+ * Chart containers are styled by `.dx-chart` in the main stylesheet
13
+ * (`@omnitend/dashboard-for-laravel/style.css`), which chart consumers already
14
+ * import for the theme and the `--dx-chart-*` palette variables — so this entry
15
+ * ships no CSS of its own.
16
+ */
17
+ export { default as DXBarChart } from "./components/charts/DXBarChart.vue";
18
+ export { default as DXLineChart } from "./components/charts/DXLineChart.vue";
19
+ export { default as DXDoughnutChart } from "./components/charts/DXDoughnutChart.vue";
@@ -2,11 +2,29 @@
2
2
  import { BAlert } from "bootstrap-vue-next";
3
3
  import { useSlots } from "vue";
4
4
 
5
+ /*
6
+ * bootstrap-vue-next's BAlert defaults `modelValue` to `false`, so a bare static
7
+ * `<DAlert variant="info">…</DAlert>` silently renders NOTHING — a footgun that
8
+ * bit our own docs repeatedly (#144). Default it to `true` (visible) at the
9
+ * wrapper; every call site that already passes `v-model`/`:model-value` is
10
+ * unaffected — only the bare-static case (which was always a bug) changes.
11
+ *
12
+ * Care (see DIVERGENCES.md): declaring `modelValue` as a prop strips it from
13
+ * `$attrs`, so it is re-bound explicitly below. `emits` is left UNDECLARED so an
14
+ * `update:modelValue` listener keeps flowing through `$attrs` to BAlert — that's
15
+ * what lets `v-model` dismissal (and the auto-dismiss countdown) still
16
+ * round-trip. The type stays `boolean | number` (a number is the auto-dismiss
17
+ * duration in ms) — don't narrow it.
18
+ */
19
+ const props = withDefaults(defineProps<{ modelValue?: boolean | number }>(), {
20
+ modelValue: true,
21
+ });
22
+
5
23
  const slots = useSlots();
6
24
  </script>
7
25
 
8
26
  <template>
9
- <BAlert v-bind="$attrs">
27
+ <BAlert :model-value="props.modelValue" v-bind="$attrs">
10
28
  <template v-for="(_, name) in slots" :key="name" #[name]>
11
29
  <slot :name="name" />
12
30
  </template>
@@ -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" />
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @component
3
+ * Type-safe wrapper around Bootstrap Vue Next BFormCheckboxGroup component.
4
+ * Renders a group of checkboxes from an `options` array (or slot content); the
5
+ * `v-model` is the ARRAY of checked option values ("pick any of N").
6
+ */
7
+ <script setup lang="ts">
8
+ import { BFormCheckboxGroup } from "bootstrap-vue-next";
9
+
10
+ defineOptions({
11
+ inheritAttrs: false,
12
+ });
13
+ </script>
14
+
15
+ <template>
16
+ <BFormCheckboxGroup v-bind="$attrs">
17
+ <!-- Dynamically pass through all named slots with their props -->
18
+ <template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
19
+ <slot :name="name" v-bind="slotProps" />
20
+ </template>
21
+ </BFormCheckboxGroup>
22
+ </template>
@@ -68,12 +68,3 @@ const mergedOptions = computed(() =>
68
68
  ),
69
69
  );
70
70
  </script>
71
-
72
- <style scoped>
73
- .dx-chart {
74
- position: relative;
75
- width: 100%;
76
- height: 100%;
77
- min-height: 240px;
78
- }
79
- </style>
@@ -74,12 +74,3 @@ const mergedOptions = computed(() =>
74
74
  ),
75
75
  );
76
76
  </script>
77
-
78
- <style scoped>
79
- .dx-chart {
80
- position: relative;
81
- width: 100%;
82
- height: 100%;
83
- min-height: 240px;
84
- }
85
- </style>
@@ -68,12 +68,3 @@ const mergedOptions = computed(() =>
68
68
  ),
69
69
  );
70
70
  </script>
71
-
72
- <style scoped>
73
- .dx-chart {
74
- position: relative;
75
- width: 100%;
76
- height: 100%;
77
- min-height: 240px;
78
- }
79
- </style>
@@ -370,6 +370,17 @@
370
370
  v-bind="controlPropsWithGuards"
371
371
  />
372
372
 
373
+ <!-- Checkbox group: "pick any of N" — the model is an ARRAY of the
374
+ checked option values (default the field to []). -->
375
+ <DFormCheckboxGroup
376
+ v-else-if="field.type === 'checkbox-group'"
377
+ v-model="fieldValue"
378
+ :options="resolvedOptions"
379
+ :state="fieldState"
380
+ :disabled="isDisabled || isReadonly || isPlaintext"
381
+ v-bind="controlPropsWithGuards"
382
+ />
383
+
373
384
  <!-- File / image upload -->
374
385
  <div v-else-if="field.type === 'image' || field.type === 'file'">
375
386
  <DFormInput
@@ -554,6 +565,7 @@ import DFormInput from "../base/DFormInput.vue";
554
565
  import DFormTextarea from "../base/DFormTextarea.vue";
555
566
  import DFormSelect from "../base/DFormSelect.vue";
556
567
  import DFormRadioGroup from "../base/DFormRadioGroup.vue";
568
+ import DFormCheckboxGroup from "../base/DFormCheckboxGroup.vue";
557
569
  import DFormCheckbox from "../base/DFormCheckbox.vue";
558
570
  import DXSwitch from "./DXSwitch.vue";
559
571
  import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
@@ -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])