@omnitend/dashboard-for-laravel 0.41.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) 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/DModal.vue.d.ts +26 -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/DXLoading.vue.d.ts +9 -0
  17. package/dist/components/extended/DXModalActions.vue.d.ts +12 -0
  18. package/dist/components/extended/DXNumberStepper.vue.d.ts +33 -0
  19. package/dist/components/extended/DXSaveButton.vue.d.ts +38 -0
  20. package/dist/components/extended/DXSwitch.vue.d.ts +1 -1
  21. package/dist/components/extended/DXTable.vue.d.ts +8 -2
  22. package/dist/composables/useForm.d.ts +7 -0
  23. package/dist/dashboard-for-laravel.js +9547 -9246
  24. package/dist/dashboard-for-laravel.js.map +1 -1
  25. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  26. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  27. package/dist/index.d.ts +5 -0
  28. package/dist/style.css +2 -2
  29. package/dist/types/index.d.ts +20 -3
  30. package/docs/public/api-reference.json +219 -17
  31. package/docs/public/docs-map.md +4 -2
  32. package/docs/public/llms.txt +7 -2
  33. package/package.json +1 -1
  34. package/resources/css/theme.scss +524 -103
  35. package/resources/fonts/maven-pro/MavenPro-Bold.woff2 +0 -0
  36. package/resources/fonts/maven-pro/MavenPro-Medium.woff2 +0 -0
  37. package/resources/fonts/maven-pro/MavenPro-Regular.woff2 +0 -0
  38. package/resources/fonts/maven-pro/MavenPro-SemiBold.woff2 +0 -0
  39. package/resources/fonts/maven-pro/OFL.txt +93 -0
  40. package/resources/fonts/poppins/OFL.txt +93 -0
  41. package/resources/fonts/poppins/Poppins-Medium.woff2 +0 -0
  42. package/resources/fonts/poppins/Poppins-SemiBold.woff2 +0 -0
  43. package/resources/js/components/base/DModal.vue +13 -2
  44. package/resources/js/components/charts/DXBarChart.vue +2 -1
  45. package/resources/js/components/charts/DXDoughnutChart.vue +2 -1
  46. package/resources/js/components/charts/DXLineChart.vue +2 -1
  47. package/resources/js/components/charts/chartTheme.ts +68 -21
  48. package/resources/js/components/extended/DXDashboard.vue +4 -2
  49. package/resources/js/components/extended/DXDashboardSidebar.vue +52 -20
  50. package/resources/js/components/extended/DXField.vue +59 -28
  51. package/resources/js/components/extended/DXForm.vue +122 -21
  52. package/resources/js/components/extended/DXLoading.vue +47 -0
  53. package/resources/js/components/extended/DXModalActions.vue +13 -0
  54. package/resources/js/components/extended/DXNumberStepper.vue +243 -0
  55. package/resources/js/components/extended/DXSaveButton.vue +82 -0
  56. package/resources/js/components/extended/DXSwitch.vue +8 -5
  57. package/resources/js/components/extended/DXTable.vue +58 -22
  58. package/resources/js/components/extended/DXTablePagination.vue +1 -1
  59. package/resources/js/composables/useForm.ts +11 -0
  60. package/resources/js/composables/useToast.ts +1 -1
  61. package/resources/js/index.ts +5 -0
  62. 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,51 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
317
562
  }
318
563
  }
319
564
 
