@omnitend/dashboard-for-laravel 0.41.2 → 0.42.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 (55) hide show
  1. package/README.md +2 -1
  2. package/dist/assets/MavenPro-Bold-35b16f06.woff2 +0 -0
  3. package/dist/assets/MavenPro-Medium-febc8674.woff2 +0 -0
  4. package/dist/assets/MavenPro-Regular-aee3df9e.woff2 +0 -0
  5. package/dist/assets/MavenPro-SemiBold-c555dbfa.woff2 +0 -0
  6. package/dist/assets/Poppins-Medium-5800fa57.woff2 +0 -0
  7. package/dist/assets/Poppins-SemiBold-94a36267.woff2 +0 -0
  8. package/dist/charts.js +148 -130
  9. package/dist/charts.js.map +1 -1
  10. package/dist/charts.umd.cjs +1 -1
  11. package/dist/charts.umd.cjs.map +1 -1
  12. package/dist/components/base/DAutocomplete.vue.d.ts +2 -2
  13. package/dist/components/charts/chartTheme.d.ts +14 -2
  14. package/dist/components/extended/DXField.vue.d.ts +16 -5
  15. package/dist/components/extended/DXForm.vue.d.ts +25 -9
  16. package/dist/components/extended/DXNumberStepper.vue.d.ts +33 -0
  17. package/dist/components/extended/DXSaveButton.vue.d.ts +38 -0
  18. package/dist/components/extended/DXTable.vue.d.ts +6 -2
  19. package/dist/composables/useForm.d.ts +7 -0
  20. package/dist/dashboard-for-laravel.js +9317 -9052
  21. package/dist/dashboard-for-laravel.js.map +1 -1
  22. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  23. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/style.css +2 -2
  26. package/dist/types/index.d.ts +20 -3
  27. package/docs/public/api-reference.json +165 -14
  28. package/docs/public/docs-map.md +4 -2
  29. package/docs/public/llms.txt +4 -2
  30. package/package.json +1 -1
  31. package/resources/css/theme.scss +558 -89
  32. package/resources/fonts/maven-pro/MavenPro-Bold.woff2 +0 -0
  33. package/resources/fonts/maven-pro/MavenPro-Medium.woff2 +0 -0
  34. package/resources/fonts/maven-pro/MavenPro-Regular.woff2 +0 -0
  35. package/resources/fonts/maven-pro/MavenPro-SemiBold.woff2 +0 -0
  36. package/resources/fonts/maven-pro/OFL.txt +93 -0
  37. package/resources/fonts/poppins/OFL.txt +93 -0
  38. package/resources/fonts/poppins/Poppins-Medium.woff2 +0 -0
  39. package/resources/fonts/poppins/Poppins-SemiBold.woff2 +0 -0
  40. package/resources/js/components/base/DAutocomplete.vue +73 -2
  41. package/resources/js/components/charts/DXBarChart.vue +2 -1
  42. package/resources/js/components/charts/DXDoughnutChart.vue +2 -1
  43. package/resources/js/components/charts/DXLineChart.vue +2 -1
  44. package/resources/js/components/charts/chartTheme.ts +68 -21
  45. package/resources/js/components/extended/DXDashboard.vue +4 -2
  46. package/resources/js/components/extended/DXDashboardSidebar.vue +52 -20
  47. package/resources/js/components/extended/DXField.vue +59 -28
  48. package/resources/js/components/extended/DXForm.vue +122 -21
  49. package/resources/js/components/extended/DXNumberStepper.vue +243 -0
  50. package/resources/js/components/extended/DXSaveButton.vue +82 -0
  51. package/resources/js/components/extended/DXTable.vue +44 -15
  52. package/resources/js/composables/useForm.ts +11 -0
  53. package/resources/js/composables/useToast.ts +1 -1
  54. package/resources/js/index.ts +2 -0
  55. package/resources/js/types/index.ts +21 -3
@@ -9,6 +9,7 @@
9
9
  @use "sass:color";
10
10
  @use "sass:list";
11
11
  @use "sass:map";
12
+ @use "sass:math";
12
13
 
13
14
  // ----------------------------------------------------------------------------
14
15
  // Colors
@@ -17,21 +18,23 @@
17
18
  // Base $theme-colors carry each hue's READABLE-ON-LIGHT identity (its "emphasis"
18
19
  // shade) — this is what `.text-*`, `.link-*`, `.border-*` and outline buttons
19
20
  // 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
+ // button) and SOFT tints (soft buttons incl. danger, badges) are applied
21
22
  // as explicit overrides after the Bootstrap import, from the $dx-variants map. See
22
23
  // plans/2026-07-18-semantic-colour-system.md.
23
24
  $primary: #151e2d; // Omni Tend brand navy (16.73:1 on white)
24
25
  $secondary: #475569; // Slate-600 (7.58:1 on white)
25
- $success: #4d7c0f; // Lime-700 emphasis (4.99:1 on white); soft/vivid tints in $dx-variants
26
- $danger: #dc2626; // Red-600 (4.83:1 on white)
27
- $warning: #b45309; // Amber-700 emphasis (5.02:1 on white)
28
- $info: #2563eb; // Blue-600 (5.17:1 on white)
26
+ $success: #236b12; // Deep green emphasis (6.60:1 on white); vivid/soft tints in $dx-variants
27
+ $danger: #a3247f; // Deep magenta emphasis (6.74:1 on white)
28
+ $warning: #8a6d00; // Deep ochre emphasis (4.92:1 on white)
29
+ $info: #31586d; // Deep slate-teal emphasis (7.64:1 on white)
29
30
  $light: #f8fafc; // Slate-50 (uses dark text)
30
31
  $dark: #151e2d; // Brand navy — sidebar/high-contrast surfaces (was Slate-900 #0f172a)
31
32
 
32
- // Links are the info-blue, independent of the near-black brand primary (a navy
33
- // link would read as bold text, not a link).
34
- $link-color: #2563eb;
33
+ // Links are the brand navy (16.73:1 on white). A navy link would read as bold
34
+ // text if it lost its underline, so links KEEP Bootstrap's default underline
35
+ // ($link-decoration is not overridden here; only `.btn-link`, the ghost
36
+ // button, drops it). The underline, not the hue, is what marks a link.
37
+ $link-color: #151e2d;
35
38
  $link-shade-percentage: 15%;
36
39
 
37
40
  // ----------------------------------------------------------------------------
@@ -47,7 +50,18 @@ $color-contrast-light: #fff; // Light text color (for dark backgrounds)
47
50
  // Typography
48
51
  // ----------------------------------------------------------------------------
49
52
 
50
- $font-family-base: "Poppins", sans-serif;
53
+ // Two bundled typefaces (SIL OFL 1.1; woff2 + licence in resources/fonts/):
54
+ // MAVEN PRO for body text, and POPPINS as the DISPLAY face, published as
55
+ // `--dx-font-family-display`. The theme applies the display face to h1–h4 and
56
+ // nothing else; consumers use the token for things like product names. Both
57
+ // are declared with `@font-face` below, so they render on every machine, not
58
+ // only where the font happens to be installed (the theme used to name Poppins
59
+ // and ship no face for it, so most visitors got the generic sans-serif).
60
+ $font-family-sans-serif: "Maven Pro", system-ui, -apple-system, "Segoe UI", Roboto,
61
+ "Helvetica Neue", Arial, sans-serif;
62
+ $dx-font-family-display: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto,
63
+ "Helvetica Neue", Arial, sans-serif;
64
+ $headings-font-family: var(--dx-font-family-display);
51
65
  $font-size-base: 1rem; // 16px — the default tier. Legacy omnitend rendered body
