@omnitend/dashboard-for-laravel 0.35.0 → 0.37.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/base/DButton.vue.d.ts +7 -2
- 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 +28 -11
- package/dist/components/extended/DXTablePagination.vue.d.ts +22 -1
- package/dist/dashboard-for-laravel.js +6704 -6645
- 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 +2 -2
- package/docs/public/api-reference.json +20 -7
- package/docs/public/docs-map.md +1 -1
- package/package.json +1 -1
- package/resources/css/theme.scss +108 -4
- package/resources/js/components/base/DButton.vue +8 -3
- 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 +87 -15
- 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-
|
|
2
|
+
"generated": "2026-07-23T11:32:06.998Z",
|
|
3
3
|
"package": {
|
|
4
4
|
"name": "@omnitend/dashboard-for-laravel",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.37.0"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
8
8
|
"base": [
|
|
@@ -211,8 +211,8 @@
|
|
|
211
211
|
"name": "variant",
|
|
212
212
|
"type": "union",
|
|
213
213
|
"required": false,
|
|
214
|
-
"default": "\"
|
|
215
|
-
"description": "The visual style variant of the button"
|
|
214
|
+
"default": "\"secondary\"",
|
|
215
|
+
"description": "The visual style variant of the button.\n\nDefaults to `'secondary'` (soft, non-emphatic). Under the soft-first\nhouse rule, `primary` is THE single emphatic action per page/modal, so a\nvariant-less button must NOT be emphatic by default — declare\n`variant=\"primary\"` explicitly where you mean the one loud action."
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"name": "size",
|
|
@@ -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",
|
|
@@ -3473,6 +3473,12 @@
|
|
|
3473
3473
|
"required": false,
|
|
3474
3474
|
"default": "false",
|
|
3475
3475
|
"description": "Apply `table-layout: fixed` to the underlying table. Off by default, so\nexisting tables keep their content-sized columns. When on, column widths\nstop depending on cell content, so they no longer reshuffle when a filter\nnarrows the rows — the table stays put as you filter (#156). Combine with\nper-field `width`/`minWidth` to control the proportions; columns with no\ndeclared width share the remaining space equally."
|
|
3476
|
+
},
|
|
3477
|
+
{
|
|
3478
|
+
"name": "primaryKey",
|
|
3479
|
+
"type": "string",
|
|
3480
|
+
"required": false,
|
|
3481
|
+
"description": "Field on each row whose value uniquely identifies it, forwarded to the\ninner bvn table's `primaryKey` (all three data modes). Without it bvn keys\nclient-side rows by their INDEX, which mis-associates per-row STATEFUL cell\ncomponents when a row is inserted/removed mid-interaction — a debounced\nedit in one row can end up saved against the wrong record after a\nconcurrent delete of a row above. Set it (e.g. `primary-key=\"id\"`) so bvn\nkeys rows by that field's value and each row keeps its identity across\ndata mutations. Non-breaking: omit it and rows key by index exactly as\nbefore."
|
|
3476
3482
|
}
|
|
3477
3483
|
],
|
|
3478
3484
|
"events": [
|
|
@@ -3948,7 +3954,7 @@
|
|
|
3948
3954
|
"name": "DXTablePagination",
|
|
3949
3955
|
"category": "extended",
|
|
3950
3956
|
"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.",
|
|
3957
|
+
"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
3958
|
"props": [
|
|
3953
3959
|
{
|
|
3954
3960
|
"name": "pagination",
|
|
@@ -4003,6 +4009,13 @@
|
|
|
4003
4009
|
"type": "boolean",
|
|
4004
4010
|
"required": true,
|
|
4005
4011
|
"description": "Whether a column filter is active — gates the \"Filtered from N\" note."
|
|
4012
|
+
},
|
|
4013
|
+
{
|
|
4014
|
+
"name": "compactThreshold",
|
|
4015
|
+
"type": "number",
|
|
4016
|
+
"required": false,
|
|
4017
|
+
"default": "576",
|
|
4018
|
+
"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
4019
|
}
|
|
4007
4020
|
],
|
|
4008
4021
|
"events": [
|
|
@@ -4024,7 +4037,7 @@
|
|
|
4024
4037
|
"name": "DXTableShell",
|
|
4025
4038
|
"category": "extended",
|
|
4026
4039
|
"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
|
|
4040
|
+
"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
4041
|
"props": [
|
|
4029
4042
|
{
|
|
4030
4043
|
"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-23T11:32:07.049Z
|
|
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
|
@@ -98,6 +98,18 @@ $btn-padding-x-lg: 1.5rem; // 24px
|
|
|
98
98
|
|
|
99
99
|
$input-padding-y: 0.375rem; // compact fields (Bootstrap's default; 0.625rem read oversized)
|
|
100
100
|
$input-padding-x: 1rem;
|
|
101
|
+
|
|
102
|
+
// Match the sm/lg INPUT vertical padding to the sm/lg BUTTON padding above
|
|
103
|
+
// (PO18). Bootstrap sizes every child of an `.input-group-sm`/`-lg` — buttons
|
|
104
|
+
// INCLUDED — with the INPUT's sized padding, so leaving these at Bootstrap's
|
|
105
|
+
// defaults (0.25rem / 0.5rem) made a sized input-group sit ~8px / ~4px shorter
|
|
106
|
+
// than a `.btn-sm`/`.btn-lg` beside it. Aligning them makes sized controls share
|
|
107
|
+
// one height by construction. NB: the BASE `$input-padding-y` (0.375rem) stays
|
|
108
|
+
// deliberately more compact than `$btn-padding-y` (0.625rem) — see the comment
|
|
109
|
+
// above ("0.625rem read oversized"); only the SIZED variants are equalised,
|
|
110
|
+
// because those are the ones whose padding Bootstrap leaks onto grouped buttons.
|
|
111
|
+
$input-padding-y-sm: 0.5rem; // == $btn-padding-y-sm (was Bootstrap's 0.25rem)
|
|
112
|
+
$input-padding-y-lg: 0.75rem; // == $btn-padding-y-lg (was Bootstrap's 0.5rem)
|
|
101
113
|
$input-border-radius: $border-radius;
|
|
102
114
|
$input-border-color: #d1d5db; // Gray-300
|
|
103
115
|
$input-focus-border-color: $primary;
|
|
@@ -652,8 +664,52 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
652
664
|
}
|
|
653
665
|
|
|
654
666
|
// DXTable header titles: muted by default (#157) so the table's CONTENT is
|
|
655
|
-
// the loud layer.
|
|
656
|
-
|
|
667
|
+
// the loud layer. Lightened again for S5b — #157's `--bs-secondary-color`
|
|
668
|
+
// (rgba(33,37,41,.75), ~5.9:1 on white) still read near-black next to legacy
|
|
669
|
+
// omnitend's light-slate headers. `#7c8293` (legacy's `$text-secondary`) is
|
|
670
|
+
// the signed-off value (~3.6:1 on white — fine for the semibold/large header
|
|
671
|
+
// text it colours, below the 4.5:1 body-text bar). Still on the same token so
|
|
672
|
+
// a consumer can re-louden (e.g. `var(--bs-body-color)`).
|
|
673
|
+
--dx-table-header-color: #7c8293;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ----------------------------------------------------------------------------
|
|
677
|
+
// Tables — header alignment + filter-row affordance
|
|
678
|
+
// ----------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
// B17: keep header labels vertically centred regardless of whether a column
|
|
681
|
+
// carries sort icons. Bootstrap sets `.table > thead { vertical-align: bottom }`,
|
|
682
|
+
// which floors a plain (non-sortable) column's label while a sortable
|
|
683
|
+
// neighbour's taller two-arrow sort stack holds ITS label higher — one header
|
|
684
|
+
// row, mixed baselines. Centring the whole thead makes vertical alignment
|
|
685
|
+
// independent of the sort indicator's presence. Scoped to `.table > thead` so a
|
|
686
|
+
// consumer who genuinely wants bottom-aligned headers can still opt back in on a
|
|
687
|
+
// specific table.
|
|
688
|
+
.table > thead {
|
|
689
|
+
vertical-align: middle;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// PL1: a magnifier glyph on DXTable's text/number filter inputs, so the filter
|
|
693
|
+
// row reads as "this searches" at a glance (legacy lt-table carried one).
|
|
694
|
+
//
|
|
695
|
+
// It MUST be a background-image, not a `::before`/`::after` — an `<input>` is a
|
|
696
|
+
// replaced element and cannot host pseudo-elements. The glyph is an inline
|
|
697
|
+
// bootstrap-icons "search" SVG data-URI, tinted `#7c8293` (the same muted slate
|
|
698
|
+
// as the header token above), sized and inset with `padding-left` so text
|
|
699
|
+
// clears it.
|
|
700
|
+
//
|
|
701
|
+
// Direct-child selector (`> th > input.form-control`) is deliberate: DXTable's
|
|
702
|
+
// text/number/date filters render a bare `.form-control` straight inside the
|
|
703
|
+
// `<th>`, whereas the `select` filter's DAutocomplete nests its input inside
|
|
704
|
+
// `.d-autocomplete .input-group .b-autocomplete-input-wrapper` — so it is not a
|
|
705
|
+
// direct child and correctly gets NO glyph (it has its own chevron affordance).
|
|
706
|
+
// `:not([type="date"])` further excludes the date filter (its own picker icon).
|
|
707
|
+
.filter-row > th > input.form-control:not([type="date"]) {
|
|
708
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%237c8293' viewBox='0 0 16 16'%3e%3cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3e%3c/svg%3e");
|
|
709
|
+
background-repeat: no-repeat;
|
|
710
|
+
background-position: left 0.65rem center;
|
|
711
|
+
background-size: 0.85em;
|
|
712
|
+
padding-left: 1.9rem;
|
|
657
713
|
}
|
|
658
714
|
|
|
659
715
|
// primary is near-black navy, so Bootstrap's darken-on-hover is imperceptible —
|
|
@@ -679,6 +735,35 @@ $bootstrap-icons-font-src: url("bootstrap-icons/font/fonts/bootstrap-icons.woff2
|
|
|
679
735
|
}
|
|
680
736
|
}
|
|
681
737
|
|
|
738
|
+
// ----------------------------------------------------------------------------
|
|
739
|
+
// Dropdowns — chevron caret (B14 / PO5)
|
|
740
|
+
// ----------------------------------------------------------------------------
|
|
741
|
+
// Bootstrap's `.dropdown-toggle::after` is a CSS border-triangle that sits high
|
|
742
|
+
// against Poppins' line box and reads crude next to the rest of the theme.
|
|
743
|
+
// Replace it with the bootstrap-icons chevron glyph, optically centred with a
|
|
744
|
+
// little breathing room from the label. The font is already available: the
|
|
745
|
+
// theme `@import`s `bootstrap-icons/font/bootstrap-icons` (declaring the
|
|
746
|
+
// `@font-face`), and #77's post-build extraction ships that woff2 with the
|
|
747
|
+
// stylesheet — so this needs no new dependency and no data-URI. Values signed
|
|
748
|
+
// off after one padding iteration in the greendragon interim; the glyph metrics
|
|
749
|
+
// are tunable via the variables below.
|
|
750
|
+
$dx-dropdown-caret-size: 0.75em; // glyph size relative to the label
|
|
751
|
+
$dx-dropdown-caret-gap: 0.4em; // space between label and chevron
|
|
752
|
+
$dx-dropdown-caret-shift: 0.05em; // vertical nudge for optical centring
|
|
753
|
+
.dropdown-toggle::after {
|
|
754
|
+
border: none; // drop Bootstrap's border-triangle
|
|
755
|
+
font-family: "bootstrap-icons";
|
|
756
|
+
content: "\F282"; // bi-chevron-down
|
|
757
|
+
font-size: $dx-dropdown-caret-size;
|
|
758
|
+
line-height: 1;
|
|
759
|
+
vertical-align: $dx-dropdown-caret-shift;
|
|
760
|
+
margin-left: $dx-dropdown-caret-gap;
|
|
761
|
+
}
|
|
762
|
+
.dropup .dropdown-toggle::after {
|
|
763
|
+
border: none;
|
|
764
|
+
content: "\F286"; // bi-chevron-up
|
|
765
|
+
}
|
|
766
|
+
|
|
682
767
|
// ----------------------------------------------------------------------------
|
|
683
768
|
// Switches — green when ON, red when OFF, by default (#158)
|
|
684
769
|
// ----------------------------------------------------------------------------
|
|
@@ -794,8 +879,10 @@ $dx-chart-palette: (#2563eb, #65a30d, #7c3aed, #0d9488, #ea580c, #0891b2, #d9770
|
|
|
794
879
|
// same method as the light set (OKLab ΔE ×100, Viénot protan/deutan): min
|
|
795
880
|
// adjacent CVD ΔE 15.1 (light set: 13.8), min normal ΔE 25.3, contrast
|
|
796
881
|
// 5.67–10.23:1 on #212529. Swapping a step needs that validation re-run.
|
|
797
|
-
// getPalette() reads
|
|
798
|
-
//
|
|
882
|
+
// getPalette() reads these CSS variables live — off the CHART'S OWN container,
|
|
883
|
+
// so nested colour-mode scopes resolve correctly, and re-read on every
|
|
884
|
+
// `data-bs-theme` change so mounted charts repaint (#161). The TS fallbacks
|
|
885
|
+
// stay the light set (SSR/no-CSS default).
|
|
799
886
|
$dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #fbbf24, #f472b6);
|
|
800
887
|
|
|
801
888
|
:root {
|
|
@@ -810,6 +897,18 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
|
|
|
810
897
|
}
|
|
811
898
|
}
|
|
812
899
|
|
|
900
|
+
// Bootstrap allows `data-bs-theme` on ANY container, so colour-mode scopes
|
|
901
|
+
// nest — and custom properties inherit. Without this re-declaration a LIGHT
|
|
902
|
+
// scope inside a dark root (a light card on a dark page) would inherit the dark
|
|
903
|
+
// palette, because only `:root` publishes the light values and the dark block
|
|
904
|
+
// above overrides it for the whole subtree (#161). Same list as `:root`, so the
|
|
905
|
+
// two can't drift.
|
|
906
|
+
[data-bs-theme="light"] {
|
|
907
|
+
@for $i from 1 through list.length($dx-chart-palette) {
|
|
908
|
+
--dx-chart-#{$i}: #{list.nth($dx-chart-palette, $i)};
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
813
912
|
// The chart container used by DXBarChart/DXLineChart/DXDoughnutChart (shipped
|
|
814
913
|
// from the `@omnitend/dashboard-for-laravel/charts` entry, #142). Global rather
|
|
815
914
|
// than a component scoped style so the charts entry emits NO CSS — chart
|
|
@@ -837,6 +936,11 @@ $dx-chart-palette-dark: (#60a5fa, #a3e635, #a78bfa, #2dd4bf, #fb923c, #22d3ee, #
|
|
|
837
936
|
.b-autocomplete-content {
|
|
838
937
|
min-width: max-content; // grow to the longest option (floors at the trigger width)
|
|
839
938
|
max-width: min(90vw, 32rem); // but don't run away
|
|
939
|
+
// S3b.1: bvn caps this popup at 300px, showing ~7 of a ~30-option list behind
|
|
940
|
+
// an inner scrollbar — it "doesn't drop all the way down the page like a
|
|
941
|
+
// native select". Let it fall most of the way down the viewport (but never
|
|
942
|
+
// taller than 42rem) so the list reads like a real dropdown.
|
|
943
|
+
max-height: min(70vh, 42rem);
|
|
840
944
|
}
|
|
841
945
|
|
|
842
946
|
// ----------------------------------------------------------------------------
|
|
@@ -37,8 +37,13 @@ type LinkVariant =
|
|
|
37
37
|
|
|
38
38
|
interface Props {
|
|
39
39
|
/**
|
|
40
|
-
* The visual style variant of the button
|
|
41
|
-
*
|
|
40
|
+
* The visual style variant of the button.
|
|
41
|
+
*
|
|
42
|
+
* Defaults to `'secondary'` (soft, non-emphatic). Under the soft-first
|
|
43
|
+
* house rule, `primary` is THE single emphatic action per page/modal, so a
|
|
44
|
+
* variant-less button must NOT be emphatic by default — declare
|
|
45
|
+
* `variant="primary"` explicitly where you mean the one loud action.
|
|
46
|
+
* @default 'secondary'
|
|
42
47
|
*/
|
|
43
48
|
variant?: ButtonVariant | LinkVariant | null;
|
|
44
49
|
|
|
@@ -105,7 +110,7 @@ interface Props {
|
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
const props = withDefaults(defineProps<Props>(), {
|
|
108
|
-
variant: "
|
|
113
|
+
variant: "secondary",
|
|
109
114
|
disabled: false,
|
|
110
115
|
loading: false,
|
|
111
116
|
block: false,
|
|
@@ -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) => {
|