565
+ // A display-only value in a form row: a badge, a line of text, a short stack
566
+ // of lines. It has no input box, so on its own it sits at the top of the row
567
+ // while the label column (`.col-form-label`) is padded down to an input's text
568
+ // line. This gives it the same offset and line-height Bootstrap gives
569
+ // `.col-form-label` and `.form-control-plaintext`, so its FIRST line shares
570
+ // the label's centre. DXField wraps a `plaintext` field's `value` slot in it;
571
+ // consumers put it on their own display-only content. A `.form-control-
572
+ // plaintext` inside already carries the offset, so it drops its own.
573
+ .dx-form-plaintext {
574
+ padding-top: calc(#{$input-padding-y} + #{$input-border-width});
575
+ padding-bottom: calc(#{$input-padding-y} + #{$input-border-width});
576
+ line-height: $input-line-height;
577
+
578
+ > .form-control-plaintext {
579
+ padding-top: 0;
580
+ padding-bottom: 0;
581
+ border-top-width: 0;
582
+ border-bottom-width: 0;
583
+ }
584
+
585
+ // A badge is an inline-block in a smaller font, so neither `baseline` nor
586
+ // `middle` centres it on the line (both land ~2px off, the way depending on
587
+ // the font's x-height). Pin it to the line's top and push it down by half
588
+ // the spare height: the line is `$input-line-height` parent-ems, the badge
589
+ // is 1em (Bootstrap's `line-height: 1`) plus its padding, in badge-ems.
590
+ .badge {
591
+ vertical-align: top;
592
+ margin-top: calc(
593
+ (#{math.div($input-line-height, math.div($badge-font-size, 1em)) * 1em} - 1em - #{$badge-padding-y * 2}) / 2
594
+ );
595
+ }
596
+ }
597
+
598
+ // Switch-list option rows (DXField `switch-list`) are one line each: a label,
599
+ // a switch and an optional trailing control (a notes input in the
600
+ // `switch-list-item` slot). Centre all three in the row, instead of the
601
+ // top-aligned horizontal rhythm above, so the padded label column, the 24px
602
+ // switch and a taller input share one centre whether or not the input is
603
+ // showing. Comes after `.dx-form--horizontal .row` at the same specificity so
604
+ // it wins. The `.row` is bootstrap-vue-next's, inside the BFormGroup wrapper,
605
+ // so a utility class on the DFormGroup lands on the wrapper and does nothing.
606
+ .dx-switch-list-row .row {
607
+ align-items: center;
608
+ }
609
+
320
610
  // Bootstrap's `.input-group` defaults to `flex-wrap: wrap`, letting a
321
611
  // narrow container (e.g. a compact table cell, a small screen) stack the
322
612
  // prepend/append affix onto its own line below the input. That breaks the
@@ -328,13 +618,44 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
328
618
  flex-wrap: nowrap;
329
619
  }
330
620
 
621
+ // DXNumberStepper: `[ − ][ 12 ][ + ]`. The buttons are DButton icon-only
622
+ // squares, sized by the input group. The number is centred between them, and
623
+ // the browser's spin arrows are hidden (they duplicate the buttons and push the
624
+ // number off centre). Global rather than scoped: the input is a bvn root, where
625
+ // a scoped rule is not reliably reached (see CLAUDE.md, scope-id trap).
626
+ // The input is a fixed few characters wide (the group is inline, so it does
627
+ // not stretch across a table cell); retune with `--dx-number-stepper-input-width`.
628
+ .dx-number-stepper {
629
+ display: inline-flex;
630
+ width: auto;
631
+ vertical-align: middle;
632
+
633
+ > .dx-number-stepper__input {
634
+ flex: 0 1 auto;
635
+ width: var(--dx-number-stepper-input-width, 6ch);
636
+ min-width: 4ch;
637
+ padding-left: 0.25rem;
638
+ padding-right: 0.25rem;
639
+ text-align: center;
640
+ -moz-appearance: textfield;
641
+ appearance: textfield;
642
+
643
+ &::-webkit-inner-spin-button,
644
+ &::-webkit-outer-spin-button {
645
+ -webkit-appearance: none;
646
+ margin: 0;
647
+ }
648
+ }
649
+ }
650
+
331
651
  // ----------------------------------------------------------------------------
332
652
  // Nav tabs
333
653
  // ----------------------------------------------------------------------------
334
654
  // Chrome recedes, content leads — the same ethos as the muted table headers
335
655
  // (#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
656
+ // var(--bs-link-color)`, i.e. the link colour (brand navy, near-black), so
657
+ // every tab reads as selected and a row of tabs competes with the fields
658
+ // below. Mute the
338
659
  // inactive tabs and let the ACTIVE one (Bootstrap's
339
660
  // `--bs-nav-tabs-link-active-color`, the near-black emphasis colour) carry the
340
661
  // selection on its own.
@@ -358,6 +679,22 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
358
679
  // Dashboard Custom Styles
359
680
  // ----------------------------------------------------------------------------
360
681
 
682
+ :root {
683
+ --dx-dashboard-gutter-x: #{$dashboard-gutter-x};
684
+ }
685
+
686
+ // Both are the `DContainer fluid` directly inside the navbar header and the
687
+ // <main> panel (which keeps only its vertical padding, `py-4`). In the main
688
+ // panel's centred branch the DRow's negative margins and the DCol's padding
689
+ // cancel, so the content edge is this padding in both branches. Two classes
690
+ // so it outranks Bootstrap's own `.container-fluid` padding regardless of
691
+ // stylesheet order.
692
+ .dashboard-navbar > .container-fluid,
693
+ .dashboard-main > .container-fluid {
694
+ padding-left: var(--dx-dashboard-gutter-x);
695
+ padding-right: var(--dx-dashboard-gutter-x);
696
+ }
697
+
361
698
  .dashboard-sidebar {
362
699
  background-color: $dark;
363
700
  width: $dashboard-sidebar-width;
@@ -519,12 +856,12 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
519
856
  // the base `.toast` rule (above, and Bootstrap's own) already reads.
520
857
  //
521
858
  // 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
859
+ // vivid-derived soft family badges use — so the toast hue tracks the
523
860
  // palette and editing a `soft-bg` moves toasts too. Previously these mixed the
524
861
  // dark *emphasis* base colours (greyer success/warning) and danger's saturated
525
862
  // red (a pink, not the soft system's plum). Mixed 50% with white so a large
526
863
  // toast surface stays fainter than a small badge while keeping the hue.
527
- @each $name in ("success", "danger", "warning", "info") {
864
+ @each $name in ("success", "danger", "warning", "info", "pending") {
528
865
  $variant: map.get($dx-variants, $name);
529
866
  &.toast-#{$name} {
530
867
  --bs-toast-bg: #{color.mix(map.get($variant, "soft-bg"), $white, 50%)};
@@ -542,9 +879,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
542
879
  // active). secondary/success/danger/warning/info are SOFT (light tint bg +
543
880
  // dark text, fully overridden incl. hover/active).
544
881
  // - Outline buttons: the emphasis shade for border/label, the soft tint on hover.
882
+ // - Disabled buttons of every variant: the neutral grey set after this loop.
545
883
  // - Badges: `.badge.text-bg-*` re-tinted soft — needs `!important` because
546
884
  // Bootstrap's `.text-bg-*` helper sets its colours `!important` by design.
547
- // - Alerts: the soft tint.
885
+ // - Alerts: the SUBTLE tint (70% white) with the subtle border.
548
886
  // - Links/text: `.link-*` and `.text-*` take the emphasis shade so they read
549
887
  // on white (the base colours already carry emphasis for most variants; this
550
888
  // pins the two — success/warning — whose base differs).
@@ -563,7 +901,6 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
563
901
  --bs-btn-color: #{$solid-text};
564
902
  --bs-btn-hover-color: #{$solid-text};
565
903
  --bs-btn-active-color: #{$solid-text};
566
- --bs-btn-disabled-color: #{$solid-text};
567
904
  }
568
905
  } @else {
569
906
  // Soft button: light tint bg + dark same-hue label; darken the tint slightly
@@ -578,9 +915,6 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
578
915
  --bs-btn-active-color: #{$soft-text};
579
916
  --bs-btn-active-bg: #{color.scale($soft-bg, $lightness: -10%)};
580
917
  --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
918
  }
585
919
  }
586
920
 
@@ -621,20 +955,25 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
621
955
  color: #{$soft-text} !important;
622
956
  }
623
957
 
624
- // Alert — soft tint, with the emphasis shade as the accent border.
958
+ // Alert — the SUBTLE tint (the 70%-white mix), the subtle border and the
959
+ // soft text. Not the soft tint: warning's soft-bg is its full butter-yellow
960
+ // solid, so a soft warning alert was as loud as a solid fill, and an alert is
961
+ // a large surface. The border (the soft tint 10% darker, the same shade as
962
+ // `$X-border-subtle`) is what keeps the paler box visible against a white
963
+ // card. Badges, soft buttons and toasts keep their own tints.
625
964
  .alert-#{$name} {
626
- --bs-alert-bg: #{$soft-bg};
965
+ --bs-alert-bg: #{dx-subtle-bg($name)};
627
966
  --bs-alert-color: #{$soft-text};
628
- --bs-alert-border-color: #{color.scale($soft-bg, $lightness: -8%)};
967
+ --bs-alert-border-color: #{dx-subtle-border($name)};
629
968
  --bs-alert-link-color: #{$emphasis};
630
969
  }
