@omnitend/dashboard-for-laravel 0.40.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.
Files changed (112) hide show
  1. package/dist/charts.d.ts +19 -0
  2. package/dist/components/base/DAccordion.vue.d.ts +12 -0
  3. package/dist/components/base/DAccordionItem.vue.d.ts +12 -0
  4. package/dist/components/base/DAlert.vue.d.ts +17 -0
  5. package/dist/components/base/DAspect.vue.d.ts +12 -0
  6. package/dist/components/base/DAutocomplete.vue.d.ts +32 -0
  7. package/dist/components/base/DAvatar.vue.d.ts +12 -0
  8. package/dist/components/base/DBadge.vue.d.ts +12 -0
  9. package/dist/components/base/DBreadcrumb.vue.d.ts +12 -0
  10. package/dist/components/base/DButton.vue.d.ts +97 -0
  11. package/dist/components/base/DButtonGroup.vue.d.ts +12 -0
  12. package/dist/components/base/DButtonToolbar.vue.d.ts +12 -0
  13. package/dist/components/base/DCard.vue.d.ts +12 -0
  14. package/dist/components/base/DCol.vue.d.ts +12 -0
  15. package/dist/components/base/DCollapse.vue.d.ts +12 -0
  16. package/dist/components/base/DContainer.vue.d.ts +12 -0
  17. package/dist/components/base/DDropdown.vue.d.ts +12 -0
  18. package/dist/components/base/DDropdownDivider.vue.d.ts +2 -0
  19. package/dist/components/base/DDropdownItem.vue.d.ts +12 -0
  20. package/dist/components/base/DForm.vue.d.ts +12 -0
  21. package/dist/components/base/DFormCheckbox.vue.d.ts +12 -0
  22. package/dist/components/base/DFormCheckboxGroup.vue.d.ts +12 -0
  23. package/dist/components/base/DFormGroup.vue.d.ts +12 -0
  24. package/dist/components/base/DFormInput.vue.d.ts +2 -0
  25. package/dist/components/base/DFormInvalidFeedback.vue.d.ts +12 -0
  26. package/dist/components/base/DFormOtp.vue.d.ts +12 -0
  27. package/dist/components/base/DFormRadio.vue.d.ts +12 -0
  28. package/dist/components/base/DFormRadioGroup.vue.d.ts +12 -0
  29. package/dist/components/base/DFormSelect.vue.d.ts +27 -0
  30. package/dist/components/base/DFormSpinbutton.vue.d.ts +12 -0
  31. package/dist/components/base/DFormTags.vue.d.ts +12 -0
  32. package/dist/components/base/DFormText.vue.d.ts +12 -0
  33. package/dist/components/base/DFormTextarea.vue.d.ts +2 -0
  34. package/dist/components/base/DImage.vue.d.ts +12 -0
  35. package/dist/components/base/DInputGroup.vue.d.ts +12 -0
  36. package/dist/components/base/DLink.vue.d.ts +12 -0
  37. package/dist/components/base/DListGroup.vue.d.ts +12 -0
  38. package/dist/components/base/DListGroupItem.vue.d.ts +12 -0
  39. package/dist/components/base/DModal.vue.d.ts +12 -0
  40. package/dist/components/base/DNav.vue.d.ts +12 -0
  41. package/dist/components/base/DNavItem.vue.d.ts +12 -0
  42. package/dist/components/base/DNavbar.vue.d.ts +12 -0
  43. package/dist/components/base/DNavbarBrand.vue.d.ts +12 -0
  44. package/dist/components/base/DNavbarNav.vue.d.ts +12 -0
  45. package/dist/components/base/DNavbarToggle.vue.d.ts +12 -0
  46. package/dist/components/base/DOffcanvas.vue.d.ts +12 -0
  47. package/dist/components/base/DOverlay.vue.d.ts +12 -0
  48. package/dist/components/base/DPagination.vue.d.ts +2 -0
  49. package/dist/components/base/DPlaceholder.vue.d.ts +12 -0
  50. package/dist/components/base/DPopover.vue.d.ts +12 -0
  51. package/dist/components/base/DProgress.vue.d.ts +12 -0
  52. package/dist/components/base/DRow.vue.d.ts +12 -0
  53. package/dist/components/base/DSpinner.vue.d.ts +2 -0
  54. package/dist/components/base/DTable.vue.d.ts +34 -0
  55. package/dist/components/base/DTabs.vue.d.ts +12 -0
  56. package/dist/components/base/DToast.vue.d.ts +12 -0
  57. package/dist/components/base/DToaster.vue.d.ts +12 -0
  58. package/dist/components/base/DTooltip.vue.d.ts +12 -0
  59. package/dist/components/charts/DXBarChart.vue.d.ts +7 -0
  60. package/dist/components/charts/DXDoughnutChart.vue.d.ts +24 -0
  61. package/dist/components/charts/DXLineChart.vue.d.ts +7 -0
  62. package/dist/components/charts/chartTheme.d.ts +59 -0
  63. package/dist/components/charts/useThemedChart.d.ts +40 -0
  64. package/dist/components/extended/DXBasicForm.vue.d.ts +10 -0
  65. package/dist/components/extended/DXChoiceField.vue.d.ts +26 -0
  66. package/dist/components/extended/DXCurrencyInput.vue.d.ts +42 -0
  67. package/dist/components/extended/DXDashboard.vue.d.ts +105 -0
  68. package/dist/components/extended/DXDashboardNavbar.vue.d.ts +86 -0
  69. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +61 -0
  70. package/dist/components/extended/DXField.vue.d.ts +86 -0
  71. package/dist/components/extended/DXFieldLabel.vue.d.ts +18 -0
  72. package/dist/components/extended/DXFieldShell.vue.d.ts +73 -0
  73. package/dist/components/extended/DXForm.vue.d.ts +112 -0
  74. package/dist/components/extended/DXFormField.vue.d.ts +41 -0
  75. package/dist/components/extended/DXNumericField.vue.d.ts +23 -0
  76. package/dist/components/extended/DXRepeater.vue.d.ts +46 -0
  77. package/dist/components/extended/DXRepeaterCards.vue.d.ts +45 -0
  78. package/dist/components/extended/DXStatCard.vue.d.ts +56 -0
  79. package/dist/components/extended/DXSwitch.vue.d.ts +47 -0
  80. package/dist/components/extended/DXTable.vue.d.ts +495 -0
  81. package/dist/components/extended/DXTableEditorModal.vue.d.ts +100 -0
  82. package/dist/components/extended/DXTablePagination.vue.d.ts +50 -0
  83. package/dist/components/extended/DXTableShell.vue.d.ts +24 -0
  84. package/dist/components/extended/DXUserAvatar.vue.d.ts +40 -0
  85. package/dist/components/extended/dxFieldContext.d.ts +11 -0
  86. package/dist/composables/defineForm.d.ts +41 -0
  87. package/dist/composables/useAsyncOptions.d.ts +20 -0
  88. package/dist/composables/useContainerWidth.d.ts +96 -0
  89. package/dist/composables/useForm.d.ts +46 -0
  90. package/dist/composables/useResourceEditor.d.ts +77 -0
  91. package/dist/composables/useToast.d.ts +2 -0
  92. package/dist/dashboard-for-laravel.js +4166 -4156
  93. package/dist/dashboard-for-laravel.js.map +1 -1
  94. package/dist/dashboard-for-laravel.umd.cjs +5 -5
  95. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  96. package/dist/index.d.ts +93 -0
  97. package/dist/style.css +2 -2
  98. package/dist/types/index.d.ts +477 -0
  99. package/dist/types/navigation.d.ts +43 -0
  100. package/dist/utils/api.d.ts +30 -0
  101. package/dist/utils/formSchema.d.ts +59 -0
  102. package/dist/utils/objectPath.d.ts +18 -0
  103. package/docs/public/api-reference.json +26 -5
  104. package/docs/public/docs-map.md +1 -1
  105. package/package.json +1 -1
  106. package/resources/css/theme.scss +33 -26
  107. package/resources/js/components/base/DAutocomplete.vue +3 -3
  108. package/resources/js/components/base/DButton.vue +39 -5
  109. package/resources/js/components/extended/DXDashboardSidebar.vue +9 -5
  110. package/resources/js/components/extended/DXSwitch.vue +10 -7
  111. package/resources/js/components/extended/DXTable.vue +86 -88
  112. package/resources/js/composables/useResourceEditor.ts +41 -1
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-08-25T19:01:45.537Z",
2
+ "generated": "2026-09-27T10:14:02.393Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.40.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",
@@ -3433,7 +3440,7 @@
3433
3440
  "name": "saveGuard",