52
66
  // and table text at 16px; the earlier 0.875rem (14px) shrank every ported page's
53
67
  // text ~12% while rem-fixed chrome stayed put, so controls read oversized and
@@ -56,14 +70,19 @@ $font-size-base: 1rem; // 16px — the default tier. Legacy omnitend rendered bo
56
70
  $font-weight-base: 400;
57
71
  $line-height-base: 1.5;
58
72
 
59
- $h1-font-size: 2rem; // 32px
60
- $h2-font-size: 1.75rem; // 28px
61
- $h3-font-size: 1.5rem; // 24px
62
- $h4-font-size: 1.25rem; // 20px
63
- $h5-font-size: 1.125rem; // 18px
73
+ // A compact, restrained heading scale for dense admin screens: chosen from
74
+ // rendered comparisons against legacy omnitend, which set headings at weight
75
+ // 500. The old 2rem…1.25rem at 600 shouted over the data. h1 is at Bootstrap's
76
+ // RFS threshold (1.25rem), so no heading is rescaled on narrow viewports. h5
77
+ // and h6 sit at body size so neither outranks h4.
78
+ $h1-font-size: 1.25rem; // 20px
79
+ $h2-font-size: 1.15rem; // 18.4px
80
+ $h3-font-size: 1.05rem; // 16.8px
81
+ $h4-font-size: 1.05rem; // 16.8px
82
+ $h5-font-size: 1rem; // 16px
64
83
  $h6-font-size: 1rem; // 16px
65
84
 
66
- $headings-font-weight: 600;
85
+ $headings-font-weight: 500;
67
86
 
68
87
  // ----------------------------------------------------------------------------
69
88
  // Spacing
@@ -137,7 +156,9 @@ $card-border-radius: $border-radius;
137
156
  $card-border-color: $border-color;
138
157
  $card-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
139
158
  0 1px 2px 0 rgba(0, 0, 0, 0.06);
140
- $card-cap-bg: transparent;
159
+ // A quiet blue-tinted cap (legacy Omni Tend's card header) so a card's title
160
+ // reads as a band above its body rather than floating on the same white.
161
+ $card-cap-bg: #f6f8fd;
141
162
  $card-spacer-y: 1.5rem; // 24px vertical padding
142
163
  $card-spacer-x: 1.5rem; // 24px horizontal padding
143
164
 
@@ -145,6 +166,11 @@ $card-spacer-x: 1.5rem; // 24px horizontal padding
145
166
  // Tables
146
167
  // ----------------------------------------------------------------------------
147
168
 
169
+ // Body cells take the body text colour (#212529) rather than Bootstrap's
170
+ // `--bs-emphasis-color` (pure black), and header cells a medium weight instead
171
+ // of the browser's bold. Header COLOUR is `--dx-table-header-color` (below).
172
+ $table-color: var(--bs-body-color);
173
+ $table-th-font-weight: 500;
148
174
  $table-cell-padding-y: 0.75rem;
149
175
  $table-cell-padding-x: 1rem;
150
176
  $table-striped-bg: #f9fafb; // Gray-50
@@ -177,25 +203,135 @@ $badge-font-weight: 500;
177
203
 
178
204
  // The ONE source of truth for the semantic colour system. Each variant carries
179
205
  // its SOLID fill + same-hue text (the primary button, solid fills), its SOFT
180
- // tint pair (soft buttons, badges, alerts, status), its EMPHASIS shade (outline
181
- // buttons, coloured links, .text-*), and whether its button is solid or soft.
206
+ // tint pair (soft buttons, badges, status; alerts use its paler subtle mix),
207
+ // its EMPHASIS shade (outline buttons, coloured links, .text-*), and whether
208
+ // its button is solid or soft.
182
209
  // The overrides after the Bootstrap import loop over this map. All pairs are
183
210
  // WCAG AA verified. See plans/2026-07-18-semantic-colour-system.md.
184
211
  //
185
212
  // Only PRIMARY is a bold solid button — one loud action (the brand). Everything
