@omnitend/dashboard-for-laravel 0.34.0 → 0.36.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.
- package/dist/charts.js +137 -97
- package/dist/charts.js.map +1 -1
- package/dist/charts.umd.cjs +1 -1
- package/dist/charts.umd.cjs.map +1 -1
- package/dist/components/charts/chartTheme.d.ts +29 -3
- package/dist/components/charts/useThemedChart.d.ts +7 -2
- package/dist/components/extended/DXForm.vue.d.ts +28 -6
- package/dist/components/extended/DXTable.vue.d.ts +38 -1
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +7 -2
- package/dist/components/extended/DXTablePagination.vue.d.ts +22 -1
- package/dist/composables/useContainerWidth.d.ts +96 -0
- package/dist/dashboard-for-laravel.js +8288 -8105
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/docs/public/api-reference.json +48 -8
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +96 -4
- package/resources/js/components/charts/DXBarChart.vue +7 -2
- package/resources/js/components/charts/DXDoughnutChart.vue +33 -10
- package/resources/js/components/charts/DXLineChart.vue +7 -2
- package/resources/js/components/charts/chartTheme.ts +106 -11
- package/resources/js/components/charts/useThemedChart.ts +39 -21
- package/resources/js/components/extended/DXDashboardSidebar.vue +1 -1
- package/resources/js/components/extended/DXForm.vue +83 -9
- package/resources/js/components/extended/DXTable.vue +216 -12
- package/resources/js/components/extended/DXTableEditorModal.vue +7 -2
- package/resources/js/components/extended/DXTablePagination.vue +165 -8
- package/resources/js/components/extended/DXTableShell.vue +105 -1
- package/resources/js/composables/useContainerWidth.ts +303 -0
- package/resources/js/index.ts +7 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-
|
|
2
|
+
"generated": "2026-07-21T12:50:16.789Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.36.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -2427,7 +2427,14 @@
|
|
|
2427
2427
|
"type": "union",
|
|
2428
2428
|
"required": false,
|
|
2429
2429
|
"default": "\"vertical\"",
|
|
2430
|
-
"description": "Form-wide field layout
|
|
2430
|
+
"description": "Form-wide field layout:\n\n- `\"vertical\"` (default) — label above input, always.\n- `\"horizontal\"` — label left, input right, always.\n- `\"auto\"` — horizontal when the form's **own container** is at least\n `layoutThreshold` px wide, vertical below that. Container-driven, not\n viewport-driven: a page narrowed by the dashboard sidebar, or a form\n inside a modal, stacks even though the window is wide (which no\n Bootstrap media query can see).\n\nOverridable per-field via `field.layout`. A field with `span: true`\nalways renders full-width, regardless of layout."
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
"name": "layoutThreshold",
|
|
2434
|
+
"type": "number",
|
|
2435
|
+
"required": false,
|
|
2436
|
+
"default": "640",
|
|
2437
|
+
"description": "Container width (px) at or above which `layout: \"auto\"` goes horizontal.\nIgnored for the explicit `\"vertical\"`/`\"horizontal\"` layouts.\n\nDefault 640, measured rather than guessed: with the default 3-column\nlabel the label's text area is `containerWidth / 4 - 18` px, so 640 gives\nit 142px — enough for a ~20-character label (\"Unit price (ex VAT)\"\nmeasures 128px at the theme's label font) to stay on one line, with the\ncontrol column still 474px. Below ~584px that label starts wrapping,\nwhich is the cramped-label symptom this exists to avoid. Raise it if your\nlabels run longer, or lower `labelCols` instead."
|
|
2431
2438
|
},
|
|
2432
2439
|
{
|
|
2433
2440
|
"name": "labelCols",
|
|
@@ -3314,7 +3321,7 @@
|
|
|
3314
3321
|
"type": "boolean",
|
|
3315
3322
|
"required": false,
|
|
3316
3323
|
"default": "true",
|
|
3317
|
-
"description": "Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,\nborderless variant — the same header, filter row, table and pagination\nrendered directly on the page background (no card border/shadow), for\ndata-heavy admin index pages that read as busy inside a card."
|
|
3324
|
+
"description": "Wrap the table in a `DCard`. Defaults to `true`. Set `false` for a plain,\nborderless variant — the same header, filter row, table and pagination\nrendered directly on the page background (no card border/shadow), for\ndata-heavy admin index pages that read as busy inside a card.\n\nThe table renders FLUSH to the card border (rows and stripes reach the\nedge) while the header and pagination stay padded. Only the table region\nis rounded to follow the card's corners — the card itself does not clip\n(#166), so a `position: absolute` popover you render into `#header` or\nany other slot is free to overhang the card.\n\nThe one place content is still clipped is INSIDE the table: Bootstrap's\n`.table-responsive` is a horizontal scroll container, so an overlay in a\n`cell(<key>)` slot is bounded by it. Teleport it (bvn's own menus already\ndo — `DDropdown` teleports to `<body>`, the column-filter menus are\n`fixed`), or pass `:responsive=\"false\"`."
|
|
3318
3325
|
},
|
|
3319
3326
|
{
|
|
3320
3327
|
"name": "editFields",
|
|
@@ -3346,7 +3353,7 @@
|
|
|
3346
3353
|
"type": "union",
|
|
3347
3354
|
"required": false,
|
|
3348
3355
|
"default": "\"horizontal\"",
|
|
3349
|
-
"description": "Field layout for the edit/create modal's form, forwarded to its `DXForm`\n(\"vertical\" = label above input, \"horizontal\" = label left). Defaults to\n**\"horizontal\"** to match the Omni Tend form convention (a raw modal form\npreviously fell back to DXForm's vertical default, which looked out of\nplace next to horizontal page forms). Pass `\"vertical\"` to opt out."
|
|
3356
|
+
"description": "Field layout for the edit/create modal's form, forwarded to its `DXForm`\n(\"vertical\" = label above input, \"horizontal\" = label left). Defaults to\n**\"horizontal\"** to match the Omni Tend form convention (a raw modal form\npreviously fell back to DXForm's vertical default, which looked out of\nplace next to horizontal page forms). Pass `\"vertical\"` to opt out.\n\n`\"auto\"` hands the decision to `DXForm`'s container-width observer, which\nmeasures the FORM rather than the window — so a modal narrowed by its own\nbreakpoint stacks to vertical even on a wide viewport, where a media query\nwould still report \"plenty of room\". See `useContainerWidth`."
|
|
3350
3357
|
},
|
|
3351
3358
|
{
|
|
3352
3359
|
"name": "editLabelCols",
|
|
@@ -3578,6 +3585,32 @@
|
|
|
3578
3585
|
}
|
|
3579
3586
|
]
|
|
3580
3587
|
},
|
|
3588
|
+
{
|
|
3589
|
+
"name": "`head-end(${field.key})`",
|
|
3590
|
+
"description": "head-end(<fieldKey>) Additive content at the end of a column's own header — a period total above a numeric column, a small badge — keeping DXTable's sort indicator and field hint. Not a `head()` override.",
|
|
3591
|
+
"bindings": [
|
|
3592
|
+
{
|
|
3593
|
+
"name": "name",
|
|
3594
|
+
"title": "binding"
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
"title": "binding",
|
|
3598
|
+
"type": {
|
|
3599
|
+
"name": "object"
|
|
3600
|
+
},
|
|
3601
|
+
"name": "field",
|
|
3602
|
+
"description": "The column's field definition."
|
|
3603
|
+
},
|
|
3604
|
+
{
|
|
3605
|
+
"title": "binding",
|
|
3606
|
+
"type": {
|
|
3607
|
+
"name": "string"
|
|
3608
|
+
},
|
|
3609
|
+
"name": "label",
|
|
3610
|
+
"description": "The column's resolved header label."
|
|
3611
|
+
}
|
|
3612
|
+
]
|
|
3613
|
+
},
|
|
3581
3614
|
{
|
|
3582
3615
|
"name": "name",
|
|
3583
3616
|
"description": "Any slot the underlying table supports, forwarded with its scope: `cell(<fieldKey>)` for a cell, `foot(<fieldKey>)` for a footer cell (needs `footClone`), `empty` for the no-rows message, `row-expansion` for expandable row detail, plus `custom-foot`, `top-row`, `bottom-row`, `thead-sub` and the `table-*` slots.",
|
|
@@ -3679,7 +3712,7 @@
|
|
|
3679
3712
|
"name": "layout",
|
|
3680
3713
|
"type": "union",
|
|
3681
3714
|
"required": false,
|
|
3682
|
-
"description": "Field layout forwarded to the modal's DXForm (defaults handled upstream)."
|
|
3715
|
+
"description": "Field layout forwarded to the modal's DXForm (defaults handled upstream).\nIncludes `\"auto\"`, which lets the form stack to vertical when the MODAL is\nnarrow even though the viewport is wide — the container-query case a\nviewport media query cannot see."
|
|
3683
3716
|
},
|
|
3684
3717
|
{
|
|
3685
3718
|
"name": "labelCols",
|
|
@@ -3915,7 +3948,7 @@
|
|
|
3915
3948
|
"name": "DXTablePagination",
|
|
3916
3949
|
"category": "extended",
|
|
3917
3950
|
"filePath": "resources/js/components/extended/DXTablePagination.vue",
|
|
3918
|
-
"description": "Internal pagination + per-page footer for `DXTable`. Renders the pager, the per-page selector, and the \"X to Y out of Z items\" info line (with the \"Filtered from N\" note when a column filter is active). Extracted because `DXTable` used to carry three byte-identical copies of this footer — one per data mode (client-side / inertia / provider) — that differed only in which `PaginationData` object and page-change handler they used, so every footer fix had to be written three times (#123). `DXTable` now derives the active pagination + handler and renders this once. It owns its own `:deep(.pagination …)` styling (rather than leaving it in `DXTable`), because a scoped `:deep()` in `DXTable` would not reach the pager once it lives in a child component — the scope-id doesn't cross the boundary. Anchoring the rules on this component's own `.dx-table-pagination` root (a plain element it owns) gives the scope-id a deterministic host.",
|
|
3951
|
+
"description": "Internal pagination + per-page footer for `DXTable`. Renders the pager, the per-page selector, and the \"X to Y out of Z items\" info line (with the \"Filtered from N\" note when a column filter is active). Extracted because `DXTable` used to carry three byte-identical copies of this footer — one per data mode (client-side / inertia / provider) — that differed only in which `PaginationData` object and page-change handler they used, so every footer fix had to be written three times (#123). `DXTable` now derives the active pagination + handler and renders this once. It owns its own `:deep(.pagination …)` styling (rather than leaving it in `DXTable`), because a scoped `:deep()` in `DXTable` would not reach the pager once it lives in a child component — the scope-id doesn't cross the boundary. Anchoring the rules on this component's own `.dx-table-pagination` root (a plain element it owns) gives the scope-id a deterministic host. In a row too narrow for the full windowed pager, the number strip collapses to a \"current / last\" status with Previous/Next either side (#162). The decision is CONTAINER-driven, not viewport-driven — see `compactThreshold`.",
|
|
3919
3952
|
"props": [
|
|
3920
3953
|
{
|
|
3921
3954
|
"name": "pagination",
|
|
@@ -3970,6 +4003,13 @@
|
|
|
3970
4003
|
"type": "boolean",
|
|
3971
4004
|
"required": true,
|
|
3972
4005
|
"description": "Whether a column filter is active — gates the \"Filtered from N\" note."
|
|
4006
|
+
},
|
|
4007
|
+
{
|
|
4008
|
+
"name": "compactThreshold",
|
|
4009
|
+
"type": "number",
|
|
4010
|
+
"required": false,
|
|
4011
|
+
"default": "576",
|
|
4012
|
+
"description": "Upper bound (px) on the pager ROW width at which the number window may\ncollapse to the compact \"current / last\" status (#162). At or above it\nthe full windowed pager always renders, whatever the page count.\n\nDefault **576** — Bootstrap's `sm` breakpoint, i.e. the width the rest of\nthe design system already treats as phone-sized. Its job is to GUARANTEE\nthat a desktop-width table is never quietly downgraded: a 45-page window\nthat doesn't quite fit at 800px should wrap (today's behaviour), not lose\nits numbers.\n\nBelow it, compact engages only when the full pager genuinely would not\nfit on one line (see `fullPagerWidth`) — so a three-page pager still\nshows `1 2 3` on a phone, where \"2 / 3\" would be strictly worse.\n\nMeasured on the ROW, not the viewport, so a table squeezed by a sidebar\nor inside a modal collapses even though the window is wide."
|
|
3973
4013
|
}
|
|
3974
4014
|
],
|
|
3975
4015
|
"events": [
|
|
@@ -3991,7 +4031,7 @@
|
|
|
3991
4031
|
"name": "DXTableShell",
|
|
3992
4032
|
"category": "extended",
|
|
3993
4033
|
"filePath": "resources/js/components/extended/DXTableShell.vue",
|
|
3994
|
-
"description": "Internal chrome wrapper for `DXTable`. Renders its content either inside a `DCard` (the default) or as a plain, borderless block on the page background (`:card=\"false\"`). Extracted so `DXTable`'s ~460-line body and all its slots aren't duplicated across a card/plain `v-if`. Slots: - `#header` — the title / action row. In card mode it fills `DCard`'s header; in plain mode it renders above the table with a subtle bottom rule. - default — the table body (spinner/error/table/pagination).",
|
|
4034
|
+
"description": "Internal chrome wrapper for `DXTable`. Renders its content either inside a `DCard` (the default) or as a plain, borderless block on the page background (`:card=\"false\"`). Extracted so `DXTable`'s ~460-line body and all its slots aren't duplicated across a card/plain `v-if`. Slots: - `#header` — the title / action row. In card mode it fills `DCard`'s header; in plain mode it renders above the table with a subtle bottom rule. - default — the table body (spinner/error/table/pagination). Card mode renders `DCard` in **`no-body`** mode so the table sits FLUSH against the card border. With a `.card-body` in between, a striped table's rows were inset by `--bs-card-spacer-x` (24px) and left a white gutter between the stripes and the border. Without it, DXTable's children — error alert, loading block, table, pagination — become direct children of `.card`, which is already a `flex-direction: column` container, so they still stack in order: .card ├─ .card-header (padded by Bootstrap's card-cap padding) ├─ .table-responsive ← FLUSH, rounded when it is at a card corner │ └─ table └─ .dx-table-pagination (pads itself — see DXTablePagination.vue) Bootstrap 5 has no `.card > .table` rounding rules (that was BS4), so the flush table's square corners have to be rounded here. The rounding is applied to the TABLE REGION, never to the card: clipping the whole card would cut off any non-teleported positioned content a consumer puts in a slot (#166).",
|
|
3995
4035
|
"props": [
|
|
3996
4036
|
{
|
|
3997
4037
|
"name": "card",
|
package/docs/public/docs-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Documentation Map
|
|
2
2
|
|
|
3
3
|
> Auto-generated hierarchical overview of all documentation
|
|
4
|
-
> Last updated: 2026-07-
|
|
4
|
+
> Last updated: 2026-07-21T12:50:16.844Z
|
|
5
5
|
|
|
6
6
|
This file provides a complete map of all available documentation for AI agents and developers.
|
|
7
7
|
|
package/package.json
CHANGED
package/resources/css/theme.scss
CHANGED
|
@@ -313,6 +313,32 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
313
313
|
flex-wrap: nowrap;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
// ----------------------------------------------------------------------------
|
|
317
|
+
// Nav tabs
|
|
318
|
+
// ----------------------------------------------------------------------------
|
|
319
|
+
// Chrome recedes, content leads — the same ethos as the muted table headers
|
|
320
|
+
// (#157). Bootstrap gives an INACTIVE tab `--bs-nav-link-color:
|
|
321
|
+
// var(--bs-link-color)`, i.e. the saturated link-blue, so a row of tabs above a
|
|
322
|
+
// form reads as a row of links competing with the fields below. Mute the
|
|
323
|
+
// inactive tabs and let the ACTIVE one (Bootstrap's
|
|
324
|
+
// `--bs-nav-tabs-link-active-color`, the near-black emphasis colour) carry the
|
|
325
|
+
// selection on its own.
|
|
326
|
+
//
|
|
327
|
+
// Driven through Bootstrap's own `--bs-nav-*` variables rather than a raw
|
|
328
|
+
// `color:` — so a consumer can re-louden one tab set by resetting the same
|
|
329
|
+
// variable, `[data-bs-theme="dark"]` remaps the tokens for free, and Bootstrap
|
|
330
|
+
// keeps ownership of the `.active`/`.disabled` resolution order.
|
|
331
|
+
//
|
|
332
|
+
// `--bs-secondary-color` is `rgba(var(--bs-body-color-rgb), .75)` → ~#585b5e on
|
|
333
|
+
// white: 6.83:1, comfortably AA (AAA for body text); 6.53:1 on the theme's
|
|
334
|
+
// `$light` #f8fafc panel. Hover goes to the full body colour (#212529, 16.1:1)
|
|
335
|
+
// so hovering reads as "heading toward selected" rather than "turning into a
|
|
336
|
+
// link".
|
|
337
|
+
.nav-tabs {
|
|
338
|
+
--bs-nav-link-color: var(--bs-secondary-color);
|
|
339
|
+
--bs-nav-link-hover-color: var(--bs-body-color);
|
|
340
|
+
}
|
|
341
|
+
|
|
316
342
|
// ----------------------------------------------------------------------------
|
|
317
343
|
// Dashboard Custom Styles
|
|
318
344
|
// ----------------------------------------------------------------------------
|
|
@@ -433,6 +459,40 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
433
459
|
color: $secondary; // muted message text
|
|
434
460
|
}
|
|
435
461
|
|
|
462
|
+
// HEADER-LESS toast (`body` but no `title` — the shape every toast created
|
|
463
|
+
// through `useToast().create({ body })` takes). BVN renders it as
|
|
464
|
+
// .toast > .d-flex > (.toast-body + .btn-close)
|
|
465
|
+
// and BVN's own `.toast .btn-close-custom { margin: <pad> <pad> auto }` pins
|
|
466
|
+
// the close X to the TOP of that row, while the `.toast-body` above still
|
|
467
|
+
// carries the reduced `padding-top` that only makes sense when it sits under
|
|
468
|
+
// a `.toast-header`. Result: the message rides high, the X sits lower, and a
|
|
469
|
+
// one-line toast reads as misaligned.
|
|
470
|
+
//
|
|
471
|
+
// With no header there is nothing above the body, so restore symmetric
|
|
472
|
+
// padding (Bootstrap's `.toast-body` is `padding: var(--bs-toast-padding-x)`
|
|
473
|
+
// all round) and centre the row. The `margin-bottom: auto` has to go too —
|
|
474
|
+
// an auto margin outranks `align-items` in flex alignment, so centring alone
|
|
475
|
+
// would leave the X pinned; `margin-block: 0` hands alignment back to the
|
|
476
|
+
// container while keeping BVN's horizontal margin.
|
|
477
|
+
//
|
|
478
|
+
// `:has()` is the structural test — it is the only way to key off "this toast
|
|
479
|
+
// has no header" from CSS, and it is supported in every evergreen browser
|
|
480
|
+
// (Chrome/Edge 105+, Safari 15.4+, Firefox 121+). A browser without it simply
|
|
481
|
+
// keeps today's slightly-off alignment; nothing breaks.
|
|
482
|
+
&:not(:has(.toast-header)) {
|
|
483
|
+
> .d-flex {
|
|
484
|
+
align-items: center;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.toast-body {
|
|
488
|
+
padding-top: var(--bs-toast-padding-x);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.btn-close-custom {
|
|
492
|
+
margin-block: 0;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
436
496
|
// Hide the auto-dismiss countdown bar — the ticking animation reads as
|
|
437
497
|
// stressful/urgent, which is wrong for a passive notification. Auto-dismiss
|
|
438
498
|
// still works; only its visual indicator is removed.
|
|
@@ -530,7 +590,18 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
530
590
|
// (which sets its colours `!important` by design). `.toast` is excluded: it
|
|
531
591
|
// has its own soft handling (a fainter 50%-white mix via `--bs-toast-bg` for
|
|
532
592
|
// its large surface) and themed toasts don't carry `.text-bg-*` anyway.
|
|
533
|
-
|
|
593
|
+
//
|
|
594
|
+
// The exclusion is wrapped in `:where()` DELIBERATELY: `:where()` always
|
|
595
|
+
// contributes ZERO specificity, so this stays a single-class 0,1,0 selector
|
|
596
|
+
// while matching exactly what a bare `:not(.toast)` would. Written bare it is
|
|
597
|
+
// 0,2,0 — and because the declarations are `!important`, a consumer painting
|
|
598
|
+
// a fully custom badge (`.my-status-badge { background: …; }`) could not win
|
|
599
|
+
// with a single-class `!important` rule and had to escalate to a
|
|
600
|
+
// repeated-class hack. One class in, one class out: a consumer's own
|
|
601
|
+
// `!important` single-class rule now overrides us on source order, as it
|
|
602
|
+
// should. (Everything Bootstrap defines for `.text-bg-*` is also 0,1,0
|
|
603
|
+
// `!important` and is imported ABOVE this block, so we still win it.)
|
|
604
|
+
.text-bg-#{$name}:where(:not(.toast)) {
|
|
534
605
|
background-color: #{$soft-bg} !important;
|
|
535
606
|
color: #{$soft-text} !important;
|
|
536
607
|
}
|
|
@@ -551,7 +622,14 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
551
622
|
// (danger/info's solid IS their emphasis). The variant-less default bar
|
|
552
623
|
// stays `$progress-bar-bg` (brand navy). `!important` beats the `.bg-*`
|
|
553
624
|
// utility, which sets its colour `!important` by design.
|
|
554
|
-
|
|
625
|
+
//
|
|
626
|
+
// `.progress-bar` is only here to SCOPE the rule (large fills, not every
|
|
627
|
+
// `.bg-*` element), so it goes in `:where()` — zero specificity, same
|
|
628
|
+
// matching, and the selector stays 0,1,0 like Bootstrap's own `.bg-*`. Same
|
|
629
|
+
// reasoning as the `.text-bg-*` rule above: at 0,2,0 `!important` a consumer
|
|
630
|
+
// recolouring one bar had to out-specify us instead of just writing
|
|
631
|
+
// `.my-bar { background-color: … !important }`.
|
|
632
|
+
:where(.progress-bar).bg-#{$name} {
|
|
555
633
|
background-color: #{$solid-bg} !important;
|
|
556
634
|
color: #{$solid-text};
|
|
557
635
|
}
|
|
@@ -716,8 +794,10 @@ $dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d9770
|
|
|
716
794
|
// same method as the light set (OKLab ΔE ×100, Viénot protan/deutan): min
|
|
717
795
|
// adjacent CVD ΔE 15.1 (light set: 13.8), min normal ΔE 25.3, contrast
|
|
718
796
|
// 5.67–10.23:1 on #212529. Swapping a step needs that validation re-run.
|
|
719
|
-
// getPalette() reads
|
|
720
|
-
//
|
|
797
|
+
// getPalette() reads these CSS variables live — off the CHART'S OWN container,
|
|
798
|
+
// so nested colour-mode scopes resolve correctly, and re-read on every
|
|
799
|
+
// `data-bs-theme` change so mounted charts repaint (#161). The TS fallbacks
|
|
800
|
+
// stay the light set (SSR/no-CSS default).
|
|
721
801
|
$dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #fbbf24, #f472b6);
|
|
722
802
|
|
|
723
803
|
:root {
|
|
@@ -732,6 +812,18 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
|
|
|
732
812
|
}
|
|
733
813
|
}
|
|
734
814
|
|
|
815
|
+
// Bootstrap allows `data-bs-theme` on ANY container, so colour-mode scopes
|
|
816
|
+
// nest — and custom properties inherit. Without this re-declaration a LIGHT
|
|
817
|
+
// scope inside a dark root (a light card on a dark page) would inherit the dark
|
|
818
|
+
// palette, because only `:root` publishes the light values and the dark block
|
|
819
|
+
// above overrides it for the whole subtree (#161). Same list as `:root`, so the
|
|
820
|
+
// two can't drift.
|
|
821
|
+
[data-bs-theme="light"] {
|
|
822
|
+
@for $i from 1 through list.length($dx-chart-palette) {
|
|
823
|
+
--dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
735
827
|
// The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped
|
|
736
828
|
// from the `@omnitend/dashboard-for-laravel/charts` entry, #142). Global rather
|
|
737
829
|
// than a component scoped style so the charts entry emits NO CSS — chart
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
Requires the optional peer deps `chart.js` and `vue-chartjs`.
|
|
9
9
|
-->
|
|
10
10
|
<template>
|
|
11
|
-
<div class="dx-chart">
|
|
11
|
+
<div ref="host" class="dx-chart">
|
|
12
12
|
<Bar :data="chartData" :options="mergedOptions" />
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
16
|
<script setup lang="ts">
|
|
17
|
+
import { ref } from "vue";
|
|
17
18
|
import { Bar } from "vue-chartjs";
|
|
18
19
|
import { registerCharts } from "./chartTheme";
|
|
19
20
|
import { useThemedChart, type ThemedChartProps } from "./useThemedChart";
|
|
@@ -27,5 +28,9 @@ const props = withDefaults(defineProps<ThemedChartProps<"bar">>(), {
|
|
|
27
28
|
showLegend: undefined,
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// The container is the `data-bs-theme` scope the palette resolves against, so a
|
|
32
|
+
// chart inside a dark card on a light page themes from the card (#161).
|
|
33
|
+
const host = ref<HTMLElement | null>(null);
|
|
34
|
+
|
|
35
|
+
const { chartData, mergedOptions } = useThemedChart("bar", props, host);
|
|
31
36
|
</script>
|
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
Requires the optional peer deps `chart.js` and `vue-chartjs`.
|
|
9
9
|
-->
|
|
10
10
|
<template>
|
|
11
|
-
<div class="dx-chart">
|
|
11
|
+
<div ref="host" class="dx-chart">
|
|
12
12
|
<Doughnut :data="chartData" :options="mergedOptions" />
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
16
|
<script setup lang="ts">
|
|
17
|
-
import { computed } from "vue";
|
|
17
|
+
import { computed, ref } from "vue";
|
|
18
18
|
import { Doughnut } from "vue-chartjs";
|
|
19
19
|
import {
|
|
20
20
|
registerCharts,
|
|
21
21
|
baseOptions,
|
|
22
22
|
applyPalette,
|
|
23
23
|
mergeOptions,
|
|
24
|
+
useColorModeVersion,
|
|
24
25
|
type ChartValueFormat,
|
|
25
26
|
} from "./chartTheme";
|
|
26
27
|
|
|
@@ -52,25 +53,47 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
52
53
|
showLegend: undefined,
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
// The container is the `data-bs-theme` scope the palette resolves against, so a
|
|
57
|
+
// chart inside a dark card on a light page themes from the card. Reading the ref
|
|
58
|
+
// inside the computeds also re-themes on mount, when the first (pre-ref) render
|
|
59
|
+
// had to fall back to the document root (#161). DXDoughnutChart doesn't share
|
|
60
|
+
// `useThemedChart` (its data shape differs), so the wiring is repeated here.
|
|
61
|
+
const host = ref<HTMLElement | null>(null);
|
|
62
|
+
|
|
63
|
+
// Bumped by a MutationObserver on every `data-bs-theme` change; read below so
|
|
64
|
+
// the CSS-variable palette — invisible to Vue's reactivity — is re-resolved
|
|
65
|
+
// when the colour mode flips under a mounted chart.
|
|
66
|
+
const colorModeVersion = useColorModeVersion();
|
|
67
|
+
|
|
55
68
|
const resolvedDatasets = computed(() =>
|
|
56
69
|
props.datasets ?? [{ data: props.data ?? [] }],
|
|
57
70
|
);
|
|
58
71
|
|
|
59
|
-
const chartData = computed(() =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
const chartData = computed(() => {
|
|
73
|
+
void colorModeVersion.value;
|
|
74
|
+
return {
|
|
75
|
+
labels: props.labels,
|
|
76
|
+
datasets: applyPalette(
|
|
77
|
+
resolvedDatasets.value,
|
|
78
|
+
"doughnut",
|
|
79
|
+
props.labels.length,
|
|
80
|
+
host.value,
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
63
84
|
|
|
64
|
-
const mergedOptions = computed(() =>
|
|
65
|
-
|
|
85
|
+
const mergedOptions = computed(() => {
|
|
86
|
+
void colorModeVersion.value;
|
|
87
|
+
return mergeOptions(
|
|
66
88
|
baseOptions({
|
|
67
89
|
valueFormat: props.valueFormat,
|
|
68
90
|
currencySymbol: props.currencySymbol,
|
|
69
91
|
locale: props.locale,
|
|
70
92
|
showLegend: props.showLegend ?? true,
|
|
71
93
|
hasValueAxis: false,
|
|
94
|
+
scope: host.value,
|
|
72
95
|
}),
|
|
73
96
|
props.options ?? {},
|
|
74
|
-
)
|
|
75
|
-
);
|
|
97
|
+
);
|
|
98
|
+
});
|
|
76
99
|
</script>
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
Requires the optional peer deps `chart.js` and `vue-chartjs`.
|
|
9
9
|
-->
|
|
10
10
|
<template>
|
|
11
|
-
<div class="dx-chart">
|
|
11
|
+
<div ref="host" class="dx-chart">
|
|
12
12
|
<Line :data="chartData" :options="mergedOptions" />
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
16
|
<script setup lang="ts">
|
|
17
|
+
import { ref } from "vue";
|
|
17
18
|
import { Line } from "vue-chartjs";
|
|
18
19
|
import { registerCharts } from "./chartTheme";
|
|
19
20
|
import { useThemedChart, type ThemedChartProps } from "./useThemedChart";
|
|
@@ -27,5 +28,9 @@ const props = withDefaults(defineProps<ThemedChartProps<"line">>(), {
|
|
|
27
28
|
showLegend: undefined,
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// The container is the `data-bs-theme` scope the palette resolves against, so a
|
|
32
|
+
// chart inside a dark card on a light page themes from the card (#161).
|
|
33
|
+
const host = ref<HTMLElement | null>(null);
|
|
34
|
+
|
|
35
|
+
const { chartData, mergedOptions } = useThemedChart("line", props, host);
|
|
31
36
|
</script>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* registration. chart.js / vue-chartjs are OPTIONAL peer deps.
|
|
6
6
|
*/
|
|
7
7
|
import { Chart, registerables } from "chart.js";
|
|
8
|
+
import { getCurrentScope, onScopeDispose, ref, type Ref } from "vue";
|
|
8
9
|
|
|
9
10
|
export type ChartValueFormat = "number" | "currency" | "percent";
|
|
10
11
|
|
|
@@ -59,12 +60,37 @@ export const PALETTE_VARS: Array<[string, string]> = [
|
|
|
59
60
|
["--dx-chart-8", "#db2777"], // pink
|
|
60
61
|
];
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The element whose computed style the theme variables are read from.
|
|
65
|
+
*
|
|
66
|
+
* Bootstrap 5.3 allows `data-bs-theme` on ANY container, not just `<html>` — a
|
|
67
|
+
* dark card on a light page is a supported pattern. Reading the variables off
|
|
68
|
+
* `document.documentElement` therefore gives a *nested* scope the wrong palette
|
|
69
|
+
* (#161), so every resolver takes the chart's own container and only falls back
|
|
70
|
+
* to the root when it has none (SSR, a caller outside a component).
|
|
71
|
+
*/
|
|
72
|
+
function themeScope(scope?: Element | null): Element | null {
|
|
64
73
|
if (typeof document === "undefined" || typeof getComputedStyle === "undefined") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (scope !== undefined && scope !== null) return scope;
|
|
77
|
+
return document.documentElement;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the themed categorical palette.
|
|
82
|
+
*
|
|
83
|
+
* @param scope The chart's container element; its computed style decides which
|
|
84
|
+
* `data-bs-theme` scope applies. Omit (or pass null) to read the document
|
|
85
|
+
* root — the pre-#161 behaviour, kept so the exported signature stays
|
|
86
|
+
* backward compatible.
|
|
87
|
+
*/
|
|
88
|
+
export function getPalette(scope?: Element | null): string[] {
|
|
89
|
+
const host = themeScope(scope);
|
|
90
|
+
if (host === null) {
|
|
65
91
|
return PALETTE_VARS.map(([, fallback]) => fallback);
|
|
66
92
|
}
|
|
67
|
-
const styles = getComputedStyle(
|
|
93
|
+
const styles = getComputedStyle(host);
|
|
68
94
|
return PALETTE_VARS.map(([name, fallback]) => {
|
|
69
95
|
const value = styles.getPropertyValue(name).trim();
|
|
70
96
|
return value || fallback;
|
|
@@ -72,16 +98,73 @@ export function getPalette(): string[] {
|
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
/** Read a single theme colour (e.g. the body text colour) with a fallback. */
|
|
75
|
-
function themeColor(name: string, fallback: string): string {
|
|
76
|
-
|
|
101
|
+
function themeColor(name: string, fallback: string, scope?: Element | null): string {
|
|
102
|
+
const host = themeScope(scope);
|
|
103
|
+
if (host === null) {
|
|
77
104
|
return fallback;
|
|
78
105
|
}
|
|
79
|
-
const value = getComputedStyle(
|
|
80
|
-
.getPropertyValue(name)
|
|
81
|
-
.trim();
|
|
106
|
+
const value = getComputedStyle(host).getPropertyValue(name).trim();
|
|
82
107
|
return value || fallback;
|
|
83
108
|
}
|
|
84
109
|
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Colour-mode reactivity (#161)
|
|
112
|
+
//
|
|
113
|
+
// The palette lives in CSS variables, which Vue cannot track: a computed that
|
|
114
|
+
// calls getPalette() depends only on the props it also reads, so flipping
|
|
115
|
+
// `data-bs-theme` repaints the page but leaves every MOUNTED chart on the old
|
|
116
|
+
// palette until a prop changes or it remounts. One shared MutationObserver
|
|
117
|
+
// bumps a version ref that the chart computeds read, which re-resolves the
|
|
118
|
+
// variables. `subtree: true` because the attribute can sit on any container,
|
|
119
|
+
// and `attributeFilter` keeps the observer from firing on unrelated mutations.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const colorModeVersion = ref(0);
|
|
123
|
+
let colorModeObserver: MutationObserver | null = null;
|
|
124
|
+
let colorModeWatchers = 0;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A version ref that increments whenever a `data-bs-theme` attribute changes
|
|
128
|
+
* anywhere in the document. Read it inside a computed that resolves theme
|
|
129
|
+
* variables to make that computed re-run on a colour-mode switch.
|
|
130
|
+
*
|
|
131
|
+
* The observer is started on first use and disconnected when the last consumer's
|
|
132
|
+
* effect scope is disposed, so nothing leaks after the charts unmount. Called
|
|
133
|
+
* outside an effect scope (or without a DOM) it is a no-op returning a static
|
|
134
|
+
* ref — there would be no teardown hook to disconnect the observer.
|
|
135
|
+
*/
|
|
136
|
+
export function useColorModeVersion(): Ref<number> {
|
|
137
|
+
if (
|
|
138
|
+
typeof document === "undefined" ||
|
|
139
|
+
typeof MutationObserver === "undefined" ||
|
|
140
|
+
getCurrentScope() === undefined
|
|
141
|
+
) {
|
|
142
|
+
return colorModeVersion;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
colorModeWatchers += 1;
|
|
146
|
+
if (colorModeObserver === null) {
|
|
147
|
+
colorModeObserver = new MutationObserver(() => {
|
|
148
|
+
colorModeVersion.value += 1;
|
|
149
|
+
});
|
|
150
|
+
colorModeObserver.observe(document.documentElement, {
|
|
151
|
+
attributes: true,
|
|
152
|
+
attributeFilter: ["data-bs-theme"],
|
|
153
|
+
subtree: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onScopeDispose(() => {
|
|
158
|
+
colorModeWatchers -= 1;
|
|
159
|
+
if (colorModeWatchers === 0 && colorModeObserver !== null) {
|
|
160
|
+
colorModeObserver.disconnect();
|
|
161
|
+
colorModeObserver = null;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return colorModeVersion;
|
|
166
|
+
}
|
|
167
|
+
|
|
85
168
|
/** Add an alpha channel to a hex or rgb() colour (best-effort). */
|
|
86
169
|
export function withAlpha(color: string, alpha: number): string {
|
|
87
170
|
const hex = color.replace(/^#/, "");
|
|
@@ -122,6 +205,9 @@ export interface BaseOptionArgs {
|
|
|
122
205
|
showLegend?: boolean;
|
|
123
206
|
/** Whether the chart has value axes (bar/line) vs none (doughnut). */
|
|
124
207
|
hasValueAxis?: boolean;
|
|
208
|
+
/** The chart's container element — decides which `data-bs-theme` scope the
|
|
209
|
+
* grid/tick colours resolve against (#161). Defaults to the document root. */
|
|
210
|
+
scope?: Element | null;
|
|
125
211
|
}
|
|
126
212
|
|
|
127
213
|
/** Dashboard default Chart.js options merged under any consumer `options`. */
|
|
@@ -132,10 +218,14 @@ export function baseOptions(args: BaseOptionArgs): Record<string, any> {
|
|
|
132
218
|
locale,
|
|
133
219
|
showLegend = false,
|
|
134
220
|
hasValueAxis = true,
|
|
221
|
+
scope,
|
|
135
222
|
} = args;
|
|
136
223
|
|
|
137
|
-
const gridColor = withAlpha(
|
|
138
|
-
|
|
224
|
+
const gridColor = withAlpha(
|
|
225
|
+
themeColor("--bs-border-color", "#dee2e6", scope),
|
|
226
|
+
0.6,
|
|
227
|
+
);
|
|
228
|
+
const tickColor = themeColor("--bs-secondary-color", "#6c757d", scope);
|
|
139
229
|
|
|
140
230
|
const options: Record<string, any> = {
|
|
141
231
|
responsive: true,
|
|
@@ -185,13 +275,18 @@ export function baseOptions(args: BaseOptionArgs): Record<string, any> {
|
|
|
185
275
|
* Apply the themed palette to any dataset that doesn't set its own colours.
|
|
186
276
|
* Bar: one colour per dataset (or per bar for a single dataset). Line: a themed
|
|
187
277
|
* stroke + translucent fill. Doughnut/pie: one colour per slice.
|
|
278
|
+
*
|
|
279
|
+
* `scope` is the chart's container element; the palette is resolved from its
|
|
280
|
+
* computed style so a nested `data-bs-theme` container themes correctly (#161).
|
|
281
|
+
* Omit it to resolve against the document root.
|
|
188
282
|
*/
|
|
189
283
|
export function applyPalette(
|
|
190
284
|
datasets: any[],
|
|
191
285
|
kind: "bar" | "line" | "doughnut",
|
|
192
286
|
labelsCount: number,
|
|
287
|
+
scope?: Element | null,
|
|
193
288
|
): any[] {
|
|
194
|
-
const palette = getPalette();
|
|
289
|
+
const palette = getPalette(scope);
|
|
195
290
|
const single = datasets.length === 1;
|
|
196
291
|
|
|
197
292
|
return datasets.map((dataset, index) => {
|