@omnitend/dashboard-for-laravel 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-07-20T07:09:59.465Z",
2
+ "generated": "2026-07-20T09:56:11.731Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.30.0"
5
+ "version": "0.32.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -2636,7 +2636,7 @@
2636
2636
  "name": "DXSwitch",
2637
2637
  "category": "extended",
2638
2638
  "filePath": "resources/js/components/extended/DXSwitch.vue",
2639
- "description": "DXSwitch — a \"filled box\" toggle switch. An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized like a `.form-control` with the label on the left and the toggle on the right, colour-coding its state (a neutral box when off, a filled primary box when on). Used standalone or, internally, by `DXField`'s `switch` field type so a form switch and a lone switch share one look. Usage: ```vue <DXSwitch v-model=\"isCurrent\" label=\"Product is current\" /> ``` For a form field, prefer a `DXField`/`DXForm` `type: 'switch'`; reach for `DXSwitch` directly when you want the styled toggle outside a form. Pass a `label` prop for simple text, or the default slot for richer label content.",
2639
+ "description": "DXSwitch — a \"filled box\" toggle switch. An opinionated switch built on `DFormCheckbox` (`switch`): a bordered box sized like a `.form-control` with the label on the left and the toggle on the right, colour-coding its state. By default (`onVariant=\"success\"`) it reads the house green-on / red-off style — a neutral box + red toggle when off, a soft-green box + green toggle when on. `onVariant=\"neutral\"` (brand primary on / grey off) is for semantically-mixed switches that shouldn't imply good/bad (#158). Used standalone or, internally, by `DXField`'s `switch` field type so a form switch and a lone switch share one look. Usage: ```vue <DXSwitch v-model=\"isCurrent\" label=\"Product is current\" /> ``` For a form field, prefer a `DXField`/`DXForm` `type: 'switch'`; reach for `DXSwitch` directly when you want the styled toggle outside a form. Pass a `label` prop for simple text, or the default slot for richer label content.",
2640
2640
  "props": [
2641
2641
  {
2642
2642
  "name": "modelValue",
@@ -2662,6 +2662,13 @@
2662
2662
  "type": "string",
2663
2663
  "required": false,
2664
2664
  "description": "Text shown when off — overrides `label` in the off state (e.g. \"Product is not current\")."
2665
+ },
2666
+ {
2667
+ "name": "onVariant",
2668
+ "type": "union",
2669
+ "required": false,
2670
+ "default": "\"success\"",
2671
+ "description": "On-state colour. `\"success\"` (default) reads the house green-on / red-off\nstyle — an active/enabled/good switch (\"Product is current\", \"Stock is\ntracked\", \"Visible\"). `\"neutral\"` is for semantically-mixed switches\n(\"contains alcohol\", an allergen toggle, \"hidden on web shop\") that\nshouldn't imply good/bad — it uses the brand primary on / grey off (#158)."
2665
2672
  }
2666
2673
  ],
2667
2674
  "events": [
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-07-20T07:09:59.531Z
4
+ > Last updated: 2026-07-20T09:56:11.791Z
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.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -16,9 +16,9 @@
16
16
 
17
17
  // Base $theme-colors carry each hue's READABLE-ON-LIGHT identity (its "emphasis"
18
18
  // shade) — this is what `.text-*`, `.link-*`, `.border-*` and outline buttons
19
- // use, so every one must clear AA on white. Bold solid FILLS (primary/danger
20
- // buttons) and SOFT tints (soft buttons, badges, alerts) are applied as explicit
21
- // overrides after the Bootstrap import, from the $dx-variants map. See
19
+ // use, so every one must clear AA on white. The one bold solid FILL (the primary
20
+ // button) and SOFT tints (soft buttons incl. danger, badges, alerts) are applied
21
+ // as explicit overrides after the Bootstrap import, from the $dx-variants map. See
22
22
  // plans/2026-07-18-semantic-colour-system.md.
23
23
  $primary: #151e2d; // Omni Tend brand navy (16.73:1 on white)
24
24
  $secondary: #475569; // Slate-600 (7.58:1 on white)
@@ -168,16 +168,22 @@ $badge-padding-x: 0.625rem;
168
168
  $badge-font-weight: 500;
169
169
 
170
170
  // The ONE source of truth for the semantic colour system. Each variant carries
171
- // its SOLID fill + same-hue text (primary/danger buttons, solid fills), its SOFT
171
+ // its SOLID fill + same-hue text (the primary button, solid fills), its SOFT
172
172
  // tint pair (soft buttons, badges, alerts, status), its EMPHASIS shade (outline
173
173
  // buttons, coloured links, .text-*), and whether its button is solid or soft.
174
174
  // The overrides after the Bootstrap import loop over this map. All pairs are
175
175
  // WCAG AA verified. See plans/2026-07-18-semantic-colour-system.md.
176
+ //
177
+ // Only PRIMARY is a bold solid button — one loud action (the brand). Everything
178
+ // else, INCLUDING danger, is soft: a soft red "Delete" reads as danger without
179
+ // a heavy fill, and one solid button per screen keeps the hierarchy clear. (Was
180
+ // primary+danger solid; danger's soft was also an off-hue plum — it is now a
181
+ // same-hue light red, matching the danger emphasis and the switch OFF.)
176
182
  $dx-variants: (
177
183
  "primary": (solid-bg: #151e2d, solid-text: #e9f0f8, soft-bg: #e9f0f8, soft-text: #151e2d, emphasis: #151e2d, button: "solid"),
178
184
  "secondary": (solid-bg: #475569, solid-text: #e6ebf2, soft-bg: #e6ebf2, soft-text: #29374a, emphasis: #475569, button: "soft"),
179
185
  "success": (solid-bg: #84cc16, solid-text: #203b0e, soft-bg: #cdf9b2, soft-text: #203b0e, emphasis: #4d7c0f, button: "soft"),
180
- "danger": (solid-bg: #dc2626, solid-text: #ffffff, soft-bg: #f5dff1, soft-text: #59194a, emphasis: #dc2626, button: "solid"),
186
+ "danger": (solid-bg: #dc2626, solid-text: #ffffff, soft-bg: #f8d4d4, soft-text: #7a1a1a, emphasis: #dc2626, button: "soft"),
181
187
  "warning": (solid-bg: #f59e0b, solid-text: #512d05, soft-bg: #fce5c4, soft-text: #512d05, emphasis: #b45309, button: "soft"),
182
188
  "info": (solid-bg: #2563eb, solid-text: #eef4ff, soft-bg: #deebff, soft-text: #12376c, emphasis: #2563eb, button: "soft"),
183
189
  );
@@ -456,10 +462,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
456
462
  // ----------------------------------------------------------------------------
457
463
  // One loop over $dx-variants applies the whole system on top of Bootstrap:
458
464
  //
459
- // - Buttons: primary/danger are SOLID (fill from the base colour; we only
465
+ // - Buttons: only PRIMARY is SOLID (fill from the base colour; we only
460
466
  // override the label to the same-hue tint, letting Bootstrap shade hover/
461
- // active). secondary/success/warning/info are SOFT (light tint bg + dark
462
- // text, fully overridden incl. hover/active).
467
+ // active). secondary/success/danger/warning/info are SOFT (light tint bg +
468
+ // dark text, fully overridden incl. hover/active).
463
469
  // - Outline buttons: the emphasis shade for border/label, the soft tint on hover.
464
470
  // - Badges: `.badge.text-bg-*` re-tinted soft — needs `!important` because
465
471
  // Bootstrap's `.text-bg-*` helper sets its colours `!important` by design.
@@ -516,8 +522,15 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
516
522
  --bs-btn-active-border-color: #{$emphasis};
517
523
  }
518
524
 
519
- // Badge — soft tint. `!important` beats Bootstrap's `.text-bg-*` helper.
520
- .badge.text-bg-#{$name} {
525
+ // `.text-bg-*` — soft tint, everywhere (not just `.badge`). Soft-first is the
526
+ // default across the board: a `.badge`, an `.input-group-text`, a chip or a
527
+ // small status bar built with `.text-bg-success` all get the same soft green
528
+ // as the badge, so a stock indicator can match a "current" badge without
529
+ // hardcoding the hue. `!important` beats Bootstrap's `.text-bg-*` helper
530
+ // (which sets its colours `!important` by design). `.toast` is excluded: it
531
+ // has its own soft handling (a fainter 50%-white mix via `--bs-toast-bg` for
532
+ // its large surface) and themed toasts don't carry `.text-bg-*` anyway.
533
+ .text-bg-#{$name}:not(.toast) {
521
534
  background-color: #{$soft-bg} !important;
522
535
  color: #{$soft-text} !important;
523
536
  }
@@ -534,9 +547,23 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
534
547
  // colour IS its emphasis shade, so Bootstrap already renders them legible.
535
548
  }
536
549
 
550
+ // Publish each variant's soft/emphasis shades as CSS variables so components
551
+ // (e.g. DXSwitch's on-box) and consumers can reference the exact soft-first
552
+ // tints on any element — Bootstrap's own `--bs-*-bg-subtle` are a different,
553
+ // greyer family derived from the dark emphasis base, so they don't match the
554
+ // soft badge/alert green. These are the same values the $dx-variants loop paints.
555
+ :root {
556
+ @each $name, $v in $dx-variants {
557
+ --dx-#{$name}-soft-bg: #{map.get($v, "soft-bg")};
558
+ --dx-#{$name}-soft-text: #{map.get($v, "soft-text")};
559
+ --dx-#{$name}-solid-bg: #{map.get($v, "solid-bg")};
560
+ --dx-#{$name}-emphasis: #{map.get($v, "emphasis")};
561
+ }
562
+ }
563
+
537
564
  // primary is near-black navy, so Bootstrap's darken-on-hover is imperceptible —
538
- // the main action button would have no visible hover feedback. Lighten instead.
539
- // (danger's red darkens visibly, so the generic solid path is fine for it.)
565
+ // the main (and now only) solid button would have no visible hover feedback.
566
+ // Lighten instead. (Other variants are soft; their hover is handled in the loop.)
540
567
  .btn-primary {
541
568
  --bs-btn-hover-bg: #{color.scale($primary, $lightness: 20%)};
542
569
  --bs-btn-hover-border-color: #{color.scale($primary, $lightness: 20%)};
@@ -557,6 +584,93 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
557
584
  }
558
585
  }
559
586
 
587
+ // ----------------------------------------------------------------------------
588
+ // Switches — green when ON, red when OFF, by default (#158)
589
+ // ----------------------------------------------------------------------------
590
+ // House style (James): an active switch — "Product is current", "Stock is
591
+ // tracked", "Visible" — should read success GREEN when on and RED when off,
592
+ // OUT OF THE BOX, not only when a consumer opts in. Bootstrap's default is a
593
+ // primary (navy) track when on / grey when off. We flip the default here.
594
+ //
595
+ // Semantically-MIXED switches ("contains alcohol", an allergen toggle, "hidden
596
+ // on web shop") shouldn't imply good/bad — those opt OUT with `.switch-neutral`
597
+ // on the `.form-switch`, which restores Bootstrap's grey-off / primary-on.
598
+ //
599
+ // Applies to any bare `.form-switch` (DFormCheckbox `switch`); DXSwitch is the
600
+ // "filled box" form where the whole control (not the toggle) carries the colour
601
+ // and the pill stays a neutral affordance — it reads the same tokens below.
602
+ $dx-switch-on: map.get(map.get($dx-variants, "success"), "solid-bg"); // vivid lime
603
+ $dx-switch-off: map.get(map.get($dx-variants, "danger"), "solid-bg"); // vivid red
604
+ $dx-switch-thumb-white: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
605
+ // A red thumb reads better than white on the light-red OFF track.
606
+ $dx-switch-thumb-red: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23dc2626'/%3e%3c/svg%3e");
607
+
608
+ // Switch fill tokens. The ON fill is the SAME soft green as the success
609
+ // badge/button (its `soft-bg`), so a switch and a success badge read as one
610
+ // green — not a separate, punchier lime. OFF is a LIGHT red (a light tint of the
611
+ // danger solid, NOT the plum `danger` soft — a switch off wants to read "red /
612
+ // stop", which the plum status tint doesn't). Derived from the base tokens so
613
+ // one edit moves them. Used by the DXSwitch filled box; the OFF track reuses them.
614
+ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cdf9b2
615
+ :root {
616
+ --dx-switch-on-bg: #{$dx-switch-on-soft};
617
+ --dx-switch-on-well: #{color.mix($dx-switch-on-soft, #fff, 55%)};
618
+ --dx-switch-on-line: #{color.mix($dx-switch-on-soft, $dx-switch-on, 55%)};
619
+ --dx-switch-on-ink: #{map.get(map.get($dx-variants, "success"), "soft-text")};
620
+ --dx-switch-off-bg: #{color.mix($dx-switch-off, #fff, 20%)};
621
+ --dx-switch-off-well: #{color.mix($dx-switch-off, #fff, 10%)};
622
+ --dx-switch-off-line: #{color.mix($dx-switch-off, #fff, 45%)};
623
+ --dx-switch-off-ink: #{color.mix($dx-switch-off, #000, 30%)};
624
+ --dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral pill in both states
625
+ }
626
+
627
+ .form-switch .form-check-input {
628
+ // OFF: light-red track, red thumb (left).
629
+ background-color: var(--dx-switch-off-bg);
630
+ border-color: var(--dx-switch-off-line);
631
+ --bs-form-switch-bg: #{$dx-switch-thumb-red};
632
+
633
+ &:checked {
634
+ // ON: vivid green track, white thumb (right — Bootstrap positions it).
635
+ background-color: #{$dx-switch-on};
636
+ border-color: #{$dx-switch-on};
637
+ --bs-form-switch-bg: #{$dx-switch-thumb-white};
638
+ }
639
+
640
+ // Neutral grey focus ring (the track carries the state colour, so the brand
641
+ // navy ring would clash); keep the state thumb rather than Bootstrap's navy.
642
+ &:focus {
643
+ --bs-form-switch-bg: #{$dx-switch-thumb-red};
644
+ border-color: var(--dx-switch-off-line);
645
+ box-shadow: 0 0 0 0.25rem rgba($dark, 0.15);
646
+ }
647
+
648
+ &:checked:focus {
649
+ border-color: #{$dx-switch-on};
650
+ --bs-form-switch-bg: #{$dx-switch-thumb-white};
651
+ }
652
+ }
653
+
654
+ // Neutral opt-out for mixed-semantics switches: restore Bootstrap's grey-off /
655
+ // primary-on (no good/bad valence).
656
+ .form-switch.switch-neutral .form-check-input {
657
+ background-color: var(--bs-body-bg);
658
+ border-color: var(--bs-border-color);
659
+ // Bootstrap's default faint-grey OFF thumb.
660
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
661
+
662
+ &:checked {
663
+ background-color: var(--bs-primary);
664
+ border-color: var(--bs-primary);
665
+ --bs-form-switch-bg: #{$dx-switch-thumb-white};
666
+ }
667
+
668
+ &:focus {
669
+ border-color: var(--bs-primary);
670
+ box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
671
+ }
672
+ }
673
+
560
674
  // ----------------------------------------------------------------------------
561
675
  // Chart palette (data-viz)
562
676
  // ----------------------------------------------------------------------------
@@ -159,6 +159,7 @@
159
159
  <DXSwitch
160
160
  v-else
161
161
  v-model="switchModel"
162
+ :on-variant="field.switchVariant"
162
163
  :disabled="isDisabled || isReadonly || isPlaintext"
163
164
  v-bind="controlPropsWithGuards"
164
165
  >
@@ -201,6 +202,7 @@
201
202
  <DXSwitch
202
203
  v-else
203
204
  v-model="switchModel"
205
+ :on-variant="field.switchVariant"
204
206
  :disabled="isDisabled || isReadonly || isPlaintext"
205
207
  v-bind="controlPropsWithGuards"
206
208
  >
@@ -4,9 +4,12 @@
4
4
 
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
- colour-coding its state (a neutral box when off, a filled primary box when on).
8
- Used standalone or, internally, by `DXField`'s `switch` field type so a form
9
- switch and a lone switch share one look.
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)
10
+ is for semantically-mixed switches that shouldn't imply good/bad (#158). Used
11
+ standalone or, internally, by `DXField`'s `switch` field type so a form switch
12
+ and a lone switch share one look.
10
13
 
11
14
  Usage:
12
15
  ```vue
@@ -18,8 +21,16 @@
18
21
  `label` prop for simple text, or the default slot for richer label content.
19
22
  -->
20
23
  <template>
21
- <div class="dx-switch" :class="{ 'dx-switch--on': isOn }">
22
- <DFormCheckbox v-model="model" switch v-bind="$attrs">
24
+ <div class="dx-switch" :class="[`dx-switch--${onVariant}`, { 'dx-switch--on': isOn }]">
25
+ <!-- For the neutral variant, tag the inner `.form-switch` with
26
+ `switch-neutral` so the global theme restores grey-off / primary-on for
27
+ the toggle itself (the box styling below handles the wrapper). -->
28
+ <DFormCheckbox
29
+ v-model="model"
30
+ switch
31
+ :class="onVariant === 'neutral' ? 'switch-neutral' : undefined"
32
+ v-bind="$attrs"
33
+ >
23
34
  <!--
24
35
  @slot Label content shown beside the toggle. Falls back to the contextual
25
36
  text / `label` prop. Scoped with the current on-state so a caller can vary
@@ -44,10 +55,19 @@ interface Props {
44
55
  textWhenTrue?: string;
45
56
  /** Text shown when off — overrides `label` in the off state (e.g. "Product is not current"). */
46
57
  textWhenFalse?: string;
58
+ /**
59
+ * On-state colour. `"success"` (default) reads the house green-on / red-off
60
+ * style — an active/enabled/good switch ("Product is current", "Stock is
61
+ * tracked", "Visible"). `"neutral"` is for semantically-mixed switches
62
+ * ("contains alcohol", an allergen toggle, "hidden on web shop") that
63
+ * shouldn't imply good/bad — it uses the brand primary on / grey off (#158).
64
+ */
65
+ onVariant?: "success" | "neutral";
47
66
  }
48
67
 
49
68
  const props = withDefaults(defineProps<Props>(), {
50
69
  modelValue: false,
70
+ onVariant: "success",
51
71
  });
52
72
 
53
73
  const emit = defineEmits<{
@@ -128,26 +148,55 @@ defineOptions({
128
148
  cursor: pointer;
129
149
  }
130
150
 
131
- /* On-state: filled primary. Green is reserved for success semantics, so a
132
- generic "on" toggle uses the brand action colour, matching the base switch,
133
- checkboxes and radios. */
134
- .dx-switch--on :deep(.form-check) {
135
- background-color: var(--bs-primary-bg-subtle);
136
- border-color: var(--bs-primary);
151
+ /* Filled box (#158): the WHOLE control carries the colour and the pill stays a
152
+ neutral grey affordance — the box IS the state, so it reads strongest at a
153
+ glance (the house style). `success` (default) is green ON / light-red OFF;
154
+ `neutral` keeps a brand primary panel for mixed-semantics switches. Pair with
155
+ `textWhenTrue`/`textWhenFalse` so the LABEL also names each state. */
156
+ .dx-switch--success :deep(.form-check) {
157
+ background-color: var(--dx-switch-off-bg);
158
+ border-color: var(--dx-switch-off-line);
137
159
  }
138
160
 
139
- .dx-switch--on :deep(.form-check-label) {
140
- color: var(--bs-primary-text-emphasis);
161
+ .dx-switch--success :deep(.form-check-label) {
162
+ color: var(--dx-switch-off-ink);
163
+ }
164
+
165
+ .dx-switch--success.dx-switch--on :deep(.form-check) {
166
+ background-color: var(--dx-switch-on-bg);
167
+ border-color: var(--dx-switch-on-line);
168
+ }
169
+
170
+ .dx-switch--success.dx-switch--on :deep(.form-check-label) {
171
+ color: var(--dx-switch-on-ink);
141
172
  font-weight: 500;
142
173
  }
143
174
 
144
- .dx-switch--on :deep(.form-check-input:checked) {
145
- background-color: var(--bs-primary);
146
- border-color: var(--bs-primary);
175
+ /* Neutral grey pill in BOTH states (white thumb, sliding) — overrides the global
176
+ green-on/red-off toggle colours within the box, so only the box carries the
177
+ state. Explicit `:checked`/`:focus` selectors so they out-specify the global
178
+ `.form-switch .form-check-input:checked` rule. */
179
+ .dx-switch--success :deep(.form-check-input),
180
+ .dx-switch--success :deep(.form-check-input:checked),
181
+ .dx-switch--success :deep(.form-check-input:focus) {
182
+ background-color: var(--dx-switch-pill);
183
+ border-color: var(--dx-switch-pill);
184
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
147
185
  }
148
186
 
149
- .dx-switch--on :deep(.form-check-input:focus) {
187
+ .dx-switch--success :deep(.form-check-input:focus) {
188
+ box-shadow: 0 0 0 0.2rem rgba(0, 0, 0, 0.12);
189
+ }
190
+
191
+ /* neutral variant (mixed-semantics): brand primary panel when on; the toggle is
192
+ handled by `.switch-neutral` on the inner form-switch (primary-on / grey-off). */
193
+ .dx-switch--neutral.dx-switch--on :deep(.form-check) {
194
+ background-color: var(--bs-primary-bg-subtle);
150
195
  border-color: var(--bs-primary);
151
- box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
196
+ }
197
+
198
+ .dx-switch--neutral.dx-switch--on :deep(.form-check-label) {
199
+ color: var(--bs-primary-text-emphasis);
200
+ font-weight: 500;
152
201
  }
153
202
  </style>
@@ -19,16 +19,40 @@
19
19
  <template>
20
20
  <div class="dx-table-pagination mt-3">
21
21
  <!-- Top row: Pagination and Per-page selector -->
22
- <div class="d-flex justify-content-between align-items-center mb-2">
23
- <!-- Pagination controls (only when multiple pages) -->
24
- <DPagination
22
+ <div class="dx-pager-row d-flex justify-content-between align-items-center gap-2 mb-2">
23
+ <!-- Windowed pager (#155): «Previous / leading / … / window / … /
24
+ trailing / Next». Client-computed from current + last page, so it
25
+ works in every DXTable mode. Only shown when >1 page. -->
26
+ <nav
25
27
  v-if="showPagination && pagination.total > pagination.per_page"
26
- :model-value="pagination.current_page"
27
- :total-rows="pagination.total"
28
- :per-page="pagination.per_page"
29
- size="sm"
30
- @update:model-value="emit('page-change', $event as number)"
31
- />
28
+ class="dx-pager d-flex flex-wrap align-items-center gap-1"
29
+ aria-label="Pagination"
30
+ >
31
+ <DButton
32
+ variant="outline-secondary"
33
+ size="sm"
34
+ :disabled="!canPrev"
35
+ @click="goToPage(currentPage - 1)"
36
+ >&laquo; Previous</DButton>
37
+
38
+ <template v-for="(item, index) in pageItems" :key="item.type === 'page' ? `p${item.page}` : `e${index}`">
39
+ <DButton
40
+ v-if="item.type === 'page'"
41
+ size="sm"
42
+ :variant="item.active ? 'primary' : 'outline-secondary'"
43
+ :aria-current="item.active ? 'page' : undefined"
44
+ @click="goToPage(item.page)"
45
+ >{{ item.page }}</DButton>
46
+ <span v-else class="dx-pager-ellipsis" aria-hidden="true">&hellip;</span>
47
+ </template>
48
+
49
+ <DButton
50
+ variant="outline-secondary"
51
+ size="sm"
52
+ :disabled="!canNext"
53
+ @click="goToPage(currentPage + 1)"
54
+ >Next &raquo;</DButton>
55
+ </nav>
32
56
  <div v-else></div>
33
57
 
34
58
  <!-- Per-page selector -->
@@ -67,7 +91,8 @@
67
91
  </template>
68
92
 
69
93
  <script setup lang="ts">
70
- import DPagination from "../base/DPagination.vue";
94
+ import { computed } from "vue";
95
+ import DButton from "../base/DButton.vue";
71
96
  import DFormSelect from "../base/DFormSelect.vue";
72
97
  import type { PaginationData } from "./DXTable.vue";
73
98
 
@@ -92,7 +117,7 @@ interface Props {
92
117
  hasActiveFilters: boolean;
93
118
  }
94
119
 
95
- defineProps<Props>();
120
+ const props = defineProps<Props>();
96
121
 
97
122
  const emit = defineEmits<{
98
123
  /** The user picked a page. Payload is the new 1-based page number. */
@@ -104,26 +129,95 @@ const emit = defineEmits<{
104
129
  */
105
130
  'per-page-change': [value: number | string | null | (number | string)[]];
106
131
  }>();
132
+
133
+ // Windowed pager (#155). Always show the first/last BOUNDARY pages and a window
134
+ // of AROUND pages each side of the current page; fill gaps with an ellipsis,
135
+ // except a single-page gap which shows the page itself (an ellipsis hiding one
136
+ // page is silly). Client-computed from current + last page so it works in every
137
+ // DXTable mode (provider / client-side / inertia) — not just where a server
138
+ // `links` array exists. Reproduces custard's `1 2 … 8 9 10 [11] 12 13 14 … 44 45`.
139
+ const BOUNDARY = 2;
140
+ const AROUND = 3;
141
+
142
+ type PageItem = { type: "page"; page: number; active: boolean } | { type: "ellipsis" };
143
+
144
+ const currentPage = computed(() => props.pagination.current_page);
145
+ const lastPage = computed(() =>
146
+ Math.max(1, Math.ceil(props.pagination.total / props.pagination.per_page)),
147
+ );
148
+
149
+ const pageItems = computed<PageItem[]>(() => {
150
+ const last = lastPage.value;
151
+ const current = currentPage.value;
152
+
153
+ const shown = new Set<number>();
154
+ // Leading + trailing boundary pages (1,2 … last-1,last).
155
+ for (let page = 1; page <= Math.min(BOUNDARY, last); page += 1) shown.add(page);
156
+ for (let page = Math.max(1, last - BOUNDARY + 1); page <= last; page += 1) shown.add(page);
157
+ // A window around the current page. When it clamps at an edge, push the other
158
+ // edge out by the shortfall so the row stays a full width near the start/end
159
+ // (a fuller run of numbers, like the house pager) instead of going sparse.
160
+ let windowLow = current - AROUND;
161
+ let windowHigh = current + AROUND;
162
+ const shortfall = 2 * AROUND + 1 - (Math.min(last, windowHigh) - Math.max(1, windowLow) + 1);
163
+ if (shortfall > 0 && windowLow < 1) windowHigh += shortfall;
164
+ else if (shortfall > 0 && windowHigh > last) windowLow -= shortfall;
165
+ for (let page = Math.max(1, windowLow); page <= Math.min(last, windowHigh); page += 1) {
166
+ shown.add(page);
167
+ }
168
+
169
+ const sorted = [...shown].sort((a, b) => a - b);
170
+ const items: PageItem[] = [];
171
+ let previous = 0;
172
+ for (const page of sorted) {
173
+ if (page - previous === 2) {
174
+ // Exactly one page missing — show it rather than an ellipsis.
175
+ items.push({ type: "page", page: previous + 1, active: previous + 1 === current });
176
+ } else if (page - previous > 2) {
177
+ items.push({ type: "ellipsis" });
178
+ }
179
+ items.push({ type: "page", page, active: page === current });
180
+ previous = page;
181
+ }
182
+ return items;
183
+ });
184
+
185
+ const canPrev = computed(() => currentPage.value > 1);
186
+ const canNext = computed(() => currentPage.value < lastPage.value);
187
+
188
+ const goToPage = (page: number) => {
189
+ if (page < 1 || page > lastPage.value || page === currentPage.value) return;
190
+ emit("page-change", page);
191
+ };
107
192
  </script>
108
193
 
109
194
  <style scoped>
110
- /* Pagination button sizing to match form controls. Anchored on this component's
111
- own `.dx-table-pagination` root so the scope-id has a plain-element host — a
112
- `:deep()` rule that targeted the pager from DXTable would not reach it once
113
- the pager lives here (the scope-id doesn't cross the component boundary). */
114
- .dx-table-pagination :deep(.pagination) {
115
- margin-bottom: 0;
195
+ /* Windowed pager (#155). The buttons are DButtons; give the numbered ones a
196
+ consistent min-width so they read as an even row of keys (like the house
197
+ custard pager), and let the whole row wrap on narrow widths rather than
198
+ overflow. `:deep(.btn)` is anchored on the plain `.dx-pager` host, so the
199
+ scope-id has a deterministic element to attach to. */
200
+ .dx-pager :deep(.btn) {
201
+ min-width: 2.5rem;
202
+ }
203
+
204
+ /* Previous / Next carry text, so they shouldn't be forced square. */
205
+ .dx-pager :deep(.btn):first-child,
206
+ .dx-pager :deep(.btn):last-child {
207
+ min-width: auto;
116
208
  }
117
209
 
118
- .dx-table-pagination :deep(.pagination-sm .page-link) {
119
- min-width: 2.25rem;
120
- height: auto;
210
+ .dx-pager-ellipsis {
211
+ display: inline-flex;
212
+ align-items: flex-end;
213
+ padding: 0 0.15rem;
214
+ color: var(--bs-secondary-color);
215
+ user-select: none;
121
216
  }
122
217
 
123
- /* Make disabled pagination buttons more subtle */
124
- .dx-table-pagination :deep(.pagination .page-item.disabled .page-link) {
125
- background-color: transparent;
126
- border-color: transparent;
127
- opacity: 0.3;
218
+ /* On narrow screens the row can get long; allow it to scroll horizontally as a
219
+ fallback so it never blows out the table width. */
220
+ .dx-pager-row {
221
+ flex-wrap: wrap;
128
222
  }
129
223
  </style>
@@ -196,6 +196,15 @@ export interface FieldDefinition {
196
196
  */
197
197
  textWhenFalse?: MaybeFn<string>;
198
198
 
199
+ /**
200
+ * For `switch` fields: the on-state colour. `"success"` (default) is the
201
+ * house green-on / red-off style for an active/enabled/good switch;
202
+ * `"neutral"` (brand primary on / grey off) is for semantically-mixed
203
+ * switches that shouldn't imply good/bad ("contains alcohol", an allergen
204
+ * toggle, "hidden on web shop"). See DXSwitch `onVariant` (#158).
205
+ */
206
+ switchVariant?: "success" | "neutral";
207
+
199
208
  /** CSS class for the form group */
200
209
  class?: string;
201
210