3434
3441
  "type": "TSFunctionType",
3435
3442
  "required": false,
3436
- "description": "Guard run when the modal's Save/Create is clicked, before the request.\nReturn a message to abort the save — it is shown immediately (as a toast)\nand no request is made; return `null`/`undefined` to proceed. Covers both\nthe `editUrl` and `createUrl` paths.\n\nAWAITED (as `deleteGuard` is), so it may return a promise: the case it\nexists for is an async control in an `edit-value` slot — an image\nupload — that has not finished when Save is clicked. Without the guard the\nform submits the previous media map, the save SUCCEEDS, the toast says\nsaved and the modal closes: the image is lost with no error anywhere,\nbecause every individual step worked. Await the upload here and the save\nsimply waits for it.\n\n`item` is the row being edited, `null` in create mode (as in the\n`edit-value` slot's binding); `data` is the live form data. Aborting\nleaves the modal open with the user's edits intact. A guard that throws\nalso aborts, surfacing its message."
3443
+ "description": "Guard run when the modal's Save/Create is clicked, before the request.\nReturn a message to abort the save — it is shown immediately (as a toast)\nand no request is made; return `null`/`undefined` to proceed. Covers both\nthe `editUrl` and `createUrl` paths.\n\nAWAITED (as `deleteGuard` is), so it may return a promise: the case it\nexists for is an async control in an `edit-value` slot — an image\nupload — that has not finished when Save is clicked. Without the guard the\nform submits the previous media map, the save SUCCEEDS, the toast says\nsaved and the modal closes: the image is lost with no error anywhere,\nbecause every individual step worked. Await the upload here and the save\nsimply waits for it.\n\n`item` is the row being edited, `null` in create mode (as in the\n`edit-value` slot's binding); `data` is the live form data. Aborting\nleaves the modal open with the user's edits intact. A guard that throws\nalso aborts, surfacing its message.\n\nDismissing the modal while the guard is still pending — by any route —\nabandons the save; nothing is written for a record the user has closed."
3437
3444
  },
