@omnitend/dashboard-for-laravel 0.41.0 → 0.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-08-27T14:46:45.385Z",
2
+ "generated": "2026-09-30T05:13:05.340Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.41.0"
5
+ "version": "0.41.2"
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",
@@ -904,6 +911,16 @@
904
911
  "props": [],
905
912
  "events": [],
906
913
  "slots": [
914
+ {
915
+ "name": "default",
916
+ "description": "",
917
+ "bindings": []
918
+ },
919
+ {
920
+ "name": "actions",
921
+ "description": "",
922
+ "bindings": []
923
+ },
907
924
  {
908
925
  "name": "name",
909
926
  "description": "",
@@ -2730,6 +2747,47 @@
2730
2747
  "slots": [],
2731
2748
  "methods": []
2732
2749
  },
2750
+ {
2751
+ "name": "DXLoading",
2752
+ "category": "extended",
2753
+ "filePath": "resources/js/components/extended/DXLoading.vue",
2754
+ "description": "",
2755
+ "props": [
2756
+ {
2757
+ "name": "text",
2758
+ "type": "string",
2759
+ "required": false,
2760
+ "default": "\"Loading…\"",
2761
+ "description": ""
2762
+ },
2763
+ {
2764
+ "name": "delay",
2765
+ "type": "number",
2766
+ "required": false,
2767
+ "default": "300",
2768
+ "description": ""
2769
+ }
2770
+ ],
2771
+ "events": [],
2772
+ "slots": [],
2773
+ "methods": []
2774
+ },
2775
+ {
2776
+ "name": "DXModalActions",
2777
+ "category": "extended",
2778
+ "filePath": "resources/js/components/extended/DXModalActions.vue",
2779
+ "description": "Shared secondary modal actions; distinct from the modal footer controls.",
2780
+ "props": [],
2781
+ "events": [],
2782
+ "slots": [
2783
+ {
2784
+ "name": "default",
2785
+ "description": "",
2786
+ "bindings": []
2787
+ }
2788
+ ],
2789
+ "methods": []
2790
+ },
2733
2791
  {
2734
2792
  "name": "DXNumericField",
2735
2793
  "category": "extended",
@@ -3072,7 +3130,7 @@
3072
3130
  "name": "DXSwitch",
3073
3131
  "category": "extended",
3074
3132
  "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.",
3133
+ "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 muted red-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
3134
  "props": [
3077
3135
  {
3078
3136
  "name": "modelValue",
@@ -3110,7 +3168,7 @@
3110
3168
  "name": "size",
3111
3169
  "type": "\"sm\"",
3112
3170
  "required": false,
3113
- "description": "`\"sm\"` renders a compact box (a few px shorter than the default, which\nmatches the input height) for dense rows that repeat the switch per item (a\nper-line visibility toggle in a modal). Omit for the standard size, which\nlines the switch up with the `.form-control`\nheight so a switch lines up with inputs beside it."
3171
+ "description": "`\"sm\"` renders a compact rounded pill (a few px shorter than the default, which\nmatches the input height) for dense rows that repeat the switch per item (a\nper-line visibility toggle in a modal). Omit for the standard size, which\nlines the switch up with the `.form-control`\nheight so a switch lines up with inputs beside it."
3114
3172
  }
3115
3173
  ],
3116
3174
  "events": [
@@ -3596,7 +3654,7 @@
3596
3654
  },
3597
3655
  {
3598
3656
  "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.",
3657
+ "description": "Rows above the column headers, such as grouped-column banners or pinned totals. Give it `<tr>`s.",
3600
3658
  "bindings": [
3601
3659
  {
3602
3660
  "title": "binding",
@@ -3616,6 +3674,20 @@
3616
3674
  }
3617
3675
  ]
3618
3676
  },
3677
+ {
3678
+ "name": "thead-sub",
3679
+ "description": "",
3680
+ "bindings": [
3681
+ {
3682
+ "name": "field",
3683
+ "title": "binding"
3684
+ },
3685
+ {
3686
+ "name": "v-bind",
3687
+ "title": "binding"
3688
+ }
3689
+ ]
3690
+ },
3619
3691
  {
3620
3692
  "name": "`head-end(${field.key})`",
3621
3693
  "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.",
@@ -3979,7 +4051,7 @@
3979
4051
  "name": "DXTablePagination",
3980
4052
  "category": "extended",
3981
4053
  "filePath": "resources/js/components/extended/DXTablePagination.vue",
3982
- "description": "Internal pagination + per-page footer for `DXTable`. Renders the pager, the per-page selector, and the \"X to Y out of Z items\" info line (with the \"Filtered from N\" note when a column filter is active). Extracted because `DXTable` used to carry three byte-identical copies of this footer — one per data mode (client-side / inertia / provider) — that differed only in which `PaginationData` object and page-change handler they used, so every footer fix had to be written three times (#123). `DXTable` now derives the active pagination + handler and renders this once. It owns its own `:deep(.pagination …)` styling (rather than leaving it in `DXTable`), because a scoped `:deep()` in `DXTable` would not reach the pager once it lives in a child component — the scope-id doesn't cross the boundary. Anchoring the rules on this component's own `.dx-table-pagination` root (a plain element it owns) gives the scope-id a deterministic host. In a row too narrow for the full windowed pager, the number strip collapses to a \"current / last\" status with Previous/Next either side (#162). The decision is CONTAINER-driven, not viewport-driven — see `compactThreshold`.",
4054
+ "description": "Pagination + per-page footer shared by `DXTable` and standalone tables. Renders the pager, the per-page selector, and the \"X to Y out of Z items\" info line (with the \"Filtered from N\" note when a column filter is active). Extracted because `DXTable` used to carry three byte-identical copies of this footer — one per data mode (client-side / inertia / provider) — that differed only in which `PaginationData` object and page-change handler they used, so every footer fix had to be written three times (#123). `DXTable` now derives the active pagination + handler and renders this once. It owns its own `:deep(.pagination …)` styling (rather than leaving it in `DXTable`), because a scoped `:deep()` in `DXTable` would not reach the pager once it lives in a child component — the scope-id doesn't cross the boundary. Anchoring the rules on this component's own `.dx-table-pagination` root (a plain element it owns) gives the scope-id a deterministic host. In a row too narrow for the full windowed pager, the number strip collapses to a \"current / last\" status with Previous/Next either side (#162). The decision is CONTAINER-driven, not viewport-driven — see `compactThreshold`.",
3983
4055
  "props": [
3984
4056
  {
3985
4057
  "name": "pagination",
@@ -4142,8 +4214,8 @@
4142
4214
  ]
4143
4215
  },
4144
4216
  "stats": {
4145
- "totalComponents": 84,
4217
+ "totalComponents": 86,
4146
4218
  "baseComponents": 60,
4147
- "extendedComponents": 24
4219
+ "extendedComponents": 26
4148
4220
  }
4149
4221
  }
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-08-27T14:46:45.443Z
4
+ > Last updated: 2026-09-30T05:13:05.394Z
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 73 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
5
+ > A dual-package library (NPM + Composer) providing 76 Vue 3 components for building Laravel dashboards with Bootstrap Vue Next.
6
6
 
7
7
  ## Installation
8
8
 
@@ -84,7 +84,7 @@ Lightweight type-safe wrappers around Bootstrap Vue Next components providing AP
84
84
  - [DToaster](/components/base/dtoaster): Bootstrap Vue Next Toaster wrapper
85
85
  - [DTooltip](/components/base/dtooltip): Bootstrap Vue Next Tooltip wrapper
86
86
 
87
- ## Extended Components (13 components)
87
+ ## Extended Components (16 components)
88
88
 
89
89
  Custom dashboard components with advanced functionality beyond Bootstrap Vue Next.
90
90
 
@@ -96,10 +96,13 @@ Custom dashboard components with advanced functionality beyond Bootstrap Vue Nex
96
96
  - [DXField](/components/extended/dxfield): Single-field renderer for any field type with value/span/info/hint slots
97
97
  - [DXFieldLabel](/components/extended/dxfieldlabel): Extended dashboard component
98
98
  - [DXForm](/components/extended/dxform): Form renderer driven by field definitions, with optional tabs, conditional fields, per-field slots, async options, nested repeaters, and auto error-tab switching
99
+ - [DXLoading](/components/extended/dxloading): Extended dashboard component
100
+ - [DXModalActions](/components/extended/dxmodalactions): Extended dashboard component
99
101
  - [DXRepeater](/components/extended/dxrepeater): Repeatable nested sub-form (field array) primitive
100
102
  - [DXStatCard](/components/extended/dxstatcard): Extended dashboard component
101
103
  - [DXSwitch](/components/extended/dxswitch): Extended dashboard component
102
104
  - [DXTable](/components/extended/dxtable): Data table with pagination, filtering, and sorting
105
+ - [DXTablePagination](/components/extended/dxtablepagination): Extended dashboard component
103
106
  - [DXUserAvatar](/components/extended/dxuseravatar): Extended dashboard component
104
107
 
105
108
  ## Composables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
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,17 @@ $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
+
697
+ // Column help uses the table's header colour rather than the form-label ink.
698
+ .table > thead th .dx-field-label__text {
699
+ color: inherit;
700
+ }
701
+
692
702
  .table > thead {
693
703
  vertical-align: middle;
694
704
  }
@@ -702,13 +712,9 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
702
712
  // as the header token above), sized and inset with `padding-left` so text
703
713
  // clears it.
704
714
  //
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"]) {
715
+ // Select filters keep their chevron without a second icon or extra text inset.
716
+ // Date inputs keep their own picker affordance.
717
+ .dx-table-filter-cell > input.form-control:not([type="date"]) {
712
718
  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
719
  background-repeat: no-repeat;
714
720
  background-position: left 0.65rem center;
@@ -749,7 +755,7 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
749
755
  // theme `@import`s `bootstrap-icons/font/bootstrap-icons` (declaring the
750
756
  // `@font-face`), and #77's post-build extraction ships that woff2 with the
751
757
  // stylesheet — so this needs no new dependency and no data-URI. Values signed
752
- // off after one padding iteration in the greendragon interim; the glyph metrics
758
+ // off after one padding iteration in the interim UI; the glyph metrics
753
759
  // are tunable via the variables below.
754
760
  $dx-dropdown-caret-size: 0.75em; // glyph size relative to the label
755
761
  $dx-dropdown-caret-gap: 0.4em; // space between label and chevron
@@ -805,22 +811,16 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
805
811
  --dx-switch-off-well: #{color.mix($dx-switch-off, #fff, 10%)};
806
812
  --dx-switch-off-line: #{color.mix($dx-switch-off, #fff, 45%)};
807
813
  --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
814
+ --dx-switch-pill: var(--bs-gray-400, #ced4da);
815
+ --dx-switch-off-pill: #{color.mix(map.get(map.get($dx-variants, "danger"), "soft-text"), $gray-400, 50%)};
816
+ --dx-switch-on-pill: #{color.mix(map.get(map.get($dx-variants, "success"), "soft-text"), $gray-400, 50%)};
809
817
  }
810
818
 
811
- // Optically centre the check/radio/switch box against its label's FIRST line.
812
- // Bootstrap's `.25em` top margin is mathematically right for a 1em box in a
813
- // 1.5em line box, but Poppins seats its glyphs low in the line box, so the box
814
- // reads visibly HIGH beside the text (same optical-metrics issue as the B14
815
- // dropdown caret). `0.325em` lands the box centre on Poppins' visual centre —
816
- // judged against a rendered filmstrip (0.25/0.30/0.325/0.35em) with Poppins
817
- // loaded, not from the maths. A top MARGIN (not flex-centring) is deliberate:
818
- // it aligns to the first line, so a multi-line label keeps the box on line one
819
- // rather than floating to the block's vertical centre. DXSwitch is unaffected —
820
- // it flex-centres and zeroes this margin on its inner input at higher
821
- // specificity (`.dx-switch :deep(.form-check-input) { margin: 0 }`).
819
+ // Centre checkboxes/radios on the first label line using their actual size.
820
+ // The old em offset assumed a 1em box; these controls are now 1.25rem tall.
821
+ // A margin keeps wrapped labels aligned to line one rather than the whole block.
822
822
  .form-check-input {
823
- margin-top: 0.325em;
823
+ margin-top: calc((1lh - #{$form-check-input-width}) / 2);
824
824
  }
825
825
 
826
826
  .form-switch .form-check-input {
@@ -1046,3 +1046,8 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
1046
1046
  margin-left: 0;
1047
1047
  }
1048
1048
  }
1049
+
1050
+ // Keep the compact filter-cell spacing when BTable supplies the subheader row.
1051
+ thead tr:has(.dx-table-filter-cell) > td {
1052
+ padding: 0.5rem;
1053
+ }
@@ -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>
@@ -1,11 +1,22 @@
1
1
  <template>
2
- <BModal v-bind="$attrs">
3
- <template v-for="(_, name) in $slots" #[name]="slotProps">
2
+ <BModal size="lg" v-bind="$attrs">
3
+ <template #default="slotProps">
4
+ <slot v-bind="slotProps" />
5
+ <DXModalActions v-if="$slots.actions" class="mt-3">
6
+ <slot name="actions" v-bind="slotProps" />
7
+ </DXModalActions>
8
+ </template>
9
+ <template v-for="name in forwardedSlots" #[name]="slotProps">
4
10
  <slot :name="name" v-bind="slotProps" />
5
11
  </template>
6
12
  </BModal>
7
13
  </template>
8
14
 
9
15
  <script setup lang="ts">
16
+ import { computed, useSlots } from "vue";
10
17
  import { BModal } from "bootstrap-vue-next";
18
+ import DXModalActions from "../extended/DXModalActions.vue";
19
+
20
+ const slots = useSlots();
21
+ const forwardedSlots = computed(() => Object.keys(slots).filter(name => name !== "default" && name !== "actions"));
11
22
  </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div
3
+ v-if="isVisible"
4
+ class="dx-loading text-center text-muted py-5"
5
+ role="status"
6
+ aria-live="polite"
7
+ >
8
+ <DSpinner variant="primary" aria-hidden="true" />
9
+ <div class="mt-2">{{ text }}</div>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { onBeforeUnmount, onMounted, ref } from "vue";
15
+ import DSpinner from "../base/DSpinner.vue";
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ text?: string;
20
+ delay?: number;
21
+ }>(),
22
+ {
23
+ text: "Loading…",
24
+ delay: 300,
25
+ },
26
+ );
27
+
28
+ const isVisible = ref(false);
29
+ let showTimer: ReturnType<typeof setTimeout> | undefined;
30
+
31
+ onMounted(() => {
32
+ if (props.delay <= 0) {
33
+ isVisible.value = true;
34
+ return;
35
+ }
36
+
37
+ showTimer = setTimeout(() => {
38
+ isVisible.value = true;
39
+ }, props.delay);
40
+ });
41
+
42
+ onBeforeUnmount(() => {
43
+ if (showTimer !== undefined) {
44
+ clearTimeout(showTimer);
45
+ }
46
+ });
47
+ </script>
@@ -0,0 +1,13 @@
1
+ <!-- Shared secondary modal actions; distinct from the modal footer controls. -->
2
+ <template>
3
+ <div class="dx-modal-actions"><slot /></div>
4
+ </template>
5
+
6
+ <style scoped>
7
+ .dx-modal-actions {
8
+ display: flex;
9
+ flex-direction: column;
10
+ align-items: stretch;
11
+ gap: 0.75rem;
12
+ }
13
+ </style>
@@ -5,8 +5,8 @@
5
5
  An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized
6
6
  like a `.form-control` with the label on the left and the toggle on the right,
7
7
  colour-coding its state. By default (`onVariant="success"`) it reads the house
8
- green-on / red-off style — a neutral box + red toggle when off, a soft-green
9
- box + green toggle when on. `onVariant="neutral"` (brand primary on / grey off)
8
+ green-on / red-off style: a light-red box with a muted red-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.
@@ -64,7 +64,7 @@ interface Props {
64
64
  */
65
65
  onVariant?: "success" | "neutral";
66
66
  /**
67
- * `"sm"` renders a compact box (a few px shorter than the default, which
67
+ * `"sm"` renders a compact rounded pill (a few px shorter than the default, which
68
68
  * matches the input height) for dense rows that repeat the switch per item (a
69
69
  * per-line visibility toggle in a modal). Omit for the standard size, which
70
70
  * lines the switch up with the `.form-control`
@@ -147,6 +147,7 @@ defineOptions({
147
147
  chrome tightens. */
148
148
  .dx-switch--sm :deep(.form-check) {
149
149
  font-size: 0.875rem;
150
+ border-radius: var(--bs-border-radius-pill, 50rem);
150
151
  min-height: auto;
151
152
  padding: 0.25rem 0.625rem;
152
153
  }
@@ -168,11 +169,15 @@ defineOptions({
168
169
  cursor: pointer;
169
170
  }
170
171
 
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
172
+ /* Filled box (#158): the WHOLE control carries the colour and the pill uses a
173
+ muted state tint — the box IS the state, so it reads strongest at a
173
174
  glance (the house style). `success` (default) is green ON / light-red OFF;
174
175
  `neutral` keeps a brand primary panel for mixed-semantics switches. Pair with
175
176
  `textWhenTrue`/`textWhenFalse` so the LABEL also names each state. */
177
+ .dx-switch--success {
178
+ --dx-switch-pill: var(--dx-switch-off-pill);
179
+ }
180
+
176
181
  .dx-switch--success :deep(.form-check) {
177
182
  background-color: var(--dx-switch-off-bg);
178
183
  border-color: var(--dx-switch-off-line);
@@ -189,12 +194,14 @@ defineOptions({
189
194
 
190
195
  .dx-switch--success.dx-switch--on :deep(.form-check-label) {
191
196
  color: var(--dx-switch-on-ink);
192
- font-weight: 500;
193
197
  }
194
198
 
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
199
+ .dx-switch--success.dx-switch--on {
200
+ --dx-switch-pill: var(--dx-switch-on-pill);
201
+ }
202
+
203
+ /* Muted pill with a white sliding thumb: green-grey ON, red-grey OFF.
204
+ The stronger box colour and position continue to communicate the state. Explicit `:checked`/`:focus` selectors so they out-specify the global
198
205
  `.form-switch .form-check-input:checked` rule. */
199
206
  .dx-switch--success :deep(.form-check-input),
200
207
  .dx-switch--success :deep(.form-check-input:checked),
@@ -217,6 +224,5 @@ defineOptions({
217
224
 
218
225
  .dx-switch--neutral.dx-switch--on :deep(.form-check-label) {
219
226
  color: var(--bs-primary-text-emphasis);
220
- font-weight: 500;
221
227
  }
222
228
  </style>