@omnitend/dashboard-for-laravel 0.35.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/DXTable.vue.d.ts +9 -10
- package/dist/components/extended/DXTablePagination.vue.d.ts +22 -1
- package/dist/dashboard-for-laravel.js +3931 -3888
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +6 -6
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/docs/public/api-reference.json +12 -5
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +16 -2
- 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/DXTable.vue +9 -10
- package/resources/js/components/extended/DXTablePagination.vue +146 -5
- package/resources/js/components/extended/DXTableShell.vue +49 -14
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated": "2026-07-21T12:16
|
|
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": [
|
|
@@ -3321,7 +3321,7 @@
|
|
|
3321
3321
|
"type": "boolean",
|
|
3322
3322
|
"required": false,
|
|
3323
3323
|
"default": "true",
|
|
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
|
|
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\"`."
|
|
3325
3325
|
},
|
|
3326
3326
|
{
|
|
3327
3327
|
"name": "editFields",
|
|
@@ -3948,7 +3948,7 @@
|
|
|
3948
3948
|
"name": "DXTablePagination",
|
|
3949
3949
|
"category": "extended",
|
|
3950
3950
|
"filePath": "resources/js/components/extended/DXTablePagination.vue",
|
|
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.",
|
|
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`.",
|
|
3952
3952
|
"props": [
|
|
3953
3953
|
{
|
|
3954
3954
|
"name": "pagination",
|
|
@@ -4003,6 +4003,13 @@
|
|
|
4003
4003
|
"type": "boolean",
|
|
4004
4004
|
"required": true,
|
|
4005
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."
|
|
4006
4013
|
}
|
|
4007
4014
|
],
|
|
4008
4015
|
"events": [
|
|
@@ -4024,7 +4031,7 @@
|
|
|
4024
4031
|
"name": "DXTableShell",
|
|
4025
4032
|
"category": "extended",
|
|
4026
4033
|
"filePath": "resources/js/components/extended/DXTableShell.vue",
|
|
4027
|
-
"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
|
|
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).",
|
|
4028
4035
|
"props": [
|
|
4029
4036
|
{
|
|
4030
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-21T12:16
|
|
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
|
@@ -794,8 +794,10 @@ $dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d9770
|
|
|
794
794
|
// same method as the light set (OKLab ΔE ×100, Viénot protan/deutan): min
|
|
795
795
|
// adjacent CVD ΔE 15.1 (light set: 13.8), min normal ΔE 25.3, contrast
|
|
796
796
|
// 5.67–10.23:1 on #212529. Swapping a step needs that validation re-run.
|
|
797
|
-
// getPalette() reads
|
|
798
|
-
//
|
|
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).
|
|
799
801
|
$dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #fbbf24, #f472b6);
|
|
800
802
|
|
|
801
803
|
:root {
|
|
@@ -810,6 +812,18 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
|
|
|
810
812
|
}
|
|
811
813
|
}
|
|
812
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
|
+
|
|
813
827
|
// The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped
|
|
814
828
|
// from the `@omnitend/dashboard-for-laravel/charts` entry, #142). Global rather
|
|
815
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) => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { computed, type ComputedRef } from "vue";
|
|
1
|
+
import { computed, type ComputedRef, type Ref } from "vue";
|
|
2
2
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
|
3
3
|
import {
|
|
4
4
|
baseOptions,
|
|
5
5
|
applyPalette,
|
|
6
6
|
mergeOptions,
|
|
7
|
+
useColorModeVersion,
|
|
7
8
|
type ChartValueFormat,
|
|
8
9
|
} from "./chartTheme";
|
|
9
10
|
|
|
@@ -35,35 +36,52 @@ export interface ThemedChartProps<TType extends "bar" | "line"> {
|
|
|
35
36
|
* data-viz palette to un-coloured datasets and deep-merges the consumer's
|
|
36
37
|
* `options` over the dashboard-friendly defaults. Pass the reactive `props`
|
|
37
38
|
* object (property access stays reactive inside the computeds).
|
|
39
|
+
*
|
|
40
|
+
* `host` is the chart's own container element (a template ref). It is BOTH the
|
|
41
|
+
* `data-bs-theme` scope the palette resolves against and — being a ref read
|
|
42
|
+
* inside the computeds — the trigger that re-themes once it is populated on
|
|
43
|
+
* mount, since the first render happens before a template ref exists (#161).
|
|
38
44
|
*/
|
|
39
45
|
export function useThemedChart<TType extends "bar" | "line">(
|
|
40
46
|
type: TType,
|
|
41
47
|
props: ThemedChartProps<TType>,
|
|
48
|
+
host: Ref<HTMLElement | null>,
|
|
42
49
|
): {
|
|
43
50
|
chartData: ComputedRef<ChartData<TType>>;
|
|
44
51
|
mergedOptions: ComputedRef<ChartOptions<TType>>;
|
|
45
52
|
} {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
datasets: applyPalette(props.datasets, type, props.labels.length),
|
|
51
|
-
}) as ChartData<TType>,
|
|
52
|
-
);
|
|
53
|
+
// Bumped by a MutationObserver on every `data-bs-theme` change; read below
|
|
54
|
+
// so the CSS-variable palette — invisible to Vue's reactivity — is
|
|
55
|
+
// re-resolved when the colour mode flips under a mounted chart.
|
|
56
|
+
const colorModeVersion = useColorModeVersion();
|
|
53
57
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
const chartData = computed(() => {
|
|
59
|
+
void colorModeVersion.value;
|
|
60
|
+
return {
|
|
61
|
+
labels: props.labels,
|
|
62
|
+
datasets: applyPalette(
|
|
63
|
+
props.datasets,
|
|
64
|
+
type,
|
|
65
|
+
props.labels.length,
|
|
66
|
+
host.value,
|
|
67
|
+
),
|
|
68
|
+
} as ChartData<TType>;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const mergedOptions = computed(() => {
|
|
72
|
+
void colorModeVersion.value;
|
|
73
|
+
return mergeOptions(
|
|
74
|
+
baseOptions({
|
|
75
|
+
valueFormat: props.valueFormat,
|
|
76
|
+
currencySymbol: props.currencySymbol,
|
|
77
|
+
locale: props.locale,
|
|
78
|
+
showLegend: props.showLegend ?? props.datasets.length > 1,
|
|
79
|
+
hasValueAxis: true,
|
|
80
|
+
scope: host.value,
|
|
81
|
+
}),
|
|
82
|
+
props.options ?? {},
|
|
83
|
+
) as ChartOptions<TType>;
|
|
84
|
+
});
|
|
67
85
|
|
|
68
86
|
return { chartData, mergedOptions };
|
|
69
87
|
}
|
|
@@ -606,17 +606,16 @@ export interface Props<TItem = any> {
|
|
|
606
606
|
* data-heavy admin index pages that read as busy inside a card.
|
|
607
607
|
*
|
|
608
608
|
* The table renders FLUSH to the card border (rows and stripes reach the
|
|
609
|
-
* edge) while the header and pagination stay padded
|
|
610
|
-
*
|
|
609
|
+
* edge) while the header and pagination stay padded. Only the table region
|
|
610
|
+
* is rounded to follow the card's corners — the card itself does not clip
|
|
611
|
+
* (#166), so a `position: absolute` popover you render into `#header` or
|
|
612
|
+
* any other slot is free to overhang the card.
|
|
611
613
|
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
* `fixed`
|
|
617
|
-
* (a custom popover in a `cell(<key>)` or `#header` slot). Teleport it, or
|
|
618
|
-
* use `:card="false"`. Narrowing the clip to the table region is tracked
|
|
619
|
-
* as #166.
|
|
614
|
+
* The one place content is still clipped is INSIDE the table: Bootstrap's
|
|
615
|
+
* `.table-responsive` is a horizontal scroll container, so an overlay in a
|
|
616
|
+
* `cell(<key>)` slot is bounded by it. Teleport it (bvn's own menus already
|
|
617
|
+
* do — `DDropdown` teleports to `<body>`, the column-filter menus are
|
|
618
|
+
* `fixed`), or pass `:responsive="false"`.
|
|
620
619
|
*/
|
|
621
620
|
card?: boolean;
|
|
622
621
|
|