3438
3445
  {
3439
3446
  "name": "createUrl",
@@ -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-25T19:01:45.587Z
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.40.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>
@@ -35,7 +35,7 @@
35
35
  </div>
36
36
  </div>
37
37
 
38
- <nav ref="navRef" class="sidebar-nav p-3">
38
+ <nav ref="navRef" class="sidebar-nav px-3 pb-3 pt-2">
39
39
  <template v-for="(group, groupIndex) in navigation" :key="groupIndex">
40
40
  <div
41
41
  v-if="group.visible !== false"
@@ -426,11 +426,15 @@ watch(activeGroupIndex, () => {
426
426
  overflow-x: hidden;
427
427
  }
428
428
 
429
- /* Dense rhythm (#95): values proven in a production dashboard, adopted as the default —
430
- natural-case headers at 0.875rem (no uppercase/letter-spacing), tight group
431
- spacing. */
429
+ /* Rhythm: natural-case headers at 0.875rem (no uppercase/letter-spacing, #95),
430
+ with a full 1rem between groups so each section reads as its own block.
431
+ The 0.25rem gap #95 shipped with left a group label 4px under the previous
432
+ group's last item — closer to that item than to its own — and the eye
433
+ attached the label to the wrong group. The nav's top padding is reduced to
434
+ match (pt-2), so the first item sits nearer the header than the groups sit
435
+ to each other. */
432
436
  .nav-group {
433
- margin-bottom: 0.25rem;
437
+ margin-bottom: 1rem;
434
438
  }
435
439
 
436
440
  .nav-group-label {
@@ -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 -->
@@ -752,6 +749,9 @@ export interface Props<TItem = any> {
752
749
  * `edit-value` slot's binding); `data` is the live form data. Aborting
753
750
  * leaves the modal open with the user's edits intact. A guard that throws
754
751
  * also aborts, surfacing its message.
752
+ *
753
+ * Dismissing the modal while the guard is still pending — by any route —
754
+ * abandons the save; nothing is written for a record the user has closed.
755
755
  */
756
756
  saveGuard?: (
757
757
  item: TItem | null,
@@ -1315,9 +1315,8 @@ const dottedCellFields = (slots: Record<string, unknown>): TableField[] =>
1315
1315
  /*
1316
1316
  * Per-column widths (#156). A field's `width`/`minWidth` is applied through a
1317
1317
  * `<colgroup>` `<col>`, NOT the header `<th>`: with `table-layout: fixed` the
1318
- * column widths are taken from the FIRST row of cells, and DXTable's inline
1319
- * filter row (`thead-top`) is that first row — so a width on the header `<th>`
1320
- * 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
1321
1320
  * authoritative regardless of row order, in both fixed and auto layout.
1322
1321
  */
1323
1322
  const sizeToCss = (value: string | number): string =>
@@ -2259,7 +2258,7 @@ const refresh = () => {
2259
2258
  *
2260
2259
  * - `head(<key>)` — DXTable draws its own column headers (sort indicators,
2261
2260
  * field hints). Forwarding a consumer's would silently drop those.
2262
- * - `thead-top` — DXTable renders the inline filter row there.
2261
+ * - `thead-top` / `thead-sub` — composed with the below-header filter cells.
2263
2262
  *
2264
2263
  * `head-end(<key>)` is DXTable's OWN additive header slot (#99) and is likewise
2265
2264
  * not forwarded: the inner table has no such slot, and the prefixes below are
@@ -2309,11 +2308,10 @@ const isTableSlot = (name: string) =>
2309
2308
  * invisible (#114). Called from the template, it re-evaluates every render.
2310
2309
  *
2311
2310
  * It also has to EXCLUDE the slots DXTable renders itself: declaring `#thead-top`
2312
- * here (even rendering nothing into it) would override DXTable's own thead-top
2313
- * template, silently dropping the filter row and the consumer's banner (#120).
2311
+ * or `#thead-sub` here would override the composed header/filter slots.
2314
2312
  */
2315
2313
  const forwardableSlotNames = (slots: Record<string, unknown>): string[] =>
2316
- Object.keys(slots).filter((name) => isTableSlot(name));
2314
+ Object.keys(slots).filter((name) => name !== 'thead-sub' && isTableSlot(name));
2317
2315
 
2318
2316
  const tableSlotSignature = (slots: Record<string, unknown>): string => {
2319
2317
  // DXTable's OWN dotted-cell slots count too (#121). bvn captures its cell