631
970
 
632
971
  // 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
972
+ // emphasis Bootstrap wires into `.bg-*` (#154). A big solid block of a
973
+ // deep emphasis shade reads muddy and clashes with the soft badge tint; the
974
+ // vivid fill is the same family as the switch colours, so badge + switch +
975
+ // bar read as one system. Every status variant's solid differs from its
976
+ // emphasis, so all four change hex here. The variant-less default bar
638
977
  // stays `$progress-bar-bg` (brand navy). `!important` beats the `.bg-*`
639
978
  // utility, which sets its colour `!important` by design.
640
979
  //
@@ -655,9 +994,10 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
655
994
 
656
995
  // Publish each variant's soft/emphasis shades as CSS variables so components
657
996
  // (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.
997
+ // tints on any element. Bootstrap's own `--bs-*-bg-subtle` are now derived
998
+ // from these same tints (a 70%-white mix, set before the import), so they are
999
+ // a paler step of this family rather than a different one; use these when you
1000
+ // need the badge/alert tint itself. These are the values the $dx-variants loop paints.
661
1001
  :root {
662
1002
  @each $name, $v in $dx-variants {
663
1003
  --dx-#{$name}-soft-bg: #{map.get($v, "soft-bg")};
@@ -694,6 +1034,11 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
694
1034
  vertical-align: middle;
695
1035
  }
696
1036
 
1037
+ // Column help uses the table's header colour rather than the form-label ink.
1038
+ .table > thead th .dx-field-label__text {
1039
+ color: inherit;
1040
+ }
1041
+
697
1042
  .table > thead {
698
1043
  vertical-align: middle;
699
1044
  }
@@ -740,17 +1085,70 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
740
1085
  }
741
1086
  }
742
1087
 
1088
+ // Disabled buttons: one neutral treatment whatever the variant. Bootstrap's is
1089
+ // opacity alone (0.65), so a disabled butter-yellow warning or lime success
1090
+ // button kept its colour and still looked pressable. Light grey fill, grey
1091
+ // label, full opacity; outline and link buttons keep no fill (grey border /
1092
+ // grey text). The label is #6c757d on #e9ecef, 3.95:1: disabled controls are
1093
+ // exempt from WCAG 1.4.3, but it stays readable. Bootstrap already sets
1094
+ // `pointer-events: none` on a disabled `.btn`, so there is no hover state and a
1095
+ // `cursor` would never show. Published as `--dx-btn-disabled-*` so a consumer
1096
+ // can retune all three together.
1097
+ $dx-btn-disabled-bg: #e9ecef; // $gray-200
1098
+ $dx-btn-disabled-color: #6c757d; // $gray-600
1099
+ $dx-btn-disabled-outline-border: #ced4da; // $gray-400
1100
+
1101
+ :root {
1102
+ --dx-btn-disabled-bg: #{$dx-btn-disabled-bg};
1103
+ --dx-btn-disabled-color: #{$dx-btn-disabled-color};
1104
+ --dx-btn-disabled-outline-border: #{$dx-btn-disabled-outline-border};
1105
+ }
1106
+
1107
+ // Drives Bootstrap's own `--bs-btn-disabled-*` variables, which its
1108
+ // `.btn:disabled` rule paints. Two classes (or `fieldset:disabled .btn`) so it
1109
+ // outranks the per-variant `.btn-*` rules that set the same variables.
1110
+ .btn:is(:disabled, .disabled),
1111
+ fieldset:disabled .btn {
1112
+ --bs-btn-disabled-opacity: 1;
1113
+ --bs-btn-disabled-bg: var(--dx-btn-disabled-bg);
1114
+ --bs-btn-disabled-color: var(--dx-btn-disabled-color);
1115
+ --bs-btn-disabled-border-color: var(--dx-btn-disabled-bg);
1116
+
1117
+ &:is([class*="btn-outline-"]) {
1118
+ --bs-btn-disabled-bg: transparent;
1119
+ --bs-btn-disabled-border-color: var(--dx-btn-disabled-outline-border);
1120
+ }
1121
+
1122
+ &.btn-link {
1123
+ --bs-btn-disabled-bg: transparent;
1124
+ --bs-btn-disabled-border-color: transparent;
1125
+ }
1126
+ }
1127
+
1128
+ // The one exception: DXSaveButton's saved state ("✓ Saved") is a REAL
1129
+ // disabled button, but it is a confirmation, not an unavailable action, so it
1130
+ // keeps its variant's own colours (by default the navy primary fill, #151e2d /
1131
+ // #e9f0f8) at full opacity rather than the neutral grey above, so saving
1132
+ // changes the label and the disabled state but not the colour. Three classes
1133
+ // plus the state, so it outranks the neutral rule (and `fieldset:disabled .btn`).
1134
+ .btn.dx-save-button--saved:is(:disabled, .disabled) {
1135
+ --bs-btn-disabled-opacity: 1;
1136
+ --bs-btn-disabled-bg: var(--bs-btn-bg);
1137
+ --bs-btn-disabled-color: var(--bs-btn-color);
1138
+ --bs-btn-disabled-border-color: var(--bs-btn-border-color);
1139
+ }
1140
+
743
1141
  // ----------------------------------------------------------------------------
744
1142
  // Dropdowns — chevron caret (B14 / PO5)
745
1143
  // ----------------------------------------------------------------------------
746
1144
  // Bootstrap's `.dropdown-toggle::after` is a CSS border-triangle that sits high
747
- // against Poppins' line box and reads crude next to the rest of the theme.
1145
+ // against the body font's line box and reads crude next to the rest of the theme.
748
1146
  // Replace it with the bootstrap-icons chevron glyph, optically centred with a
749
1147
  // little breathing room from the label. The font is already available: the
750
1148
  // theme `@import`s `bootstrap-icons/font/bootstrap-icons` (declaring the
751
1149
  // `@font-face`), and #77's post-build extraction ships that woff2 with the
752
1150
  // stylesheet — so this needs no new dependency and no data-URI. Values signed
753
- // off after one padding iteration in the greendragon interim; the glyph metrics
1151
+ // off after one padding iteration in the interim UI; the glyph metrics
754
1152
  // are tunable via the variables below.
755
1153
  $dx-dropdown-caret-size: 0.75em; // glyph size relative to the label
756
1154
  $dx-dropdown-caret-gap: 0.4em; // space between label and chevron
@@ -770,10 +1168,10 @@ $dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
770
1168
  }
771
1169
 
772
1170
  // ----------------------------------------------------------------------------
773
- // Switches — green when ON, red when OFF, by default (#158)
1171
+ // Switches — green when ON, magenta (danger) when OFF, by default (#158)
774
1172
  // ----------------------------------------------------------------------------
775
1173
  // House style (James): an active switch — "Product is current", "Stock is
776
- // tracked", "Visible" — should read success GREEN when on and RED when off,
1174
+ // tracked", "Visible" — should read success GREEN when on and danger when off,
777
1175
  // OUT OF THE BOX, not only when a consumer opts in. Bootstrap's default is a
778
1176
  // primary (navy) track when on / grey when off. We flip the default here.
779
1177
  //
@@ -784,19 +1182,22 @@ $dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
784
1182
  // Applies to any bare `.form-switch` (DFormCheckbox `switch`); DXSwitch is the
785
1183
  // "filled box" form where the whole control (not the toggle) carries the colour
786
1184
  // and the pill stays a neutral affordance — it reads the same tokens below.
787
- $dx-switch-on: map.get(map.get($dx-variants, "success"), "solid-bg"); // vivid lime
788
- $dx-switch-off: map.get(map.get($dx-variants, "danger"), "solid-bg"); // vivid red
1185
+ $dx-switch-on: map.get(map.get($dx-variants, "success"), "solid-bg"); // vivid lime #7bf25a
1186
+ $dx-switch-off: map.get(map.get($dx-variants, "danger"), "solid-bg"); // vivid magenta #e46ab9
789
1187
  $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");
790
- // A red thumb reads better than white on the light-red OFF track.
791
- $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");
1188
+ // A coloured thumb reads better than white on the light OFF track. Its fill is
1189
+ // the danger EMPHASIS (#a3247f) — an SVG data URI can't take a Sass variable
1190
+ // without string surgery, so keep this literal in step with $dx-variants.
1191
+ $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");
792
1192
 