186
- // else, INCLUDING danger, is soft: a soft red "Delete" reads as danger without
187
- // a heavy fill, and one solid button per screen keeps the hierarchy clear. (Was
188
- // primary+danger solid; danger's soft was also an off-hue plum — it is now a
189
- // same-hue light red, matching the danger emphasis and the switch OFF.)
213
+ // else, INCLUDING danger, is soft: a soft magenta "Delete" reads as danger
214
+ // without a heavy fill, and one solid button per screen keeps the hierarchy
215
+ // clear.
216
+ //
217
+ // The status hues are the "fruity" palette (2026-10): a vivid lime, magenta,
218
+ // butter yellow and sky blue for the solid fills, light tints of the same hues
219
+ // for the soft pair, and a deep shade of each for the emphasis. Every label is
220
+ // a dark same-hue ink (no white text on a status fill). Contrast: solid text on
221
+ // solid fill 5.50:1 (danger) to 12.67:1 (warning); soft text on soft fill
222
+ // 9.93:1 to 14.56:1; emphasis on white 4.92:1 (warning) to 16.73:1. Warning's
223
+ // soft pair IS its solid pair: the butter yellow is already light enough to be
224
+ // the tint.
225
+ //
226
+ // PENDING is a status of its own: waiting, and the next move is not yours
227
+ // ("awaiting payment", "sent to printer, not yet confirmed"). It is a violet
228
+ // from the chart lavender (#b9a3f0 is chart slot 5), kept clear of info's
229
+ // slate blue by hue and by its violet ink. Contrast: solid 7.06:1, soft
230
+ // 9.10:1, emphasis 6.54:1 on white, soft text on its subtle (alert) tint
231
+ // 11.58:1.
190
232
  $dx-variants: (
191
233
  "primary": (solid-bg: #151e2d, solid-text: #e9f0f8, soft-bg: #e9f0f8, soft-text: #151e2d, emphasis: #151e2d, button: "solid"),
192
234
  "secondary": (solid-bg: #475569, solid-text: #e6ebf2, soft-bg: #e6ebf2, soft-text: #29374a, emphasis: #475569, button: "soft"),
193
- "success": (solid-bg: #84cc16, solid-text: #203b0e, soft-bg: #cdf9b2, soft-text: #203b0e, emphasis: #4d7c0f, button: "soft"),
194
- "danger": (solid-bg: #dc2626, solid-text: #ffffff, soft-bg: #f8d4d4, soft-text: #7a1a1a, emphasis: #dc2626, button: "soft"),
195
- "warning": (solid-bg: #f59e0b, solid-text: #512d05, soft-bg: #fce5c4, soft-text: #512d05, emphasis: #b45309, button: "soft"),
196
- "info": (solid-bg: #2563eb, solid-text: #eef4ff, soft-bg: #deebff, soft-text: #12376c, emphasis: #2563eb, button: "soft"),
235
+ "success": (solid-bg: #7bf25a, solid-text: #153c04, soft-bg: #c3faaa, soft-text: #153c04, emphasis: #236b12, button: "soft"),
236
+ "danger": (solid-bg: #e46ab9, solid-text: #3d0a2f, soft-bg: #f9dff2, soft-text: #61124c, emphasis: #a3247f, button: "soft"),
237
+ "warning": (solid-bg: #efd574, solid-text: #121419, soft-bg: #efd574, soft-text: #121419, emphasis: #8a6d00, button: "soft"),
238
+ "info": (solid-bg: #7fd7fd, solid-text: #192547, soft-bg: #d5dcf0, soft-text: #192547, emphasis: #31586d, button: "soft"),
239
+ "pending": (solid-bg: #b9a3f0, solid-text: #2a1260, soft-bg: #e3d3fb, soft-text: #3b1a80, emphasis: #6a43c4, button: "soft"),
240
+ );
241
+
242
+ // Bootstrap's base map plus `pending` (its emphasis, like every other base
243
+ // colour), so Bootstrap generates `.btn-pending`, `.btn-outline-pending`,
244
+ // `.text-bg-pending`, `.text-pending`, `.link-pending`, `.border-pending`,
245
+ // `.alert-pending` and `.list-group-item-pending` itself. The subtle maps get
246
+ // their `pending` entries just before the Bootstrap import (they only exist
247
+ // once Bootstrap's own variables and maps have been read).
248
+ $theme-colors: (
249
+ "primary": $primary,
250
+ "secondary": $secondary,
251
+ "success": $success,
252
+ "info": $info,
253
+ "warning": $warning,
254
+ "danger": $danger,
255
+ "pending": map.get(map.get($dx-variants, "pending"), "emphasis"),
256
+ "light": $light,
257
+ "dark": $dark,
258
+ );
259
+
260
+ // Bootstrap's "subtle" family (`.bg-*-subtle`, `.border-*-subtle`,
261
+ // `.text-*-emphasis`, list-group and table variants) is derived from the SAME
262
+ // soft tints, so a subtle surface is a paler cousin of the badge rather than a
263
+ // separate, greyer family Bootstrap would otherwise mix from the dark emphasis
264
+ // base. Set here, before the import, so Bootstrap's own maps pick them up.
265
+ //
266
+ // The subtle background is the soft tint mixed with white so the result is 70%
267
+ // white. On real pages legacy omnitend's tinted table rows measure about that
268
+ // (danger #fdf6fb, warning #fbf3d8); the earlier 50% mix read too strong for a
269
+ // row or an alert, which are large surfaces.
270
+ // Sass cannot create variables in a loop, hence one line per variable; the
271
+ // functions keep the three formulas in one place. Dark mode's `*-dark`
272
+ // variables are left to Bootstrap.
273
+ @function dx-soft($name, $key) {
274
+ @return map.get(map.get($dx-variants, $name), $key);
275
+ }
276
+ @function dx-subtle-bg($name) {
277
+ @return color.mix(#fff, dx-soft($name, "soft-bg"), 70%);
278
+ }
279
+ @function dx-subtle-border($name) {
280
+ @return color.scale(dx-soft($name, "soft-bg"), $lightness: -10%);
281
+ }
282
+
283
+ $primary-bg-subtle: dx-subtle-bg("primary");
284
+ $primary-border-subtle: dx-subtle-border("primary");
285
+ $primary-text-emphasis: dx-soft("primary", "soft-text");
286
+ $secondary-bg-subtle: dx-subtle-bg("secondary");
287
+ $secondary-border-subtle: dx-subtle-border("secondary");
288
+ $secondary-text-emphasis: dx-soft("secondary", "soft-text");
289
+ $success-bg-subtle: dx-subtle-bg("success");
290
+ $success-border-subtle: dx-subtle-border("success");
291
+ $success-text-emphasis: dx-soft("success", "soft-text");
292
+ $danger-bg-subtle: dx-subtle-bg("danger");
293
+ $danger-border-subtle: dx-subtle-border("danger");
294
+ $danger-text-emphasis: dx-soft("danger", "soft-text");
295
+ $warning-bg-subtle: dx-subtle-bg("warning");
296
+ $warning-border-subtle: dx-subtle-border("warning");
297
+ $warning-text-emphasis: dx-soft("warning", "soft-text");
298
+ $info-bg-subtle: dx-subtle-bg("info");
299
+ $info-border-subtle: dx-subtle-border("info");
300
+ $info-text-emphasis: dx-soft("info", "soft-text");
301
+
302
+ // Table row variants (`.table-success` etc.) use the same 70%-white tints.
303
+ // Bootstrap would otherwise shift each base colour toward white, which for the
304
+ // dark emphasis bases gives a muddy row tint.
305
+ $table-variants: (
306
+ "primary": dx-subtle-bg("primary"),
307
+ "secondary": dx-subtle-bg("secondary"),
308
+ "success": dx-subtle-bg("success"),
309
+ "danger": dx-subtle-bg("danger"),
310
+ "warning": dx-subtle-bg("warning"),
311
+ "info": dx-subtle-bg("info"),
312
+ "pending": dx-subtle-bg("pending"),
313
+ "light": $light,
314
+ "dark": $dark,
197
315
  );
198
316
 
317
+ // Form errors are crimson, NOT the danger magenta: an invalid field has to
318
+ // read as "wrong" at a glance, which the playful magenta badge hue does not.
319
+ // #c8102e is 5.88:1 on white. Danger badges and buttons stay magenta.
320
+ $dx-form-error: #c8102e;
321
+ $form-invalid-color: $dx-form-error;
322
+ $form-invalid-border-color: $dx-form-error;
323
+ $form-feedback-invalid-color: $dx-form-error;
324
+ $form-feedback-icon-invalid-color: $dx-form-error;
325
+
326
+ // Width of a horizontal field's label column when the caller sets no
327
+ // `labelCols` (DXField/DXForm). 45% sits between Bootstrap's 5/12 and 6/12
328
+ // steps on purpose: chosen from rendered options, it lets a label with help
329
+ // text wrap far less than the old 3/12 without starving the input. Applies
330
+ // from `sm` up; below `sm` the label stacks. Published as
331
+ // `--dx-form-label-width` so a consumer can retune it in CSS. An explicit
332
+ // `labelCols` uses the 12-column grid instead and ignores this.
333
+ $dx-form-label-width: 45%;
334
+
199
335
  // ----------------------------------------------------------------------------
200
336
  // Dashboard
201
337
  // ----------------------------------------------------------------------------
@@ -204,6 +340,14 @@ $dashboard-sidebar-width: 280px;
204
340
  $dashboard-sidebar-collapsed-width: 80px;
205
341
  $dashboard-navbar-height: 64px;
206
342
 
343
+ // The shell's one horizontal gutter: the navbar's inner padding AND the page
344
+ // content's left/right edge both come from it, so the navbar's first item (the
345
+ // menu toggle, or whatever leads the bar) starts exactly where the page content
346
+ // starts. They used to be 12px and 36px, so nothing on the bar lined up with
347
+ // anything on the page. Published as `--dx-dashboard-gutter-x` on `:root`;
348
+ // a consumer can retune both edges together by overriding that.
349
+ $dashboard-gutter-x: 20px;
350
+
207
351
  // The bar's vertical padding, and the single-row CONTENT BUDGET that follows
208
352
  // from it. Everything on the bar's single row — the avatar toggle, whatever a
209
353
  // consumer puts in the `actions` or `user-icon` slots — has to fit this, or the
@@ -237,6 +381,48 @@ $dashboard-navbar-content-height: calc(
237
381
  // Import Bootstrap
238
382
  // ----------------------------------------------------------------------------
239
383
 
384
+ // `pending` in the subtle family. Bootstrap's subtle maps, and the utility maps
385
+ // behind `.bg-*-subtle`, `.border-*-subtle` and `.text-*-emphasis`, list the
386
+ // eight stock colours by name and are defined from Bootstrap's own variables,
387
+ // so they can only be extended after those are read. Import the configuration
388
+ // layer first, merge, then import Bootstrap as a whole: every variable and map
389
+ // in it is `!default`, so the second read keeps these values and the layer
390
+ // emits no CSS. Dark mode follows Bootstrap's own derivation from the base
391
+ // colour (text tint 40%, background shade 80%, border shade 40%).
392
+ @import "bootstrap/scss/functions";
393
+ @import "bootstrap/scss/variables";
394
+ @import "bootstrap/scss/variables-dark";
395
+ @import "bootstrap/scss/maps";
396
+
397
+ $dx-pending-emphasis: map.get(map.get($dx-variants, "pending"), "emphasis");
398
+ $theme-colors-text: map.merge($theme-colors-text, ("pending": dx-soft("pending", "soft-text")));
399
+ $theme-colors-bg-subtle: map.merge($theme-colors-bg-subtle, ("pending": dx-subtle-bg("pending")));
400
+ $theme-colors-border-subtle: map.merge(
401
+ $theme-colors-border-subtle,
402
+ ("pending": dx-subtle-border("pending"))
403
+ );
404
+ $theme-colors-text-dark: map.merge(
405
+ $theme-colors-text-dark,
406
+ ("pending": tint-color($dx-pending-emphasis, 40%))
407
+ );
408
+ $theme-colors-bg-subtle-dark: map.merge(
409
+ $theme-colors-bg-subtle-dark,
410
+ ("pending": shade-color($dx-pending-emphasis, 80%))
411
+ );
412
+ $theme-colors-border-subtle-dark: map.merge(
413
+ $theme-colors-border-subtle-dark,
414
+ ("pending": shade-color($dx-pending-emphasis, 40%))
415
+ );
416
+ $utilities-text-emphasis-colors: map.merge(
417
+ $utilities-text-emphasis-colors,
418
+ ("pending-emphasis": var(--bs-pending-text-emphasis))
419
+ );
420
+ $utilities-bg-subtle: map.merge($utilities-bg-subtle, ("pending-subtle": var(--bs-pending-bg-subtle)));
421
+ $utilities-border-subtle: map.merge(
422
+ $utilities-border-subtle,
423
+ ("pending-subtle": var(--bs-pending-border-subtle))
424
+ );
425
+
240
426
  @import "bootstrap/scss/bootstrap";
241
427
 
242
428
  // ----------------------------------------------------------------------------
@@ -268,6 +454,53 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
268
454
  format("woff2");
269
455
  @import "bootstrap-icons/font/bootstrap-icons";
270
456
 
457
+ // ----------------------------------------------------------------------------
458
+ // Bundled text fonts
459
+ // ----------------------------------------------------------------------------
460
+ // Maven Pro (body) and Poppins (display), woff2 only, one face per weight the
461
+ // theme uses: Maven Pro 400/500/600/700 (body, `.fw-medium`/`.fw-semibold`,
462
+ // `<strong>`/`.fw-bold`), Poppins 500 (h1–h4) and 600 (`.fw-semibold` on a
463
+ // display heading, e.g. DXDashboardNavbar's page title). A weight not listed
464
+ // here is synthesised by the browser, so add a face rather than relying on it.
465
+ //
466
+ // These are extracted out of `dist/style.css` the same way as the icon font
467
+ // (#77): Vite inlines every CSS-referenced woff2 in library mode, and
468
+ // `scripts/extract-icon-font.mjs` writes each back out as
469
+ // `dist/assets/<file>-<hash>.woff2`. A face downloads only when text actually
470
+ // renders in it. `swap` shows the fallback stack while the file loads rather
471
+ // than invisible text.
472
+ $dx-bundled-fonts: (
473
+ ("Maven Pro", 400, "maven-pro/MavenPro-Regular"),
474
+ ("Maven Pro", 500, "maven-pro/MavenPro-Medium"),
475
+ ("Maven Pro", 600, "maven-pro/MavenPro-SemiBold"),
476
+ ("Maven Pro", 700, "maven-pro/MavenPro-Bold"),
477
+ ("Poppins", 500, "poppins/Poppins-Medium"),
478
+ ("Poppins", 600, "poppins/Poppins-SemiBold"),
479
+ );
480
+
481
+ @each $family, $weight, $file in $dx-bundled-fonts {
482
+ @font-face {
483
+ font-family: $family;
484
+ font-style: normal;
485
+ font-weight: $weight;
486
+ font-display: swap;
487
+ src: url("../fonts/#{$file}.woff2") format("woff2");
488
+ }
489
+ }
490
+
491
+ :root {
492
+ --dx-font-family-display: #{$dx-font-family-display};
493
+ }
494
+
495
+ // Bootstrap gives `$headings-font-family` to h1–h6; the display face is for
496
+ // h1–h4 only, so the two smallest headings stay in the body face.
497
+ h5,
498
+ .h5,
499
+ h6,
500
+ .h6 {
501
+ font-family: var(--bs-body-font-family);
502
+ }
503
+
271
504
  // ----------------------------------------------------------------------------
272
505
  // Forms
273
506
  // ----------------------------------------------------------------------------
@@ -286,6 +519,18 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
286
519
  --dx-input-height: calc(
287
520
  #{$input-line-height * 1em} + #{$input-padding-y * 2} + (2 * #{$input-border-width})
288
521
  );
522
+ --dx-form-label-width: #{$dx-form-label-width};
523
+ }
524
+
525
+ // The default horizontal label column. DXField renders it with BFormGroup's
526
+ // `labelColsSm` (so it is a `col-sm` that stacks below `sm`, as any `labelCols`
527
+ // breakpoint does) plus this modifier, which fixes its width; the input column
528
+ // is a plain `.col` and takes the rest. `.row >` lifts it over `.col-sm`.
529
+ @include media-breakpoint-up(sm) {
530
+ .row > .dx-field-label-col--default-width {
531
+ flex: 0 0 auto;
532
+ width: var(--dx-form-label-width);
533
+ }
289
534
  }
290
535
 
291
536
  // Bootstrap sizes grouped buttons with padding properties, but leaves their
@@ -317,6 +562,92 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
317
562
  }
318
563
  }
319
564
 
565
+ // A horizontal row whose content column holds NO form control shows a
566
+ // display-only value: text, a link, a badge, a flex row of text, a small
567
+ // table, a stack of lines. Such a row is baseline-aligned instead of
568
+ // top-aligned, so the first line of text in the value, whatever its shape,
569
+ // sits on the label's first line (the label's padding just moves the shared
570
+ // baseline down). No class or field flag is needed; `.dx-form-plaintext`
571
+ // below still works and is now redundant inside a horizontal form.
572
+ //
573
+ // Every row that holds a control keeps the top alignment above: a checkbox,
574
+ // switch or file input exposes a synthesized or box-edge baseline, so
575
+ // baseline alignment would move them. The list is what the library renders
576
+ // as a control plus the native ones; a `.btn` counts because it is
577
+ // input-height and already lines its text up with the label when
578
+ // top-aligned. Content with no text at all (an image, an icon) sits its
579
+ // bottom edge on the label's baseline, as an inline image would in a line of
580
+ // text. The rule needs `:has()` (Chrome 105, Safari 15.4, Firefox 121); a
581
+ // browser without it falls back to the top alignment.
582
+ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has(> .col-form-label ~ * :is(input:not([type="hidden"]), select, textarea, button, .btn, .form-control, .form-select, .form-check, .input-group, [contenteditable])))';
583
+
584
+ #{$dx-display-only-row} {
585
+ align-items: baseline;
586
+
587
+ // A table's baseline is its first row's, and a row's baseline is its
588
+ // first BASELINE-aligned cell's; with every cell `middle` (the theme's
589
+ // default) CSS falls back to the bottom of the cell, ~6px below the text.
590
+ // Baseline-align the first row's cells so the table offers its text.
591
+ > .col-form-label ~ * table tr:first-child > * {
592
+ vertical-align: baseline;
593
+ }
594
+
595
+ // A badge's own baseline is its smaller text's, which lands its box ~2px
596
+ // off the label's centre; centre it on its line instead, as
597
+ // `.dx-form-plaintext .badge` does below.
598
+ > .col-form-label ~ * .badge {
599
+ vertical-align: top;
600
+ margin-top: calc(
601
+ (#{math.div($input-line-height, math.div($badge-font-size, 1em)) * 1em} - 1em - #{$badge-padding-y * 2}) / 2
602
+ );
603
+ }
604
+ }
605
+
606
+ // A display-only value in a form row: a badge, a line of text, a short stack
607
+ // of lines. It has no input box, so on its own it sits at the top of the row
608
+ // while the label column (`.col-form-label`) is padded down to an input's text
609
+ // line. This gives it the same offset and line-height Bootstrap gives
610
+ // `.col-form-label` and `.form-control-plaintext`, so its FIRST line shares
611
+ // the label's centre. DXField wraps a `plaintext` field's `value` slot in it;
612
+ // consumers put it on their own display-only content. A `.form-control-
613
+ // plaintext` inside already carries the offset, so it drops its own.
614
+ .dx-form-plaintext {
615
+ padding-top: calc(#{$input-padding-y} + #{$input-border-width});
616
+ padding-bottom: calc(#{$input-padding-y} + #{$input-border-width});
617
+ line-height: $input-line-height;
618
+
619
+ > .form-control-plaintext {
620
+ padding-top: 0;
621
+ padding-bottom: 0;
622
+ border-top-width: 0;
623
+ border-bottom-width: 0;
624
+ }
625
+
626
+ // A badge is an inline-block in a smaller font, so neither `baseline` nor
627
+ // `middle` centres it on the line (both land ~2px off, the way depending on
628
+ // the font's x-height). Pin it to the line's top and push it down by half
629
+ // the spare height: the line is `$input-line-height` parent-ems, the badge
630
+ // is 1em (Bootstrap's `line-height: 1`) plus its padding, in badge-ems.
631
+ .badge {
632
+ vertical-align: top;
633
+ margin-top: calc(
634
+ (#{math.div($input-line-height, math.div($badge-font-size, 1em)) * 1em} - 1em - #{$badge-padding-y * 2}) / 2
635
+ );
636
+ }
637
+ }
638
+
639
+ // Switch-list option rows (DXField `switch-list`) are one line each: a label,
640
+ // a switch and an optional trailing control (a notes input in the
641
+ // `switch-list-item` slot). Centre all three in the row, instead of the
642
+ // top-aligned horizontal rhythm above, so the padded label column, the 24px
643
+ // switch and a taller input share one centre whether or not the input is
644
+ // showing. Comes after `.dx-form--horizontal .row` at the same specificity so
645
+ // it wins. The `.row` is bootstrap-vue-next's, inside the BFormGroup wrapper,
646
+ // so a utility class on the DFormGroup lands on the wrapper and does nothing.
647
+ .dx-switch-list-row .row {
648
+ align-items: center;
649
+ }
650
+
320
651
  // Bootstrap's `.input-group` defaults to `flex-wrap: wrap`, letting a
321
652
  // narrow container (e.g. a compact table cell, a small screen) stack the
322
653
  // prepend/append affix onto its own line below the input. That breaks the
@@ -328,13 +659,44 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
328
659
  flex-wrap: nowrap;
329
660
  }
330
661
 
662
+ // DXNumberStepper: `[ − ][ 12 ][ + ]`. The buttons are DButton icon-only
663
+ // squares, sized by the input group. The number is centred between them, and
664
+ // the browser's spin arrows are hidden (they duplicate the buttons and push the
665
+ // number off centre). Global rather than scoped: the input is a bvn root, where
666
+ // a scoped rule is not reliably reached (see CLAUDE.md, scope-id trap).
667
+ // The input is a fixed few characters wide (the group is inline, so it does
668
+ // not stretch across a table cell); retune with `--dx-number-stepper-input-width`.
669
+ .dx-number-stepper {
670
+ display: inline-flex;
671
+ width: auto;
672
+ vertical-align: middle;
673
+
674
+ > .dx-number-stepper__input {
675
+ flex: 0 1 auto;
676
+ width: var(--dx-number-stepper-input-width, 6ch);
677
+ min-width: 4ch;
678
+ padding-left: 0.25rem;
679
+ padding-right: 0.25rem;
680
+ text-align: center;
681
+ -moz-appearance: textfield;
682
+ appearance: textfield;
683
+
684
+ &::-webkit-inner-spin-button,
685
+ &::-webkit-outer-spin-button {
686
+ -webkit-appearance: none;
687
+ margin: 0;
688
+ }
689
+ }
690
+ }
691
+
331
692
  // ----------------------------------------------------------------------------
332
693
  // Nav tabs
333
694
  // ----------------------------------------------------------------------------
334
695
  // Chrome recedes, content leads — the same ethos as the muted table headers
335
696
  // (#157). Bootstrap gives an INACTIVE tab `--bs-nav-link-color:
336
- // var(--bs-link-color)`, i.e. the saturated link-blue, so a row of tabs above a
337
- // form reads as a row of links competing with the fields below. Mute the
697
+ // var(--bs-link-color)`, i.e. the link colour (brand navy, near-black), so
698
+ // every tab reads as selected and a row of tabs competes with the fields
699
+ // below. Mute the
338
700
  // inactive tabs and let the ACTIVE one (Bootstrap's
339
701
  // `--bs-nav-tabs-link-active-color`, the near-black emphasis colour) carry the
340
702
  // selection on its own.
@@ -358,6 +720,22 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
358
720
  // Dashboard Custom Styles
359
721
  // ----------------------------------------------------------------------------
360
722
 
723
+ :root {
724
+ --dx-dashboard-gutter-x: #{$dashboard-gutter-x};
725
+ }
726
+
727
+ // Both are the `DContainer fluid` directly inside the navbar header and the
728
+ // <main> panel (which keeps only its vertical padding, `py-4`). In the main
729
+ // panel's centred branch the DRow's negative margins and the DCol's padding
730
+ // cancel, so the content edge is this padding in both branches. Two classes
731
+ // so it outranks Bootstrap's own `.container-fluid` padding regardless of
732
+ // stylesheet order.
733
+ .dashboard-navbar > .container-fluid,
734
+ .dashboard-main > .container-fluid {
735
+ padding-left: var(--dx-dashboard-gutter-x);
736
+ padding-right: var(--dx-dashboard-gutter-x);
737
+ }
738
+
361
739
  .dashboard-sidebar {
362
740
  background-color: $dark;
363
741
  width: $dashboard-sidebar-width;
@@ -519,12 +897,12 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
519
897
  // the base `.toast` rule (above, and Bootstrap's own) already reads.
520
898
  //
521
899
  // Sourced from each variant's `soft-bg` in $dx-variants (#143) — the same
522
- // vivid-derived soft family badges/alerts use — so the toast hue tracks the
900
+ // vivid-derived soft family badges use — so the toast hue tracks the
523
901
  // palette and editing a `soft-bg` moves toasts too. Previously these mixed the
524
902
  // dark *emphasis* base colours (greyer success/warning) and danger's saturated
525
903
  // red (a pink, not the soft system's plum). Mixed 50% with white so a large
526
904
  // toast surface stays fainter than a small badge while keeping the hue.
527
- @each $name in ("success", "danger", "warning", "info") {
905
+ @each $name in ("success", "danger", "warning", "info", "pending") {
528
906
  $variant: map.get($dx-variants, $name);
529
907
  &.toast-#{$name} {
530
908
  --bs-toast-bg: #{color.mix(map.get($variant, "soft-bg"), $white, 50%)};
@@ -542,9 +920,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
542
920
  // active). secondary/success/danger/warning/info are SOFT (light tint bg +
543
921
  // dark text, fully overridden incl. hover/active).
544
922
  // - Outline buttons: the emphasis shade for border/label, the soft tint on hover.
923
+ // - Disabled buttons of every variant: the neutral grey set after this loop.
545
924
  // - Badges: `.badge.text-bg-*` re-tinted soft — needs `!important` because
546
925
  // Bootstrap's `.text-bg-*` helper sets its colours `!important` by design.
547
- // - Alerts: the soft tint.
926
+ // - Alerts: the SUBTLE tint (70% white) with the subtle border.
548
927
  // - Links/text: `.link-*` and `.text-*` take the emphasis shade so they read
549
928
  // on white (the base colours already carry emphasis for most variants; this
550
929
  // pins the two — success/warning — whose base differs).
@@ -563,7 +942,6 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
563
942
  --bs-btn-color: #{$solid-text};
564
943
  --bs-btn-hover-color: #{$solid-text};
565
944
  --bs-btn-active-color: #{$solid-text};
566
- --bs-btn-disabled-color: #{$solid-text};
567
945
  }
568
946
  } @else {
569
947
  // Soft button: light tint bg + dark same-hue label; darken the tint slightly
@@ -578,9 +956,6 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
578
956
  --bs-btn-active-color: #{$soft-text};
579
957
  --bs-btn-active-bg: #{color.scale($soft-bg, $lightness: -10%)};
580
958
  --bs-btn-active-border-color: #{color.scale($soft-bg, $lightness: -10%)};
581
- --bs-btn-disabled-color: #{$soft-text};
582
- --bs-btn-disabled-bg: #{$soft-bg};
583
- --bs-btn-disabled-border-color: #{$soft-bg};
584
959
  }
585
960
  }
586
961
 
@@ -621,20 +996,25 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
621
996
  color: #{$soft-text} !important;
622
997
  }
623
998
 
624
- // Alert — soft tint, with the emphasis shade as the accent border.
999
+ // Alert — the SUBTLE tint (the 70%-white mix), the subtle border and the
1000
+ // soft text. Not the soft tint: warning's soft-bg is its full butter-yellow
1001
+ // solid, so a soft warning alert was as loud as a solid fill, and an alert is
1002
+ // a large surface. The border (the soft tint 10% darker, the same shade as
1003
+ // `$X-border-subtle`) is what keeps the paler box visible against a white
1004
+ // card. Badges, soft buttons and toasts keep their own tints.
625
1005
  .alert-#{$name} {
626
- --bs-alert-bg: #{$soft-bg};
1006
+ --bs-alert-bg: #{dx-subtle-bg($name)};
627
1007
  --bs-alert-color: #{$soft-text};
628
- --bs-alert-border-color: #{color.scale($soft-bg, $lightness: -8%)};
1008
+ --bs-alert-border-color: #{dx-subtle-border($name)};
629
1009
  --bs-alert-link-color: #{$emphasis};
630
1010
  }
631
1011
 
632
1012
  // Progress bars — large FILLS use the vivid `solid-bg`, not the dark
633
- // emphasis Bootstrap wires into `.bg-*` (#154). A big solid block of the
634
- // emphasis olive reads muddy and clashes with the soft badge green; the
635
- // vivid fill is the same family as the switch-ON colour, so badge + switch +
636
- // bar read as one system. Only success and warning actually change hex
637
- // (danger/info's solid IS their emphasis). The variant-less default bar
1013
+ // emphasis Bootstrap wires into `.bg-*` (#154). A big solid block of a
1014
+ // deep emphasis shade reads muddy and clashes with the soft badge tint; the
1015
+ // vivid fill is the same family as the switch colours, so badge + switch +
1016
+ // bar read as one system. Every status variant's solid differs from its
1017
+ // emphasis, so all four change hex here. The variant-less default bar
638
1018
  // stays `$progress-bar-bg` (brand navy). `!important` beats the `.bg-*`
639
1019
  // utility, which sets its colour `!important` by design.
640
1020
  //
@@ -655,9 +1035,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
655
1035
 
656
1036
  // Publish each variant's soft/emphasis shades as CSS variables so components
657
1037
  // (e.g. DXSwitch's on-box) and consumers can reference the exact soft-first
658
- // tints on any element — Bootstrap's own `--bs-*-bg-subtle` are a different,
659
- // greyer family derived from the dark emphasis base, so they don't match the
660
- // soft badge/alert green. These are the same values the $dx-variants loop paints.
1038
+ // tints on any element. Bootstrap's own `--bs-*-bg-subtle` are now derived
1039
+ // from these same tints (a 70%-white mix, set before the import), so they are
1040
+ // a paler step of this family rather than a different one; use these when you
1041
+ // need the badge/alert tint itself. These are the values the $dx-variants loop paints.
661
1042
  :root {
662
1043
  @each $name, $v in $dx-variants {
663
1044
  --dx-#{$name}-soft-bg: #{map.get($v, "soft-bg")};
@@ -745,11 +1126,69 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
745
1126
  }
746
1127
  }
747
1128
 
1129
+ // Disabled buttons: one neutral treatment whatever the variant. Bootstrap's is
1130
+ // opacity alone (0.65), so a disabled butter-yellow warning or lime success
1131
+ // button kept its colour and still looked pressable. Light grey fill, grey
1132
+ // label, full opacity; outline and link buttons keep no fill (grey border /
1133
+ // grey text). The label is #6c757d on #e9ecef, 3.95:1: disabled controls are
1134
+ // exempt from WCAG 1.4.3, but it stays readable. Bootstrap already sets
1135
+ // `pointer-events: none` on a disabled `.btn`, so there is no hover state and a
1136
+ // `cursor` would never show. Published as `--dx-btn-disabled-*` so a consumer
1137
+ // can retune all three together.
1138
+ $dx-btn-disabled-bg: #e9ecef; // $gray-200
1139
+ $dx-btn-disabled-color: #6c757d; // $gray-600
1140
+ $dx-btn-disabled-outline-border: #ced4da; // $gray-400
1141
+
1142
+ :root {
1143
+ --dx-btn-disabled-bg: #{$dx-btn-disabled-bg};
1144
+ --dx-btn-disabled-color: #{$dx-btn-disabled-color};
1145
+ --dx-btn-disabled-outline-border: #{$dx-btn-disabled-outline-border};
1146
+ }
1147
+
1148
+ // Drives Bootstrap's own `--bs-btn-disabled-*` variables, which its
1149
+ // `.btn:disabled` rule paints. Two classes (or `fieldset:disabled .btn`) so it
1150
+ // outranks the per-variant `.btn-*` rules that set the same variables.
1151
+ .btn:is(:disabled, .disabled),
1152
+ fieldset:disabled .btn {
1153
+ --bs-btn-disabled-opacity: 1;
1154
+ --bs-btn-disabled-bg: var(--dx-btn-disabled-bg);
1155
+ --bs-btn-disabled-color: var(--dx-btn-disabled-color);
1156
+ --bs-btn-disabled-border-color: var(--dx-btn-disabled-bg);
1157
+
1158
+ &:is([class*="btn-outline-"]) {
1159
+ --bs-btn-disabled-bg: transparent;
1160
+ --bs-btn-disabled-border-color: var(--dx-btn-disabled-outline-border);
1161
+ }
1162
+
1163
+ &.btn-link {
1164
+ --bs-btn-disabled-bg: transparent;
1165
+ --bs-btn-disabled-border-color: transparent;
1166
+ }
1167
+ }
1168
+
1169
+ // The exceptions: two states that are REALLY disabled but are not unavailable
1170
+ // actions, so they keep their variant's own colours (by default the navy
1171
+ // primary fill, #151e2d / #e9f0f8) at full opacity rather than the neutral grey
1172
+ // above:
1173
+ // - a busy button (DButton `loading`, marked `aria-busy="true"`), disabled only
1174
+ // so it can't be pressed twice. Greying it made every save blink navy → grey
1175
+ // → navy, since the grey lasted exactly as long as the request;
1176
+ // - DXSaveButton's saved state ("✓ Saved"), a confirmation.
1177
+ // So saving changes the label and the disabled state but never the colour.
1178
+ // `.btn` + the `:is()` (its most specific argument counts) + the state outranks
1179
+ // the neutral rule (and `fieldset:disabled .btn`).
1180
+ .btn:is(.dx-save-button--saved, [aria-busy="true"]):is(:disabled, .disabled) {
1181
+ --bs-btn-disabled-opacity: 1;
1182
+ --bs-btn-disabled-bg: var(--bs-btn-bg);
1183
+ --bs-btn-disabled-color: var(--bs-btn-color);
1184
+ --bs-btn-disabled-border-color: var(--bs-btn-border-color);
1185
+ }
1186
+
748
1187
  // ----------------------------------------------------------------------------
749
1188
  // Dropdowns — chevron caret (B14 / PO5)
750
1189
  // ----------------------------------------------------------------------------
751
1190
  // Bootstrap's `.dropdown-toggle::after` is a CSS border-triangle that sits high
752
- // against Poppins' line box and reads crude next to the rest of the theme.
1191
+ // against the body font's line box and reads crude next to the rest of the theme.
753
1192
  // Replace it with the bootstrap-icons chevron glyph, optically centred with a
754
1193
  // little breathing room from the label. The font is already available: the
755
1194
  // theme `@import`s `bootstrap-icons/font/bootstrap-icons` (declaring the
@@ -775,10 +1214,10 @@ $dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
775
1214
  }
776
1215
 
777
1216
  // ----------------------------------------------------------------------------
778
- // Switches — green when ON, red when OFF, by default (#158)
1217
+ // Switches — green when ON, magenta (danger) when OFF, by default (#158)
779
1218
  // ----------------------------------------------------------------------------
780
1219
  // House style (James): an active switch — "Product is current", "Stock is
781
- // tracked", "Visible" — should read success GREEN when on and RED when off,
1220
+ // tracked", "Visible" — should read success GREEN when on and danger when off,
782
1221
  // OUT OF THE BOX, not only when a consumer opts in. Bootstrap's default is a
783
1222
  // primary (navy) track when on / grey when off. We flip the default here.
784
1223
  //
@@ -789,19 +1228,22 @@ $dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
789
1228
  // Applies to any bare `.form-switch` (DFormCheckbox `switch`); DXSwitch is the
790
1229
  // "filled box" form where the whole control (not the toggle) carries the colour
791
1230
  // and the pill stays a neutral affordance — it reads the same tokens below.
792
- $dx-switch-on: map.get(map.get($dx-variants, "success"), "solid-bg"); // vivid lime
793
- $dx-switch-off: map.get(map.get($dx-variants, "danger"), "solid-bg"); // vivid red
1231
+ $dx-switch-on: map.get(map.get($dx-variants, "success"), "solid-bg"); // vivid lime #7bf25a
1232
+ $dx-switch-off: map.get(map.get($dx-variants, "danger"), "solid-bg"); // vivid magenta #e46ab9
794
1233
  $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");
795
- // A red thumb reads better than white on the light-red OFF track.
796
- $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");
1234
+ // A coloured thumb reads better than white on the light OFF track. Its fill is
1235
+ // the danger EMPHASIS (#a3247f) — an SVG data URI can't take a Sass variable
1236
+ // without string surgery, so keep this literal in step with $dx-variants.
1237
+ $dx-switch-thumb-off: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23a3247f'/%3e%3c/svg%3e");
797
1238
 
798
1239
  // Switch fill tokens. The ON fill is the SAME soft green as the success
799
1240
  // badge/button (its `soft-bg`), so a switch and a success badge read as one
800
- // green — not a separate, punchier lime. OFF is a LIGHT red (a light tint of the
801
- // danger solid, NOT the plum `danger` soft — a switch off wants to read "red /
802
- // stop", which the plum status tint doesn't). Derived from the base tokens so
803
- // one edit moves them. Used by the DXSwitch filled box; the OFF track reuses them.
804
- $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cdf9b2
1241
+ // green — not a separate, punchier lime. OFF is a LIGHT magenta: a 20% tint of
1242
+ // the danger solid (mixed here rather than the danger `soft-bg`, so the
1243
+ // track's well/line/ink steps all come from one hue). Derived from the base
1244
+ // tokens so one edit moves them. Used by the DXSwitch filled box; the OFF track
1245
+ // reuses them.
1246
+ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #c3faaa
805
1247
  :root {
806
1248
  --dx-switch-on-bg: #{$dx-switch-on-soft};
807
1249
  --dx-switch-on-well: #{color.mix($dx-switch-on-soft, #fff, 55%)};
@@ -824,10 +1266,10 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
824
1266
  }
825
1267
 
826
1268
  .form-switch .form-check-input {
827
- // OFF: light-red track, red thumb (left).
1269
+ // OFF: light-magenta track, deep-magenta thumb (left).
828
1270
  background-color: var(--dx-switch-off-bg);
829
1271
  border-color: var(--dx-switch-off-line);
830
- --bs-form-switch-bg: #{$dx-switch-thumb-red};
1272
+ --bs-form-switch-bg: #{$dx-switch-thumb-off};
831
1273
 
832
1274
  &:checked {
833
1275
  // ON: vivid green track, white thumb (right — Bootstrap positions it).
@@ -839,7 +1281,7 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
839
1281
  // Neutral grey focus ring (the track carries the state colour, so the brand
840
1282
  // navy ring would clash); keep the state thumb rather than Bootstrap's navy.
841
1283
  &:focus {
842
- --bs-form-switch-bg: #{$dx-switch-thumb-red};
1284
+ --bs-form-switch-bg: #{$dx-switch-thumb-off};
843
1285
  border-color: var(--dx-switch-off-line);
844
1286
  box-shadow: 0 0 0 0.25rem rgba($dark, 0.15);
845
1287
  }
@@ -873,59 +1315,86 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
873
1315
  // ----------------------------------------------------------------------------
874
1316
  // Chart palette (data-viz)
875
1317
  // ----------------------------------------------------------------------------
876
- // Dedicated categorical palette for the DX*Chart components — deliberately NOT
877
- // the semantic UI colours (#141). Two reasons: the base $theme-colors carry
1318
+ // Dedicated categorical palette for the DX*Chart components — its own lists,
1319
+ // NOT the semantic `--bs-*` variables (#141): the base $theme-colors carry
878
1320
  // dark "emphasis" shades (AA text/link colours, too muted for data series), and
879
- // status colours (success/danger) shouldn't impersonate "series 2" — a green or
880
- // red series should mean something.
1321
+ // a series colour must not move when a status colour is retuned. The fills do
1322
+ // share the fruity family's hues (slots 1/2/3/6 are the info/success/danger/
1323
+ // warning solids) so charts and badges read as one system; the order, not the
1324
+ // status meaning, is what the slots encode.
1325
+ //
1326
+ // Eight light, vivid FILLS in a FIXED order, plus a darker LINE shade per slot
1327
+ // and one EDGE colour. The fills are light (1.35–2.97:1 on white), so they
1328
+ // cannot carry WCAG 1.4.11 non-text contrast on their own; that job goes to:
1329
+ // - the EDGE: bars and doughnut segments get a 1px near-black outline
1330
+ // (#121419, the warning label ink; 18.42:1 on white), which separates each
1331
+ // mark from the white page and from its neighbours whatever the fill;
1332
+ // - the LINE shades: each fill darkened toward black (same hue) until it
1333
+ // clears 3.5:1 on white. Line strokes and point outlines use them; the
1334
+ // fill only shows as point centres and a 35% area wash.
881
1335
  //
882
- // Eight vivid mid-lightness hues in a FIXED order. The order is load-bearing:
883
- // it was derived by exhaustively permuting the hues (slot 1 pinned to the brand
884
- // info-blue) to maximise the minimum adjacent-pair colour-vision-deficiency
885
- // separation — validated at min adjacent ΔE 13.8 (OKLab ×100, protan/deutan
886
- // simulated), min normal-vision ΔE 20.3, every hue ≥ 3:1 contrast on white.
887
- // Reordering or swapping a hue needs that validation re-run, not an eyeball.
1336
+ // The order is load-bearing: slot 1 is pinned and slots 2..8 were chosen by
1337
+ // exhaustive permutation to maximise the minimum adjacent-pair separation
1338
+ // under colour-vision deficiency (OKLab ΔE ×100, Viénot protan/deutan). The
1339
+ // shipped order is the best available for both lists: fills min adjacent CVD
1340
+ // ΔE 19.56, lines 13.73 (weakest neighbours: the magenta and olive line
1341
+ // shades, slots 3/4). Min adjacent normal-vision ΔE: fills 21.41, lines 16.63.
1342
+ // Reordering, swapping a hue, or editing a line shade needs the validation
1343
+ // re-run, not an eyeball: `node scripts/validate-chart-palette.mjs` reads these
1344
+ // lists from this file, prints the scores, and checks every line shade is its
1345
+ // fill darkened to 3.5:1.
888
1346
  //
889
- // Published as --dx-chart-1..8 so consumers can retheme charts by overriding
890
- // the variables; chartTheme.ts reads them at runtime (with these hexes as
891
- // fallbacks for SSR / missing-CSS cases — keep the two lists in sync).
892
- $dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d97706, #db2777);
893
-
894
- // Dark-surface remap (#145). SAME hue slots — the CVD-derived order above is
895
- // load-bearing, so only lightness moves: each hue lifted into the dark band so
896
- // every step clears the 3:1 non-text guideline on Bootstrap's dark body
897
- // (#212529), where two of the light steps dipped below. Validated with the
898
- // same method as the light set (OKLab ΔE ×100, Viénot protan/deutan): min
899
- // adjacent CVD ΔE 15.1 (light set: 13.8), min normal ΔE 25.3, contrast
900
- // 5.67–10.23:1 on #212529. Swapping a step needs that validation re-run.
1347
+ // Published as --dx-chart-1..8 (fills), --dx-chart-line-1..8 and
1348
+ // --dx-chart-edge so consumers can retheme charts by overriding the
1349
+ // variables; chartTheme.ts reads them at runtime (with these hexes as
1350
+ // fallbacks for SSR / missing-CSS cases — keep the lists in sync; the sync is
1351
+ // enforced by tests/components/charts.test.ts, which parses this file).
1352
+ $dx-chart-palette: (#7fd7fd, #7bf25a, #e46ab9, #d6e86a, #b9a3f0, #efd574, #9fb4ff, #f7a072);
1353
+ $dx-chart-line-palette: (#5590aa, #4d9839, #d161a9, #848f41, #907fbb, #99884a, #7686be, #ba7956);
1354
+ $dx-chart-edge: #121419;
1355
+
1356
+ // Dark-surface remap (#145). The dark set predates the light fills above and
1357
+ // is unchanged: mid-lightness hues that already clear 3:1 on Bootstrap's dark
1358
+ // body (#212529) — 5.67–10.23:1 — so in dark mode the fills ARE the line
1359
+ // shades, and the edge is the dark body colour itself (an invisible 1px
1360
+ // separator between segments, as before the light edge existed). Validated
1361
+ // with the same method: min adjacent CVD ΔE 15.09, min adjacent normal ΔE
1362
+ // 25.33. Swapping a step needs that validation re-run.
901
1363
  // getPalette() reads these CSS variables live — off the CHART'S OWN container,
902
1364
  // so nested colour-mode scopes resolve correctly, and re-read on every
903
1365
  // `data-bs-theme` change so mounted charts repaint (#161). The TS fallbacks
904
1366
  // stay the light set (SSR/no-CSS default).
905
1367
  $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #fbbf24, #f472b6);
1368
+ $dx-chart-edge-dark: $body-bg-dark;
906
1369
 
907
- :root {
1370
+ @mixin dx-chart-light-vars {
908
1371
  @for $i from 1 through list.length($dx-chart-palette) {
909
1372
  --dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
1373
+ --dx-chart-line-#{$i}: #{list.nth($dx-chart-line-palette, $i)};
910
1374
  }
1375
+ --dx-chart-edge: #{$dx-chart-edge};
1376
+ }
1377
+
1378
+ :root {
1379
+ @include dx-chart-light-vars;
911
1380
  }
912
1381
 
913
1382
  [data-bs-theme="dark"] {
914
1383
  @for $i from 1 through list.length($dx-chart-palette-dark) {
915
1384
  --dx-chart-#{$i}: #{list.nth($dx-chart-palette-dark, $i)};
1385
+ --dx-chart-line-#{$i}: #{list.nth($dx-chart-palette-dark, $i)};
916
1386
  }
1387
+ --dx-chart-edge: #{$dx-chart-edge-dark};
917
1388
  }
918
1389
 
919
1390
  // Bootstrap allows `data-bs-theme` on ANY container, so colour-mode scopes
920
1391
  // nest — and custom properties inherit. Without this re-declaration a LIGHT
921
1392
  // scope inside a dark root (a light card on a dark page) would inherit the dark
922
1393
  // palette, because only `:root` publishes the light values and the dark block
923
- // above overrides it for the whole subtree (#161). Same list as `:root`, so the
924
- // two can't drift.
1394
+ // above overrides it for the whole subtree (#161). Same mixin as `:root`, so
1395
+ // the two can't drift.
925
1396
  [data-bs-theme="light"] {
926
- @for $i from 1 through list.length($dx-chart-palette) {
927
- --dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
928
- }
1397
+ @include dx-chart-light-vars;
929
1398
  }
930
1399
 
931
1400
  // The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped