@omnitend/dashboard-for-laravel 0.42.2 → 0.44.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/components/base/DFormSelect.vue.d.ts +1 -1
- package/dist/components/extended/DXChoiceField.vue.d.ts +3 -3
- package/dist/components/extended/DXCurrencyInput.vue.d.ts +2 -2
- package/dist/components/extended/DXDashboard.vue.d.ts +14 -6
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +12 -3
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +21 -1
- package/dist/components/extended/DXForm.vue.d.ts +66 -1
- package/dist/components/extended/DXFormErrorSummary.vue.d.ts +70 -0
- package/dist/components/extended/DXFormField.vue.d.ts +3 -0
- package/dist/components/extended/DXStackingTable.vue.d.ts +25 -0
- package/dist/components/extended/DXTable.vue.d.ts +20 -0
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +14 -10
- package/dist/composables/useAsyncOptions.d.ts +14 -1
- package/dist/composables/useForm.d.ts +26 -0
- package/dist/dashboard-for-laravel.js +12002 -11223
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +8 -8
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +12 -0
- package/dist/types/navigation.d.ts +8 -0
- package/dist/utils/formErrorTargets.d.ts +86 -0
- package/dist/utils/pageScrollLock.d.ts +9 -0
- package/docs/public/api-reference.json +223 -9
- package/docs/public/docs-map.md +4 -2
- package/docs/public/llms.txt +4 -2
- package/package.json +9 -8
- package/resources/css/theme.scss +508 -36
- package/resources/js/components/base/DFormSelect.vue +5 -2
- package/resources/js/components/base/DModal.vue +20 -2
- package/resources/js/components/extended/DXChoiceField.vue +42 -26
- package/resources/js/components/extended/DXCurrencyInput.vue +3 -1
- package/resources/js/components/extended/DXDashboard.vue +236 -43
- package/resources/js/components/extended/DXDashboardNavbar.vue +23 -13
- package/resources/js/components/extended/DXDashboardSidebar.vue +103 -29
- package/resources/js/components/extended/DXField.vue +15 -9
- package/resources/js/components/extended/DXForm.vue +676 -28
- package/resources/js/components/extended/DXFormErrorSummary.vue +232 -0
- package/resources/js/components/extended/DXFormField.vue +11 -2
- package/resources/js/components/extended/DXRepeater.vue +30 -4
- package/resources/js/components/extended/DXStackingTable.vue +264 -0
- package/resources/js/components/extended/DXTable.vue +36 -4
- package/resources/js/components/extended/DXTableEditorModal.vue +29 -1
- package/resources/js/composables/useAsyncOptions.ts +30 -5
- package/resources/js/composables/useContainerWidth.ts +12 -3
- package/resources/js/composables/useForm.ts +196 -18
- package/resources/js/composables/useResourceEditor.ts +140 -107
- package/resources/js/index.ts +17 -0
- package/resources/js/types/index.ts +13 -0
- package/resources/js/types/navigation.ts +9 -0
- package/resources/js/utils/formErrorTargets.ts +317 -0
- package/resources/js/utils/pageScrollLock.ts +27 -0
- package/scripts/mcp-server.mjs +58 -6
package/resources/css/theme.scss
CHANGED
|
@@ -338,6 +338,11 @@ $dx-form-label-width: 45%;
|
|
|
338
338
|
|
|
339
339
|
$dashboard-sidebar-width: 280px;
|
|
340
340
|
$dashboard-sidebar-collapsed-width: 80px;
|
|
341
|
+
// The height of BOTH halves of the dashboard's top edge: the sidebar header
|
|
342
|
+
// (brand row) and the navbar. Published as `--dx-dashboard-header-height` on
|
|
343
|
+
// `:root` and read by both, so their bottom edges line up by construction; a
|
|
344
|
+
// consumer retunes the shell's header height by overriding that ONE property
|
|
345
|
+
// (never by sizing the two separately, which is how a stepped corner appears).
|
|
341
346
|
$dashboard-navbar-height: 64px;
|
|
342
347
|
|
|
343
348
|
// The shell's one horizontal gutter: the navbar's inner padding AND the page
|
|
@@ -348,6 +353,10 @@ $dashboard-navbar-height: 64px;
|
|
|
348
353
|
// a consumer can retune both edges together by overriding that.
|
|
349
354
|
$dashboard-gutter-x: 20px;
|
|
350
355
|
|
|
356
|
+
// Below `sm` the gutter tightens to the full-screen modal's (Bootstrap's
|
|
357
|
+
// `$modal-inner-padding`, 1rem), so phone pages and phone modals share an edge.
|
|
358
|
+
$dashboard-gutter-x-phone: 16px;
|
|
359
|
+
|
|
351
360
|
// The bar's vertical padding, and the single-row CONTENT BUDGET that follows
|
|
352
361
|
// from it. Everything on the bar's single row — the avatar toggle, whatever a
|
|
353
362
|
// consumer puts in the `actions` or `user-icon` slots — has to fit this, or the
|
|
@@ -373,10 +382,19 @@ $dashboard-navbar-border-width: 1px; // Bootstrap's $border-width
|
|
|
373
382
|
// runtime is the more correct answer anyway, since the padding then still scales
|
|
374
383
|
// with the root font size.
|
|
375
384
|
$dashboard-navbar-content-height: calc(
|
|
376
|
-
|
|
385
|
+
var(--dx-dashboard-header-height) - var(--dx-dashboard-header-border-width) -
|
|
377
386
|
#{$dashboard-navbar-bar-padding-y} * 2
|
|
378
387
|
);
|
|
379
388
|
|
|
389
|
+
// The narrowest the navbar search region may get on the toggle / search /
|
|
390
|
+
// user-menu row before it wraps onto a row of its own, and the width below
|
|
391
|
+
// which a `<kbd>` shortcut hint inside it (e.g. "⌘K") is hidden to give the
|
|
392
|
+
// input room. The region is a size container named `dx-navbar-search`, so
|
|
393
|
+
// slot content can make its own container queries against it (a shorter
|
|
394
|
+
// placeholder, say).
|
|
395
|
+
$dashboard-navbar-search-min-width: 9rem;
|
|
396
|
+
$dashboard-navbar-search-hint-min-width: 16rem;
|
|
397
|
+
|
|
380
398
|
// ----------------------------------------------------------------------------
|
|
381
399
|
// Import Bootstrap
|
|
382
400
|
// ----------------------------------------------------------------------------
|
|
@@ -636,6 +654,62 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
636
654
|
}
|
|
637
655
|
}
|
|
638
656
|
|
|
657
|
+
// DXFormErrorSummary (#194): a heading (the title, or the server's message
|
|
658
|
+
// when there are no field errors), then one row per validation message. The alert keeps the danger alert's subtle tint; a row
|
|
659
|
+
// that points at a field is a button drawn as underlined text in the alert's
|
|
660
|
+
// own colour (an alert-coloured link, not the brand navy), so the list reads
|
|
661
|
+
// as text and still says "you can click this". Global, not scoped: the rows
|
|
662
|
+
// are rendered inside DAlert's slot.
|
|
663
|
+
.dx-form-error-summary {
|
|
664
|
+
.dx-form-error-summary__message {
|
|
665
|
+
margin-bottom: 0;
|
|
666
|
+
font-weight: $font-weight-semibold;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.dx-form-error-summary__list {
|
|
670
|
+
margin: map.get($spacers, 2) 0 0;
|
|
671
|
+
padding-inline-start: 1.25rem;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// "Other problems": the rows no field owns, after the owned ones. One
|
|
675
|
+
// step lighter than the heading (semibold), still above the body text,
|
|
676
|
+
// and tied to the list it introduces rather than the one above it.
|
|
677
|
+
.dx-form-error-summary__other-title {
|
|
678
|
+
margin: map.get($spacers, 2) 0 0;
|
|
679
|
+
font-weight: $font-weight-medium;
|
|
680
|
+
|
|
681
|
+
+ .dx-form-error-summary__list {
|
|
682
|
+
margin-top: map.get($spacers, 1);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.dx-form-error-summary__target {
|
|
687
|
+
padding: 0;
|
|
688
|
+
border: 0;
|
|
689
|
+
background: none;
|
|
690
|
+
color: inherit;
|
|
691
|
+
font: inherit;
|
|
692
|
+
text-align: start;
|
|
693
|
+
text-decoration: underline;
|
|
694
|
+
text-underline-offset: 0.15em;
|
|
695
|
+
cursor: pointer;
|
|
696
|
+
// A button is an inline-block, and the list bullet sits on the line
|
|
697
|
+
// box's baseline, which a baseline-aligned inline-block takes from its
|
|
698
|
+
// LAST line: a row that wraps had its bullet beside its second line.
|
|
699
|
+
vertical-align: top;
|
|
700
|
+
|
|
701
|
+
&:hover {
|
|
702
|
+
text-decoration-thickness: 2px;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
&:focus-visible {
|
|
706
|
+
outline: var(--bs-focus-ring-width) solid currentColor;
|
|
707
|
+
outline-offset: 2px;
|
|
708
|
+
border-radius: var(--bs-border-radius-sm);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
639
713
|
// Switch-list option rows (DXField `switch-list`) are one line each: a label,
|
|
640
714
|
// a switch and an optional trailing control (a notes input in the
|
|
641
715
|
// `switch-list-item` slot). Centre all three in the row, instead of the
|
|
@@ -722,6 +796,23 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
722
796
|
|
|
723
797
|
:root {
|
|
724
798
|
--dx-dashboard-gutter-x: #{$dashboard-gutter-x};
|
|
799
|
+
--dx-dashboard-header-height: #{$dashboard-navbar-height};
|
|
800
|
+
// The top band (2026-10-06, "A2"): ONE background for the navbar AND the
|
|
801
|
+
// light sidebar's brand row, so the band runs the full width and the two can
|
|
802
|
+
// never differ, and no line under it. The dark sidebar keeps its own navy
|
|
803
|
+
// brand row and the line under both (`.dashboard-layout--sidebar-dark`).
|
|
804
|
+
--dx-dashboard-header-bg: var(--bs-light);
|
|
805
|
+
--dx-dashboard-header-border-width: 0px; // with a unit: it is subtracted in calc()
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.dashboard-layout--sidebar-dark {
|
|
809
|
+
--dx-dashboard-header-border-width: #{$dashboard-navbar-border-width};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
@include media-breakpoint-down(sm) {
|
|
813
|
+
:root {
|
|
814
|
+
--dx-dashboard-gutter-x: #{$dashboard-gutter-x-phone};
|
|
815
|
+
}
|
|
725
816
|
}
|
|
726
817
|
|
|
727
818
|
// Both are the `DContainer fluid` directly inside the navbar header and the
|
|
@@ -736,8 +827,103 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
736
827
|
padding-right: var(--dx-dashboard-gutter-x);
|
|
737
828
|
}
|
|
738
829
|
|
|
830
|
+
// Sidebar colour schemes (DXDashboard `sidebarVariant`, DXDashboardSidebar
|
|
831
|
+
// `variant`). Every colour, the typeface and the weights come from a
|
|
832
|
+
// `--dx-sidebar-*` custom property, so an install can rebrand either scheme in
|
|
833
|
+
// CSS without Sass: override on `:root` for the light default, on
|
|
834
|
+
// `.dashboard-sidebar--dark` for the dark one.
|
|
835
|
+
//
|
|
836
|
+
// LIGHT (the default since 0.43) follows the legacy Omni Tend admin: a white
|
|
837
|
+
// pane with no edge line and no line under the brand row, tinted group-header
|
|
838
|
+
// pills, grey links, a green active
|
|
839
|
+
// item, Poppins (500 links, 600 active and headers). Links are #6b7182
|
|
840
|
+
// (4.87:1 on white); the legacy #7c8293 is 3.84:1 and fails WCAG AA.
|
|
841
|
+
//
|
|
842
|
+
// DARK is the navy rail dfl shipped before, value for value. Its typeface and
|
|
843
|
+
// link size/weight are `initial` (no value), which makes those declarations
|
|
844
|
+
// fall back to inheriting from the page, exactly as before the tokens existed.
|
|
845
|
+
$dx-sidebar-bg: #ffffff !default;
|
|
846
|
+
$dx-sidebar-color: #121419 !default;
|
|
847
|
+
$dx-sidebar-edge-width: 0 !default;
|
|
848
|
+
$dx-sidebar-edge-color: #e6e9f0 !default;
|
|
849
|
+
$dx-sidebar-header-border-width: 0 !default;
|
|
850
|
+
$dx-sidebar-separator-color: #e6e9f0 !default;
|
|
851
|
+
$dx-sidebar-group-color: #121419 !default;
|
|
852
|
+
$dx-sidebar-group-bg: #f5f8fe !default;
|
|
853
|
+
$dx-sidebar-group-hover-bg: #ebf0fb !default;
|
|
854
|
+
$dx-sidebar-link-color: #6b7182 !default;
|
|
855
|
+
$dx-sidebar-link-hover-color: #363e4d !default;
|
|
856
|
+
$dx-sidebar-link-hover-bg: #f9f9fc !default;
|
|
857
|
+
$dx-sidebar-active-color: #121419 !default;
|
|
858
|
+
$dx-sidebar-active-bg: #c3faaa !default;
|
|
859
|
+
$dx-sidebar-focus-ring: #6b7182 !default;
|
|
860
|
+
$dx-sidebar-scrollbar-track: rgba($black, 0.04) !default;
|
|
861
|
+
$dx-sidebar-scrollbar-thumb: rgba($black, 0.18) !default;
|
|
862
|
+
$dx-sidebar-scrollbar-thumb-hover: rgba($black, 0.28) !default;
|
|
863
|
+
$dx-sidebar-font-family: var(--dx-font-family-display) !default;
|
|
864
|
+
$dx-sidebar-link-font-size: 0.9375rem !default;
|
|
865
|
+
$dx-sidebar-link-font-weight: 500 !default;
|
|
866
|
+
$dx-sidebar-active-font-weight: 600 !default;
|
|
867
|
+
$dx-sidebar-group-font-weight: 600 !default;
|
|
868
|
+
|
|
869
|
+
:root {
|
|
870
|
+
--dx-sidebar-bg: #{$dx-sidebar-bg};
|
|
871
|
+
--dx-sidebar-color: #{$dx-sidebar-color};
|
|
872
|
+
--dx-sidebar-edge-width: #{$dx-sidebar-edge-width};
|
|
873
|
+
--dx-sidebar-edge-color: #{$dx-sidebar-edge-color};
|
|
874
|
+
--dx-sidebar-header-border-width: #{$dx-sidebar-header-border-width};
|
|
875
|
+
--dx-sidebar-separator-color: #{$dx-sidebar-separator-color};
|
|
876
|
+
--dx-sidebar-group-color: #{$dx-sidebar-group-color};
|
|
877
|
+
--dx-sidebar-group-bg: #{$dx-sidebar-group-bg};
|
|
878
|
+
--dx-sidebar-group-hover-bg: #{$dx-sidebar-group-hover-bg};
|
|
879
|
+
--dx-sidebar-link-color: #{$dx-sidebar-link-color};
|
|
880
|
+
--dx-sidebar-link-hover-color: #{$dx-sidebar-link-hover-color};
|
|
881
|
+
--dx-sidebar-link-hover-bg: #{$dx-sidebar-link-hover-bg};
|
|
882
|
+
--dx-sidebar-active-color: #{$dx-sidebar-active-color};
|
|
883
|
+
--dx-sidebar-active-bg: #{$dx-sidebar-active-bg};
|
|
884
|
+
--dx-sidebar-focus-ring: #{$dx-sidebar-focus-ring};
|
|
885
|
+
--dx-sidebar-divider-color: rgba(var(--bs-secondary-rgb), 1);
|
|
886
|
+
--dx-sidebar-scrollbar-track: #{$dx-sidebar-scrollbar-track};
|
|
887
|
+
--dx-sidebar-scrollbar-thumb: #{$dx-sidebar-scrollbar-thumb};
|
|
888
|
+
--dx-sidebar-scrollbar-thumb-hover: #{$dx-sidebar-scrollbar-thumb-hover};
|
|
889
|
+
--dx-sidebar-font-family: #{$dx-sidebar-font-family};
|
|
890
|
+
--dx-sidebar-link-font-size: #{$dx-sidebar-link-font-size};
|
|
891
|
+
--dx-sidebar-link-font-weight: #{$dx-sidebar-link-font-weight};
|
|
892
|
+
--dx-sidebar-active-font-weight: #{$dx-sidebar-active-font-weight};
|
|
893
|
+
--dx-sidebar-group-font-weight: #{$dx-sidebar-group-font-weight};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.dashboard-sidebar--dark {
|
|
897
|
+
--dx-sidebar-bg: #{$dark};
|
|
898
|
+
--dx-sidebar-color: #{$white};
|
|
899
|
+
--dx-sidebar-edge-width: 0;
|
|
900
|
+
--dx-sidebar-edge-color: transparent;
|
|
901
|
+
--dx-sidebar-header-border-width: 1px;
|
|
902
|
+
--dx-sidebar-header-bg: transparent;
|
|
903
|
+
--dx-sidebar-separator-color: #{$navbar-dark-toggler-border-color};
|
|
904
|
+
--dx-sidebar-group-color: #{$navbar-dark-color};
|
|
905
|
+
--dx-sidebar-group-bg: transparent;
|
|
906
|
+
--dx-sidebar-group-hover-bg: #{rgba($white, 0.08)};
|
|
907
|
+
--dx-sidebar-link-color: #{$navbar-dark-hover-color};
|
|
908
|
+
--dx-sidebar-link-hover-color: #{$navbar-dark-active-color};
|
|
909
|
+
--dx-sidebar-link-hover-bg: #{rgba($white, 0.1)};
|
|
910
|
+
--dx-sidebar-active-color: #{$navbar-dark-active-color};
|
|
911
|
+
--dx-sidebar-active-bg: #{rgba($white, 0.15)};
|
|
912
|
+
--dx-sidebar-focus-ring: #{rgba($white, 0.5)};
|
|
913
|
+
--dx-sidebar-scrollbar-track: #{rgba($black, 0.1)};
|
|
914
|
+
--dx-sidebar-scrollbar-thumb: #{rgba($white, 0.2)};
|
|
915
|
+
--dx-sidebar-scrollbar-thumb-hover: #{rgba($white, 0.3)};
|
|
916
|
+
--dx-sidebar-font-family: initial;
|
|
917
|
+
--dx-sidebar-link-font-size: initial;
|
|
918
|
+
--dx-sidebar-link-font-weight: initial;
|
|
919
|
+
--dx-sidebar-active-font-weight: 500;
|
|
920
|
+
--dx-sidebar-group-font-weight: 600;
|
|
921
|
+
}
|
|
922
|
+
|
|
739
923
|
.dashboard-sidebar {
|
|
740
|
-
background-color:
|
|
924
|
+
background-color: var(--dx-sidebar-bg);
|
|
925
|
+
color: var(--dx-sidebar-color);
|
|
926
|
+
border-right: var(--dx-sidebar-edge-width) solid var(--dx-sidebar-edge-color);
|
|
741
927
|
width: $dashboard-sidebar-width;
|
|
742
928
|
|
|
743
929
|
&.sidebar-collapsed {
|
|
@@ -745,50 +931,157 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
745
931
|
}
|
|
746
932
|
|
|
747
933
|
.sidebar-header {
|
|
748
|
-
height:
|
|
749
|
-
|
|
934
|
+
height: var(--dx-dashboard-header-height);
|
|
935
|
+
// Light: the navbar's band colour, read from the same token at this
|
|
936
|
+
// element, so a consumer override anywhere above reaches both. Dark sets
|
|
937
|
+
// `--dx-sidebar-header-bg: transparent` (the navy pane shows through).
|
|
938
|
+
background-color: var(--dx-sidebar-header-bg, var(--dx-dashboard-header-bg));
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.sidebar-nav,
|
|
942
|
+
.sidebar-footer {
|
|
943
|
+
font-family: var(--dx-sidebar-font-family);
|
|
750
944
|
}
|
|
751
945
|
|
|
752
946
|
.nav-group-label,
|
|
753
947
|
.nav-group-toggle {
|
|
754
|
-
color:
|
|
948
|
+
color: var(--dx-sidebar-group-color);
|
|
949
|
+
font-weight: var(--dx-sidebar-group-font-weight);
|
|
755
950
|
}
|
|
756
951
|
|
|
757
|
-
.nav-group-
|
|
758
|
-
|
|
952
|
+
.nav-group-toggle {
|
|
953
|
+
background-color: var(--dx-sidebar-group-bg);
|
|
954
|
+
|
|
955
|
+
&:hover {
|
|
956
|
+
background-color: var(--dx-sidebar-group-hover-bg);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
&:focus-visible {
|
|
960
|
+
outline: 2px solid var(--dx-sidebar-focus-ring);
|
|
961
|
+
outline-offset: 2px;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.nav-group-divider hr {
|
|
966
|
+
border-color: var(--dx-sidebar-divider-color);
|
|
759
967
|
}
|
|
760
968
|
|
|
761
969
|
.nav-link {
|
|
762
|
-
color:
|
|
970
|
+
color: var(--dx-sidebar-link-color);
|
|
971
|
+
font-size: var(--dx-sidebar-link-font-size);
|
|
972
|
+
font-weight: var(--dx-sidebar-link-font-weight);
|
|
763
973
|
|
|
764
974
|
&:hover {
|
|
765
|
-
color:
|
|
766
|
-
background-color:
|
|
975
|
+
color: var(--dx-sidebar-link-hover-color);
|
|
976
|
+
background-color: var(--dx-sidebar-link-hover-bg);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Bootstrap's own ring is a primary-tinted box-shadow, which disappears
|
|
980
|
+
// against the dark pane; an outline in the scheme's ring colour shows on
|
|
981
|
+
// both.
|
|
982
|
+
&:focus-visible {
|
|
983
|
+
outline: 2px solid var(--dx-sidebar-focus-ring);
|
|
984
|
+
outline-offset: 2px;
|
|
985
|
+
box-shadow: none;
|
|
767
986
|
}
|
|
768
987
|
|
|
769
988
|
&.active {
|
|
770
|
-
color:
|
|
771
|
-
background-color:
|
|
772
|
-
font-weight:
|
|
989
|
+
color: var(--dx-sidebar-active-color);
|
|
990
|
+
background-color: var(--dx-sidebar-active-bg);
|
|
991
|
+
font-weight: var(--dx-sidebar-active-font-weight);
|
|
773
992
|
}
|
|
774
993
|
}
|
|
775
994
|
}
|
|
776
995
|
|
|
996
|
+
// Light geometry: the group headers are pills (static labels too, so the
|
|
997
|
+
// default non-collapsible sidebar has them) and the links line up with the
|
|
998
|
+
// pill text. `aside` outranks the component's scoped padding and margin (same
|
|
999
|
+
// class count, but those land later in the stylesheet). The collapsed rail
|
|
1000
|
+
// keeps its centred icon padding.
|
|
1001
|
+
//
|
|
1002
|
+
// The gap under a pill is on its first item rather than the pill itself:
|
|
1003
|
+
// inside a closed collapsible group that item is clipped away with the rest,
|
|
1004
|
+
// so closed pills sit exactly the group gap (1rem) apart, as in the legacy
|
|
1005
|
+
// admin, instead of 1rem plus the header's own margin.
|
|
1006
|
+
aside.dashboard-sidebar--light {
|
|
1007
|
+
.nav-group-label,
|
|
1008
|
+
.nav-group-toggle {
|
|
1009
|
+
display: flex;
|
|
1010
|
+
align-items: center;
|
|
1011
|
+
min-height: 2.2rem;
|
|
1012
|
+
margin-bottom: 0;
|
|
1013
|
+
padding-left: 1rem;
|
|
1014
|
+
padding-right: 0.9rem;
|
|
1015
|
+
background-color: var(--dx-sidebar-group-bg);
|
|
1016
|
+
border-radius: $border-radius;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.nav-group-label + .nav-group-items > .nav > .nav-item:first-child,
|
|
1020
|
+
.nav-group-toggle + .nav-group-items > .nav > .nav-item:first-child {
|
|
1021
|
+
margin-top: 0.25rem;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
&:not(.sidebar-collapsed) .nav-link {
|
|
1025
|
+
padding-left: 1rem;
|
|
1026
|
+
border-radius: $border-radius;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Phones (below `sm`, where DModal also goes full screen): the sidebar
|
|
1031
|
+
// DXDashboard owns (`phoneMenu`, class `dashboard-sidebar--phone-menu`) is a
|
|
1032
|
+
// full-screen menu over the page rather than a rail squeezing it, and hidden
|
|
1033
|
+
// until opened. DXDashboard pairs this with its phone behaviour (starts
|
|
1034
|
+
// closed, closes on a link or Escape, holds focus) and puts
|
|
1035
|
+
// `dx-dashboard-menu-open` on <html> while it is open, so the page behind
|
|
1036
|
+
// cannot scroll. A standalone DXDashboardSidebar keeps its rail. The JS reads
|
|
1037
|
+
// the width from `--dx-dashboard-phone-max-width`, published here from
|
|
1038
|
+
// `$grid-breakpoints`, so a custom `sm` moves both together. Below the modal
|
|
1039
|
+
// layer (1055), so a modal opened from the menu still sits on top.
|
|
1040
|
+
:root {
|
|
1041
|
+
--dx-dashboard-phone-max-width: #{breakpoint-max(sm, $grid-breakpoints)};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
@include media-breakpoint-down(sm) {
|
|
1045
|
+
// `aside` outranks the component's scoped sticky/100vh/flex rules (same
|
|
1046
|
+
// class count) whatever order the two land in the stylesheet. The open
|
|
1047
|
+
// class is set only once mounted, so a server-rendered page never paints an
|
|
1048
|
+
// open menu before hydration.
|
|
1049
|
+
aside.dashboard-sidebar--phone-menu:not(.dashboard-sidebar--phone-menu-open) {
|
|
1050
|
+
display: none;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
aside.dashboard-sidebar--phone-menu.dashboard-sidebar--phone-menu-open {
|
|
1054
|
+
position: fixed;
|
|
1055
|
+
inset: 0;
|
|
1056
|
+
z-index: $zindex-offcanvas;
|
|
1057
|
+
width: 100%;
|
|
1058
|
+
height: auto;
|
|
1059
|
+
min-height: 0;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
html.dx-dashboard-menu-open,
|
|
1063
|
+
html.dx-dashboard-menu-open body {
|
|
1064
|
+
overflow: hidden;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
777
1068
|
.dashboard-navbar {
|
|
778
1069
|
// Published so slot content can size itself to the budget rather than guess:
|
|
779
1070
|
// .my-navbar-action { height: var(--dx-navbar-content-height); }
|
|
780
|
-
--dx-navbar-height:
|
|
1071
|
+
--dx-navbar-height: var(--dx-dashboard-header-height);
|
|
781
1072
|
--dx-navbar-content-height: #{$dashboard-navbar-content-height};
|
|
782
1073
|
|
|
783
1074
|
// Floor, not a fixed height: below `md` the search (and actions) drop to
|
|
784
1075
|
// their own rows and the bar must grow to contain them (#93). Single-row
|
|
785
1076
|
// content is shorter than this, so desktop stays exactly this height,
|
|
786
1077
|
// aligned with the sidebar header.
|
|
787
|
-
min-height:
|
|
1078
|
+
min-height: var(--dx-dashboard-header-height);
|
|
788
1079
|
// A light grey top bar over the white content panel (higher contrast than a
|
|
789
|
-
// grey-everywhere layout)
|
|
790
|
-
//
|
|
791
|
-
|
|
1080
|
+
// grey-everywhere layout), from the band token the light sidebar's brand row
|
|
1081
|
+
// shares. The bottom line is a token too (none by default); the markup has no
|
|
1082
|
+
// `border-bottom` utility, whose `!important` would pin it.
|
|
1083
|
+
background-color: var(--dx-dashboard-header-bg);
|
|
1084
|
+
border-bottom: var(--dx-dashboard-header-border-width) var(--bs-border-style) var(--bs-border-color);
|
|
792
1085
|
}
|
|
793
1086
|
|
|
794
1087
|
// The avatar dropdown trigger (DXDashboardNavbar sets this via toggle-class, so
|
|
@@ -1084,6 +1377,173 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
1084
1377
|
vertical-align: middle;
|
|
1085
1378
|
}
|
|
1086
1379
|
|
|
1380
|
+
// ----------------------------------------------------------------------------
|
|
1381
|
+
// DXStackingTable — a hand-built table that becomes one card per body row
|
|
1382
|
+
// ----------------------------------------------------------------------------
|
|
1383
|
+
//
|
|
1384
|
+
// Global, not scoped: the table is the consumer's slot content, which a
|
|
1385
|
+
// component's scoped styles cannot reach. The component toggles
|
|
1386
|
+
// `.dx-stacking-table--stacked` from its MEASURED width and writes each cell's
|
|
1387
|
+
// column label to `data-dx-stack-label`; everything visual is here. Only the
|
|
1388
|
+
// wrapper's DIRECT child table is affected, so a table nested in a cell keeps
|
|
1389
|
+
// its own layout.
|
|
1390
|
+
|
|
1391
|
+
// ----------------------------------------------------------------------------
|
|
1392
|
+
// DXCurrencyInput — keep the amount usable in a narrow container
|
|
1393
|
+
// ----------------------------------------------------------------------------
|
|
1394
|
+
//
|
|
1395
|
+
// Bootstrap gives an input-group's control `min-width: 0`, so a long unit
|
|
1396
|
+
// ("10L Bag in Box") crushed the amount to ~32px. The amount keeps room for a
|
|
1397
|
+
// price, the group still does not wrap (a wrapped affix would land on its own line
|
|
1398
|
+
// with broken corners: the theme already makes every `.input-group` nowrap),
|
|
1399
|
+
// and the affix text wraps onto two lines instead.
|
|
1400
|
+
.dx-currency-input {
|
|
1401
|
+
> .form-control {
|
|
1402
|
+
min-width: 5.5rem;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
> .input-group-text {
|
|
1406
|
+
white-space: normal;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// What a stacked card line stretches: a form control, or an element wrapping
|
|
1411
|
+
// one. Not buttons, which keep their size.
|
|
1412
|
+
$dx-stack-controls: 'input:not([type="hidden"]), select, textarea, .form-control, .form-select, .input-group, [contenteditable]';
|
|
1413
|
+
$dx-stack-fill: ':is(#{$dx-stack-controls}):not(button, .btn), :not(button, .btn):has(#{$dx-stack-controls})';
|
|
1414
|
+
|
|
1415
|
+
// Wide layout: the wrapper is the old `.table-responsive` (a horizontal scroll
|
|
1416
|
+
// container), which also keeps its measured width equal to the space it has.
|
|
1417
|
+
.dx-stacking-table {
|
|
1418
|
+
overflow-x: auto;
|
|
1419
|
+
-webkit-overflow-scrolling: touch;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
.dx-stacking-table--stacked > table {
|
|
1423
|
+
display: block;
|
|
1424
|
+
width: 100%;
|
|
1425
|
+
|
|
1426
|
+
> thead {
|
|
1427
|
+
display: none;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
> tbody,
|
|
1431
|
+
> tfoot,
|
|
1432
|
+
> tbody > tr,
|
|
1433
|
+
> tfoot > tr {
|
|
1434
|
+
display: block;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Each body row is a card.
|
|
1438
|
+
> tbody > tr {
|
|
1439
|
+
border: var(--bs-border-width) solid var(--bs-border-color);
|
|
1440
|
+
border-radius: var(--bs-border-radius);
|
|
1441
|
+
padding: 0.25rem $table-cell-padding-x;
|
|
1442
|
+
margin-bottom: 0.75rem;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// A `.dx-stack-continue` row (a note under a line) joins the card above it.
|
|
1446
|
+
> tbody > tr:has(+ tr.dx-stack-continue) {
|
|
1447
|
+
margin-bottom: 0;
|
|
1448
|
+
border-bottom-width: 0;
|
|
1449
|
+
border-bottom-left-radius: 0;
|
|
1450
|
+
border-bottom-right-radius: 0;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
> tbody > tr.dx-stack-continue {
|
|
1454
|
+
border-top-width: 0;
|
|
1455
|
+
border-top-left-radius: 0;
|
|
1456
|
+
border-top-right-radius: 0;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// Footer rows (a total) are not cards: they line up with the cards' content.
|
|
1460
|
+
// The transparent border matches the cards' box exactly, so the 33% label
|
|
1461
|
+
// column resolves to the same width and the total sits on the same edge.
|
|
1462
|
+
> tfoot > tr {
|
|
1463
|
+
padding: 0 $table-cell-padding-x;
|
|
1464
|
+
border: var(--bs-border-width) solid transparent;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
> tbody > tr > *,
|
|
1468
|
+
> tfoot > tr > * {
|
|
1469
|
+
display: block;
|
|
1470
|
+
width: 100%;
|
|
1471
|
+
padding: 0.5rem 0;
|
|
1472
|
+
border-bottom-width: 0;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// A labelled cell is a row of the card: a fixed label column, then its
|
|
1476
|
+
// content, so every control in a card starts at the same left edge (like a
|
|
1477
|
+
// horizontal form's label column). The column is a third of the card,
|
|
1478
|
+
// clamped so short labels waste no space and long ones wrap instead of
|
|
1479
|
+
// pushing the control. Tune it with `--dx-stacking-table-label-width` on the
|
|
1480
|
+
// wrapper or any ancestor (deliberately NOT declared here, so an ancestor's
|
|
1481
|
+
// value is not shadowed).
|
|
1482
|
+
//
|
|
1483
|
+
// Values start at that edge too (text-align: start, overriding a cell's
|
|
1484
|
+
// `.text-end`/`.text-center`, which only meant something in a column):
|
|
1485
|
+
// a total reads as one more line of the same column, not a stray figure at
|
|
1486
|
+
// the far right.
|
|
1487
|
+
//
|
|
1488
|
+
// `.dx-stack-hide` is excluded HERE rather than out-ranked below: this
|
|
1489
|
+
// selector is (0,2,0) to the hide rule's (0,1,0), so a hidden cell that has a
|
|
1490
|
+
// label would otherwise stay on screen as a flex line.
|
|
1491
|
+
> tbody > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span, .dx-stack-hide),
|
|
1492
|
+
> tfoot > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span, .dx-stack-hide) {
|
|
1493
|
+
display: flex;
|
|
1494
|
+
align-items: center;
|
|
1495
|
+
justify-content: flex-start;
|
|
1496
|
+
gap: 1rem;
|
|
1497
|
+
text-align: start !important;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// A VALUE (text, a currency span) keeps its natural width, so it starts at
|
|
1501
|
+
// the label edge even when it, or something inside it, aligns itself to the
|
|
1502
|
+
// end (`.text-end`, `d-block text-end`): stretched, that end-alignment would
|
|
1503
|
+
// carry it to the card's right edge.
|
|
1504
|
+
> tbody > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > *,
|
|
1505
|
+
> tfoot > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > * {
|
|
1506
|
+
flex: 0 1 auto;
|
|
1507
|
+
min-width: 0;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// A CONTROL (or a wrapper holding one) fills the rest of the line: an input
|
|
1511
|
+
// group or a number input stretches to the card's edge. Buttons keep their
|
|
1512
|
+
// own size. Same control list as the horizontal form's display-only rule.
|
|
1513
|
+
> tbody > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > :is(#{$dx-stack-fill}),
|
|
1514
|
+
> tfoot > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > :is(#{$dx-stack-fill}) {
|
|
1515
|
+
flex: 1 1 0;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
> tbody > tr > [data-dx-stack-label]:not(.dx-stack-span)::before,
|
|
1519
|
+
> tfoot > tr > [data-dx-stack-label]:not(.dx-stack-span)::before {
|
|
1520
|
+
content: attr(data-dx-stack-label);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// A consumer `data-label` wins over the derived one (same specificity, later).
|
|
1524
|
+
> tbody > tr > [data-label]:not(.dx-stack-span)::before,
|
|
1525
|
+
> tfoot > tr > [data-label]:not(.dx-stack-span)::before {
|
|
1526
|
+
content: attr(data-label);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// The label reads as the header it replaces.
|
|
1530
|
+
> tbody > tr > *::before,
|
|
1531
|
+
> tfoot > tr > *::before {
|
|
1532
|
+
flex: 0 0 var(--dx-stacking-table-label-width, clamp(5rem, 33%, 10rem));
|
|
1533
|
+
text-align: start;
|
|
1534
|
+
overflow-wrap: anywhere;
|
|
1535
|
+
font-weight: $table-th-font-weight;
|
|
1536
|
+
color: var(--dx-table-header-color);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Hidden when stacked: opted out, or an empty cell with no label (the
|
|
1540
|
+
// ` ` filler beside a footer total).
|
|
1541
|
+
> tbody > tr > :is(.dx-stack-hide, [data-dx-stack-empty]),
|
|
1542
|
+
> tfoot > tr > :is(.dx-stack-hide, [data-dx-stack-empty]) {
|
|
1543
|
+
display: none;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1087
1547
|
// PL1: a magnifier glyph on DXTable's text/number filter inputs, so the filter
|
|
1088
1548
|
// row reads as "this searches" at a glance (legacy lt-table carried one).
|
|
1089
1549
|
//
|
|
@@ -1463,17 +1923,21 @@ $dx-chart-edge-dark: $body-bg-dark;
|
|
|
1463
1923
|
// utilities and not the media query, leaving a band of widths where the two
|
|
1464
1924
|
// disagree (#101).
|
|
1465
1925
|
//
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
// wraps
|
|
1926
|
+
// At every width the search sits inline between the toggle (and title, from
|
|
1927
|
+
// `md` up) and the user menu, filling the middle: one row on a phone, with the
|
|
1928
|
+
// region never narrower than $dashboard-navbar-search-min-width (below that it
|
|
1929
|
+
// wraps onto a row of its own rather than becoming unusable).
|
|
1470
1930
|
//
|
|
1471
|
-
//
|
|
1472
|
-
// (
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
1475
|
-
|
|
1931
|
+
// Mobile-first: the actions take their own full-width row below that one
|
|
1932
|
+
// (flex-basis 100% forces the wrap; the bar and the sticky header grow to
|
|
1933
|
+
// contain it — #93). The actions row itself wraps, so a wide button group
|
|
1934
|
+
// stacks instead of overflowing the viewport. From `md` up the actions go
|
|
1935
|
+
// inline too, right-aligned next to the user menu.
|
|
1936
|
+
|
|
1937
|
+
// The bar fills the header (less its bottom border), so its single row is
|
|
1938
|
+
// centred in the same height as the sidebar's brand row.
|
|
1476
1939
|
.dashboard-navbar__bar {
|
|
1940
|
+
min-height: calc(var(--dx-dashboard-header-height) - var(--dx-dashboard-header-border-width));
|
|
1477
1941
|
padding: $dashboard-navbar-bar-padding-y 0;
|
|
1478
1942
|
}
|
|
1479
1943
|
|
|
@@ -1481,24 +1945,32 @@ $dx-chart-edge-dark: $body-bg-dark;
|
|
|
1481
1945
|
margin-left: auto;
|
|
1482
1946
|
}
|
|
1483
1947
|
|
|
1484
|
-
.dashboard-
|
|
1948
|
+
.dashboard-navbar__search {
|
|
1949
|
+
order: 1;
|
|
1950
|
+
flex: 1 1 0;
|
|
1951
|
+
min-width: $dashboard-navbar-search-min-width;
|
|
1952
|
+
container: dx-navbar-search / inline-size;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
@container dx-navbar-search (max-width: #{$dashboard-navbar-search-hint-min-width}) {
|
|
1956
|
+
.dashboard-navbar__search kbd {
|
|
1957
|
+
display: none;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
.dashboard-navbar__end {
|
|
1485
1962
|
order: 2;
|
|
1486
|
-
flex: 0 0 100%;
|
|
1487
|
-
flex-wrap: wrap;
|
|
1488
1963
|
}
|
|
1489
1964
|
|
|
1490
|
-
.dashboard-
|
|
1965
|
+
.dashboard-navbar__actions {
|
|
1491
1966
|
order: 3;
|
|
1492
1967
|
flex: 0 0 100%;
|
|
1968
|
+
flex-wrap: wrap;
|
|
1493
1969
|
}
|
|
1494
1970
|
|
|
1495
1971
|
@include media-breakpoint-up(md) {
|
|
1496
|
-
.dashboard-navbar__search {
|
|
1497
|
-
order: 1;
|
|
1498
|
-
flex: 1 1 auto;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
1972
|
.dashboard-navbar__actions {
|
|
1973
|
+
order: 2;
|
|
1502
1974
|
flex: 0 1 auto;
|
|
1503
1975
|
flex-wrap: nowrap;
|
|
1504
1976
|
margin-left: auto;
|
|
@@ -18,8 +18,11 @@ import { BFormSelect } from "bootstrap-vue-next";
|
|
|
18
18
|
import { computed, useSlots } from "vue";
|
|
19
19
|
|
|
20
20
|
// Mirrors the shapes BVN's BFormSelect accepts per option (primitive, object, or
|
|
21
|
-
// group)
|
|
22
|
-
|
|
21
|
+
// group): bvn types them `object | string | number | boolean`. `object`, not
|
|
22
|
+
// `Record<string, unknown>`, so an option INTERFACE such as DXTable's
|
|
23
|
+
// `FilterOption` (no index signature) fits without a cast (#171). Only object
|
|
24
|
+
// options can carry a `null` value.
|
|
25
|
+
export type OptionItem = string | number | boolean | object;
|
|
23
26
|
|
|
24
27
|
export interface Props<TValue = unknown> {
|
|
25
28
|
modelValue?: TValue | TValue[] | null;
|