793
1193
  // Switch fill tokens. The ON fill is the SAME soft green as the success
794
1194
  // badge/button (its `soft-bg`), so a switch and a success badge read as one
795
- // green — not a separate, punchier lime. OFF is a LIGHT red (a light tint of the
796
- // danger solid, NOT the plum `danger` soft — a switch off wants to read "red /
797
- // stop", which the plum status tint doesn't). Derived from the base tokens so
798
- // one edit moves them. Used by the DXSwitch filled box; the OFF track reuses them.
799
- $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cdf9b2
1195
+ // green — not a separate, punchier lime. OFF is a LIGHT magenta: a 20% tint of
1196
+ // the danger solid (mixed here rather than the danger `soft-bg`, so the
1197
+ // track's well/line/ink steps all come from one hue). Derived from the base
1198
+ // tokens so one edit moves them. Used by the DXSwitch filled box; the OFF track
1199
+ // reuses them.
1200
+ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #c3faaa
800
1201
  :root {
801
1202
  --dx-switch-on-bg: #{$dx-switch-on-soft};
802
1203
  --dx-switch-on-well: #{color.mix($dx-switch-on-soft, #fff, 55%)};
@@ -806,30 +1207,23 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
806
1207
  --dx-switch-off-well: #{color.mix($dx-switch-off, #fff, 10%)};
807
1208
  --dx-switch-off-line: #{color.mix($dx-switch-off, #fff, 45%)};
808
1209
  --dx-switch-off-ink: #{color.mix($dx-switch-off, #000, 30%)};
809
- --dx-switch-pill: var(--bs-gray-400, #ced4da); // neutral OFF pill
1210
+ --dx-switch-pill: var(--bs-gray-400, #ced4da);
1211
+ --dx-switch-off-pill: #{color.mix(map.get(map.get($dx-variants, "danger"), "soft-text"), $gray-400, 50%)};
810
1212
  --dx-switch-on-pill: #{color.mix(map.get(map.get($dx-variants, "success"), "soft-text"), $gray-400, 50%)};
811
1213
  }
812
1214
 
813
- // Optically centre the check/radio/switch box against its label's FIRST line.
814
- // Bootstrap's `.25em` top margin is mathematically right for a 1em box in a
815
- // 1.5em line box, but Poppins seats its glyphs low in the line box, so the box
816
- // reads visibly HIGH beside the text (same optical-metrics issue as the B14
817
- // dropdown caret). `0.325em` lands the box centre on Poppins' visual centre —
818
- // judged against a rendered filmstrip (0.25/0.30/0.325/0.35em) with Poppins
819
- // loaded, not from the maths. A top MARGIN (not flex-centring) is deliberate:
820
- // it aligns to the first line, so a multi-line label keeps the box on line one
821
- // rather than floating to the block's vertical centre. DXSwitch is unaffected —
822
- // it flex-centres and zeroes this margin on its inner input at higher
823
- // specificity (`.dx-switch :deep(.form-check-input) { margin: 0 }`).
1215
+ // Centre checkboxes/radios on the first label line using their actual size.
1216
+ // The old em offset assumed a 1em box; these controls are now 1.25rem tall.
1217
+ // A margin keeps wrapped labels aligned to line one rather than the whole block.
824
1218
  .form-check-input {
825
- margin-top: 0.325em;
1219
+ margin-top: calc((1lh - #{$form-check-input-width}) / 2);
826
1220
  }
827
1221
 
828
1222
  .form-switch .form-check-input {
829
- // OFF: light-red track, red thumb (left).
1223
+ // OFF: light-magenta track, deep-magenta thumb (left).
830
1224
  background-color: var(--dx-switch-off-bg);
831
1225
  border-color: var(--dx-switch-off-line);
832
- --bs-form-switch-bg: #{$dx-switch-thumb-red};
1226
+ --bs-form-switch-bg: #{$dx-switch-thumb-off};
833
1227
 
834
1228
  &:checked {
835
1229
  // ON: vivid green track, white thumb (right — Bootstrap positions it).
@@ -841,7 +1235,7 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
841
1235
  // Neutral grey focus ring (the track carries the state colour, so the brand
842
1236
  // navy ring would clash); keep the state thumb rather than Bootstrap's navy.
843
1237
  &:focus {
844
- --bs-form-switch-bg: #{$dx-switch-thumb-red};
1238
+ --bs-form-switch-bg: #{$dx-switch-thumb-off};
845
1239
  border-color: var(--dx-switch-off-line);
846
1240
  box-shadow: 0 0 0 0.25rem rgba($dark, 0.15);
847
1241
  }
@@ -875,59 +1269,86 @@ $dx-switch-on-soft: map.get(map.get($dx-variants, "success"), "soft-bg"); // #cd
875
1269
  // ----------------------------------------------------------------------------
876
1270
  // Chart palette (data-viz)
877
1271
  // ----------------------------------------------------------------------------
878
- // Dedicated categorical palette for the DX*Chart components — deliberately NOT
879
- // the semantic UI colours (#141). Two reasons: the base $theme-colors carry
1272
+ // Dedicated categorical palette for the DX*Chart components — its own lists,
1273
+ // NOT the semantic `--bs-*` variables (#141): the base $theme-colors carry
880
1274
  // dark "emphasis" shades (AA text/link colours, too muted for data series), and
881
- // status colours (success/danger) shouldn't impersonate "series 2" — a green or
882
- // red series should mean something.
1275
+ // a series colour must not move when a status colour is retuned. The fills do
1276
+ // share the fruity family's hues (slots 1/2/3/6 are the info/success/danger/
1277
+ // warning solids) so charts and badges read as one system; the order, not the
1278
+ // status meaning, is what the slots encode.
1279
+ //
1280
+ // Eight light, vivid FILLS in a FIXED order, plus a darker LINE shade per slot
1281
+ // and one EDGE colour. The fills are light (1.35–2.97:1 on white), so they
1282
+ // cannot carry WCAG 1.4.11 non-text contrast on their own; that job goes to:
1283
+ // - the EDGE: bars and doughnut segments get a 1px near-black outline
1284
+ // (#121419, the warning label ink; 18.42:1 on white), which separates each
1285
+ // mark from the white page and from its neighbours whatever the fill;
1286
+ // - the LINE shades: each fill darkened toward black (same hue) until it
1287
+ // clears 3.5:1 on white. Line strokes and point outlines use them; the
1288
+ // fill only shows as point centres and a 35% area wash.
883
1289
  //
884
- // Eight vivid mid-lightness hues in a FIXED order. The order is load-bearing:
885
- // it was derived by exhaustively permuting the hues (slot 1 pinned to the brand
886
- // info-blue) to maximise the minimum adjacent-pair colour-vision-deficiency
887
- // separation — validated at min adjacent ΔE 13.8 (OKLab ×100, protan/deutan
888
- // simulated), min normal-vision ΔE 20.3, every hue ≥ 3:1 contrast on white.
889
- // Reordering or swapping a hue needs that validation re-run, not an eyeball.
1290
+ // The order is load-bearing: slot 1 is pinned and slots 2..8 were chosen by
1291
+ // exhaustive permutation to maximise the minimum adjacent-pair separation
1292
+ // under colour-vision deficiency (OKLab ΔE ×100, Viénot protan/deutan). The
1293
+ // shipped order is the best available for both lists: fills min adjacent CVD
1294
+ // ΔE 19.56, lines 13.73 (weakest neighbours: the magenta and olive line
1295
+ // shades, slots 3/4). Min adjacent normal-vision ΔE: fills 21.41, lines 16.63.
1296
+ // Reordering, swapping a hue, or editing a line shade needs the validation
1297
+ // re-run, not an eyeball: `node scripts/validate-chart-palette.mjs` reads these
1298
+ // lists from this file, prints the scores, and checks every line shade is its
1299
+ // fill darkened to 3.5:1.
890
1300
  //
891
- // Published as --dx-chart-1..8 so consumers can retheme charts by overriding
892
- // the variables; chartTheme.ts reads them at runtime (with these hexes as
893
- // fallbacks for SSR / missing-CSS cases — keep the two lists in sync).
894
- $dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d97706, #db2777);
895
-
896
- // Dark-surface remap (#145). SAME hue slots — the CVD-derived order above is
897
- // load-bearing, so only lightness moves: each hue lifted into the dark band so
898
- // every step clears the 3:1 non-text guideline on Bootstrap's dark body
899
- // (#212529), where two of the light steps dipped below. Validated with the
900
- // same method as the light set (OKLab ΔE ×100, Viénot protan/deutan): min
901
- // adjacent CVD ΔE 15.1 (light set: 13.8), min normal ΔE 25.3, contrast
902
- // 5.67–10.23:1 on #212529. Swapping a step needs that validation re-run.
1301
+ // Published as --dx-chart-1..8 (fills), --dx-chart-line-1..8 and
1302
+ // --dx-chart-edge so consumers can retheme charts by overriding the
1303
+ // variables; chartTheme.ts reads them at runtime (with these hexes as
1304
+ // fallbacks for SSR / missing-CSS cases — keep the lists in sync; the sync is
1305
+ // enforced by tests/components/charts.test.ts, which parses this file).
1306
+ $dx-chart-palette: (#7fd7fd, #7bf25a, #e46ab9, #d6e86a, #b9a3f0, #efd574, #9fb4ff, #f7a072);
1307
+ $dx-chart-line-palette: (#5590aa, #4d9839, #d161a9, #848f41, #907fbb, #99884a, #7686be, #ba7956);
1308
+ $dx-chart-edge: #121419;
1309
+
1310
+ // Dark-surface remap (#145). The dark set predates the light fills above and
1311
+ // is unchanged: mid-lightness hues that already clear 3:1 on Bootstrap's dark
1312
+ // body (#212529) — 5.67–10.23:1 — so in dark mode the fills ARE the line
1313
+ // shades, and the edge is the dark body colour itself (an invisible 1px
1314
+ // separator between segments, as before the light edge existed). Validated
1315
+ // with the same method: min adjacent CVD ΔE 15.09, min adjacent normal ΔE
1316
+ // 25.33. Swapping a step needs that validation re-run.
903
1317
  // getPalette() reads these CSS variables live — off the CHART'S OWN container,
904
1318
  // so nested colour-mode scopes resolve correctly, and re-read on every
905
1319
  // `data-bs-theme` change so mounted charts repaint (#161). The TS fallbacks
906
1320
  // stay the light set (SSR/no-CSS default).
907
1321
  $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #fbbf24, #f472b6);
1322
+ $dx-chart-edge-dark: $body-bg-dark;
908
1323
 
909
- :root {
1324
+ @mixin dx-chart-light-vars {
910
1325
  @for $i from 1 through list.length($dx-chart-palette) {
911
1326
  --dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
1327
+ --dx-chart-line-#{$i}: #{list.nth($dx-chart-line-palette, $i)};
912
1328
  }
1329
+ --dx-chart-edge: #{$dx-chart-edge};
1330
+ }
1331
+
1332
+ :root {
1333
+ @include dx-chart-light-vars;
913
1334
  }
914
1335
 
915
1336
  [data-bs-theme="dark"] {
916
1337
  @for $i from 1 through list.length($dx-chart-palette-dark) {
917
1338
  --dx-chart-#{$i}: #{list.nth($dx-chart-palette-dark, $i)};
1339
+ --dx-chart-line-#{$i}: #{list.nth($dx-chart-palette-dark, $i)};
918
1340
  }
1341
+ --dx-chart-edge: #{$dx-chart-edge-dark};
919
1342
  }
920
1343
 
921
1344
  // Bootstrap allows `data-bs-theme` on ANY container, so colour-mode scopes
922
1345
  // nest — and custom properties inherit. Without this re-declaration a LIGHT
923
1346
  // scope inside a dark root (a light card on a dark page) would inherit the dark
924
1347
  // palette, because only `:root` publishes the light values and the dark block
925
- // above overrides it for the whole subtree (#161). Same list as `:root`, so the
926
- // two can't drift.
1348
+ // above overrides it for the whole subtree (#161). Same mixin as `:root`, so
1349
+ // the two can't drift.
927
1350
  [data-bs-theme="light"] {
928
- @for $i from 1 through list.length($dx-chart-palette) {
929
- --dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
930
- }
1351
+ @include dx-chart-light-vars;
931
1352
  }
932
1353
 
933
1354
  // The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped