@omnitend/dashboard-for-laravel 0.42.2 → 0.43.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/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/DXStackingTable.vue.d.ts +25 -0
- package/dist/dashboard-for-laravel.js +9967 -9739
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/style.css +1 -1
- package/dist/types/navigation.d.ts +8 -0
- package/dist/utils/pageScrollLock.d.ts +9 -0
- package/docs/public/api-reference.json +81 -7
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +1 -1
- package/resources/css/theme.scss +452 -36
- package/resources/js/components/base/DModal.vue +20 -2
- 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/DXStackingTable.vue +264 -0
- package/resources/js/composables/useContainerWidth.ts +12 -3
- package/resources/js/index.ts +2 -0
- package/resources/js/types/navigation.ts +9 -0
- package/resources/js/utils/pageScrollLock.ts +27 -0
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
|
// ----------------------------------------------------------------------------
|
|
@@ -722,6 +740,23 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
722
740
|
|
|
723
741
|
:root {
|
|
724
742
|
--dx-dashboard-gutter-x: #{$dashboard-gutter-x};
|
|
743
|
+
--dx-dashboard-header-height: #{$dashboard-navbar-height};
|
|
744
|
+
// The top band (2026-10-06, "A2"): ONE background for the navbar AND the
|
|
745
|
+
// light sidebar's brand row, so the band runs the full width and the two can
|
|
746
|
+
// never differ, and no line under it. The dark sidebar keeps its own navy
|
|
747
|
+
// brand row and the line under both (`.dashboard-layout--sidebar-dark`).
|
|
748
|
+
--dx-dashboard-header-bg: var(--bs-light);
|
|
749
|
+
--dx-dashboard-header-border-width: 0px; // with a unit: it is subtracted in calc()
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.dashboard-layout--sidebar-dark {
|
|
753
|
+
--dx-dashboard-header-border-width: #{$dashboard-navbar-border-width};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
@include media-breakpoint-down(sm) {
|
|
757
|
+
:root {
|
|
758
|
+
--dx-dashboard-gutter-x: #{$dashboard-gutter-x-phone};
|
|
759
|
+
}
|
|
725
760
|
}
|
|
726
761
|
|
|
727
762
|
// Both are the `DContainer fluid` directly inside the navbar header and the
|
|
@@ -736,8 +771,103 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
736
771
|
padding-right: var(--dx-dashboard-gutter-x);
|
|
737
772
|
}
|
|
738
773
|
|
|
774
|
+
// Sidebar colour schemes (DXDashboard `sidebarVariant`, DXDashboardSidebar
|
|
775
|
+
// `variant`). Every colour, the typeface and the weights come from a
|
|
776
|
+
// `--dx-sidebar-*` custom property, so an install can rebrand either scheme in
|
|
777
|
+
// CSS without Sass: override on `:root` for the light default, on
|
|
778
|
+
// `.dashboard-sidebar--dark` for the dark one.
|
|
779
|
+
//
|
|
780
|
+
// LIGHT (the default since 0.43) follows the legacy Omni Tend admin: a white
|
|
781
|
+
// pane with no edge line and no line under the brand row, tinted group-header
|
|
782
|
+
// pills, grey links, a green active
|
|
783
|
+
// item, Poppins (500 links, 600 active and headers). Links are #6b7182
|
|
784
|
+
// (4.87:1 on white); the legacy #7c8293 is 3.84:1 and fails WCAG AA.
|
|
785
|
+
//
|
|
786
|
+
// DARK is the navy rail dfl shipped before, value for value. Its typeface and
|
|
787
|
+
// link size/weight are `initial` (no value), which makes those declarations
|
|
788
|
+
// fall back to inheriting from the page, exactly as before the tokens existed.
|
|
789
|
+
$dx-sidebar-bg: #ffffff !default;
|
|
790
|
+
$dx-sidebar-color: #121419 !default;
|
|
791
|
+
$dx-sidebar-edge-width: 0 !default;
|
|
792
|
+
$dx-sidebar-edge-color: #e6e9f0 !default;
|
|
793
|
+
$dx-sidebar-header-border-width: 0 !default;
|
|
794
|
+
$dx-sidebar-separator-color: #e6e9f0 !default;
|
|
795
|
+
$dx-sidebar-group-color: #121419 !default;
|
|
796
|
+
$dx-sidebar-group-bg: #f5f8fe !default;
|
|
797
|
+
$dx-sidebar-group-hover-bg: #ebf0fb !default;
|
|
798
|
+
$dx-sidebar-link-color: #6b7182 !default;
|
|
799
|
+
$dx-sidebar-link-hover-color: #363e4d !default;
|
|
800
|
+
$dx-sidebar-link-hover-bg: #f9f9fc !default;
|
|
801
|
+
$dx-sidebar-active-color: #121419 !default;
|
|
802
|
+
$dx-sidebar-active-bg: #c3faaa !default;
|
|
803
|
+
$dx-sidebar-focus-ring: #6b7182 !default;
|
|
804
|
+
$dx-sidebar-scrollbar-track: rgba($black, 0.04) !default;
|
|
805
|
+
$dx-sidebar-scrollbar-thumb: rgba($black, 0.18) !default;
|
|
806
|
+
$dx-sidebar-scrollbar-thumb-hover: rgba($black, 0.28) !default;
|
|
807
|
+
$dx-sidebar-font-family: var(--dx-font-family-display) !default;
|
|
808
|
+
$dx-sidebar-link-font-size: 0.9375rem !default;
|
|
809
|
+
$dx-sidebar-link-font-weight: 500 !default;
|
|
810
|
+
$dx-sidebar-active-font-weight: 600 !default;
|
|
811
|
+
$dx-sidebar-group-font-weight: 600 !default;
|
|
812
|
+
|
|
813
|
+
:root {
|
|
814
|
+
--dx-sidebar-bg: #{$dx-sidebar-bg};
|
|
815
|
+
--dx-sidebar-color: #{$dx-sidebar-color};
|
|
816
|
+
--dx-sidebar-edge-width: #{$dx-sidebar-edge-width};
|
|
817
|
+
--dx-sidebar-edge-color: #{$dx-sidebar-edge-color};
|
|
818
|
+
--dx-sidebar-header-border-width: #{$dx-sidebar-header-border-width};
|
|
819
|
+
--dx-sidebar-separator-color: #{$dx-sidebar-separator-color};
|
|
820
|
+
--dx-sidebar-group-color: #{$dx-sidebar-group-color};
|
|
821
|
+
--dx-sidebar-group-bg: #{$dx-sidebar-group-bg};
|
|
822
|
+
--dx-sidebar-group-hover-bg: #{$dx-sidebar-group-hover-bg};
|
|
823
|
+
--dx-sidebar-link-color: #{$dx-sidebar-link-color};
|
|
824
|
+
--dx-sidebar-link-hover-color: #{$dx-sidebar-link-hover-color};
|
|
825
|
+
--dx-sidebar-link-hover-bg: #{$dx-sidebar-link-hover-bg};
|
|
826
|
+
--dx-sidebar-active-color: #{$dx-sidebar-active-color};
|
|
827
|
+
--dx-sidebar-active-bg: #{$dx-sidebar-active-bg};
|
|
828
|
+
--dx-sidebar-focus-ring: #{$dx-sidebar-focus-ring};
|
|
829
|
+
--dx-sidebar-divider-color: rgba(var(--bs-secondary-rgb), 1);
|
|
830
|
+
--dx-sidebar-scrollbar-track: #{$dx-sidebar-scrollbar-track};
|
|
831
|
+
--dx-sidebar-scrollbar-thumb: #{$dx-sidebar-scrollbar-thumb};
|
|
832
|
+
--dx-sidebar-scrollbar-thumb-hover: #{$dx-sidebar-scrollbar-thumb-hover};
|
|
833
|
+
--dx-sidebar-font-family: #{$dx-sidebar-font-family};
|
|
834
|
+
--dx-sidebar-link-font-size: #{$dx-sidebar-link-font-size};
|
|
835
|
+
--dx-sidebar-link-font-weight: #{$dx-sidebar-link-font-weight};
|
|
836
|
+
--dx-sidebar-active-font-weight: #{$dx-sidebar-active-font-weight};
|
|
837
|
+
--dx-sidebar-group-font-weight: #{$dx-sidebar-group-font-weight};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.dashboard-sidebar--dark {
|
|
841
|
+
--dx-sidebar-bg: #{$dark};
|
|
842
|
+
--dx-sidebar-color: #{$white};
|
|
843
|
+
--dx-sidebar-edge-width: 0;
|
|
844
|
+
--dx-sidebar-edge-color: transparent;
|
|
845
|
+
--dx-sidebar-header-border-width: 1px;
|
|
846
|
+
--dx-sidebar-header-bg: transparent;
|
|
847
|
+
--dx-sidebar-separator-color: #{$navbar-dark-toggler-border-color};
|
|
848
|
+
--dx-sidebar-group-color: #{$navbar-dark-color};
|
|
849
|
+
--dx-sidebar-group-bg: transparent;
|
|
850
|
+
--dx-sidebar-group-hover-bg: #{rgba($white, 0.08)};
|
|
851
|
+
--dx-sidebar-link-color: #{$navbar-dark-hover-color};
|
|
852
|
+
--dx-sidebar-link-hover-color: #{$navbar-dark-active-color};
|
|
853
|
+
--dx-sidebar-link-hover-bg: #{rgba($white, 0.1)};
|
|
854
|
+
--dx-sidebar-active-color: #{$navbar-dark-active-color};
|
|
855
|
+
--dx-sidebar-active-bg: #{rgba($white, 0.15)};
|
|
856
|
+
--dx-sidebar-focus-ring: #{rgba($white, 0.5)};
|
|
857
|
+
--dx-sidebar-scrollbar-track: #{rgba($black, 0.1)};
|
|
858
|
+
--dx-sidebar-scrollbar-thumb: #{rgba($white, 0.2)};
|
|
859
|
+
--dx-sidebar-scrollbar-thumb-hover: #{rgba($white, 0.3)};
|
|
860
|
+
--dx-sidebar-font-family: initial;
|
|
861
|
+
--dx-sidebar-link-font-size: initial;
|
|
862
|
+
--dx-sidebar-link-font-weight: initial;
|
|
863
|
+
--dx-sidebar-active-font-weight: 500;
|
|
864
|
+
--dx-sidebar-group-font-weight: 600;
|
|
865
|
+
}
|
|
866
|
+
|
|
739
867
|
.dashboard-sidebar {
|
|
740
|
-
background-color:
|
|
868
|
+
background-color: var(--dx-sidebar-bg);
|
|
869
|
+
color: var(--dx-sidebar-color);
|
|
870
|
+
border-right: var(--dx-sidebar-edge-width) solid var(--dx-sidebar-edge-color);
|
|
741
871
|
width: $dashboard-sidebar-width;
|
|
742
872
|
|
|
743
873
|
&.sidebar-collapsed {
|
|
@@ -745,50 +875,157 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
745
875
|
}
|
|
746
876
|
|
|
747
877
|
.sidebar-header {
|
|
748
|
-
height:
|
|
749
|
-
|
|
878
|
+
height: var(--dx-dashboard-header-height);
|
|
879
|
+
// Light: the navbar's band colour, read from the same token at this
|
|
880
|
+
// element, so a consumer override anywhere above reaches both. Dark sets
|
|
881
|
+
// `--dx-sidebar-header-bg: transparent` (the navy pane shows through).
|
|
882
|
+
background-color: var(--dx-sidebar-header-bg, var(--dx-dashboard-header-bg));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.sidebar-nav,
|
|
886
|
+
.sidebar-footer {
|
|
887
|
+
font-family: var(--dx-sidebar-font-family);
|
|
750
888
|
}
|
|
751
889
|
|
|
752
890
|
.nav-group-label,
|
|
753
891
|
.nav-group-toggle {
|
|
754
|
-
color:
|
|
892
|
+
color: var(--dx-sidebar-group-color);
|
|
893
|
+
font-weight: var(--dx-sidebar-group-font-weight);
|
|
755
894
|
}
|
|
756
895
|
|
|
757
|
-
.nav-group-
|
|
758
|
-
|
|
896
|
+
.nav-group-toggle {
|
|
897
|
+
background-color: var(--dx-sidebar-group-bg);
|
|
898
|
+
|
|
899
|
+
&:hover {
|
|
900
|
+
background-color: var(--dx-sidebar-group-hover-bg);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
&:focus-visible {
|
|
904
|
+
outline: 2px solid var(--dx-sidebar-focus-ring);
|
|
905
|
+
outline-offset: 2px;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.nav-group-divider hr {
|
|
910
|
+
border-color: var(--dx-sidebar-divider-color);
|
|
759
911
|
}
|
|
760
912
|
|
|
761
913
|
.nav-link {
|
|
762
|
-
color:
|
|
914
|
+
color: var(--dx-sidebar-link-color);
|
|
915
|
+
font-size: var(--dx-sidebar-link-font-size);
|
|
916
|
+
font-weight: var(--dx-sidebar-link-font-weight);
|
|
763
917
|
|
|
764
918
|
&:hover {
|
|
765
|
-
color:
|
|
766
|
-
background-color:
|
|
919
|
+
color: var(--dx-sidebar-link-hover-color);
|
|
920
|
+
background-color: var(--dx-sidebar-link-hover-bg);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Bootstrap's own ring is a primary-tinted box-shadow, which disappears
|
|
924
|
+
// against the dark pane; an outline in the scheme's ring colour shows on
|
|
925
|
+
// both.
|
|
926
|
+
&:focus-visible {
|
|
927
|
+
outline: 2px solid var(--dx-sidebar-focus-ring);
|
|
928
|
+
outline-offset: 2px;
|
|
929
|
+
box-shadow: none;
|
|
767
930
|
}
|
|
768
931
|
|
|
769
932
|
&.active {
|
|
770
|
-
color:
|
|
771
|
-
background-color:
|
|
772
|
-
font-weight:
|
|
933
|
+
color: var(--dx-sidebar-active-color);
|
|
934
|
+
background-color: var(--dx-sidebar-active-bg);
|
|
935
|
+
font-weight: var(--dx-sidebar-active-font-weight);
|
|
773
936
|
}
|
|
774
937
|
}
|
|
775
938
|
}
|
|
776
939
|
|
|
940
|
+
// Light geometry: the group headers are pills (static labels too, so the
|
|
941
|
+
// default non-collapsible sidebar has them) and the links line up with the
|
|
942
|
+
// pill text. `aside` outranks the component's scoped padding and margin (same
|
|
943
|
+
// class count, but those land later in the stylesheet). The collapsed rail
|
|
944
|
+
// keeps its centred icon padding.
|
|
945
|
+
//
|
|
946
|
+
// The gap under a pill is on its first item rather than the pill itself:
|
|
947
|
+
// inside a closed collapsible group that item is clipped away with the rest,
|
|
948
|
+
// so closed pills sit exactly the group gap (1rem) apart, as in the legacy
|
|
949
|
+
// admin, instead of 1rem plus the header's own margin.
|
|
950
|
+
aside.dashboard-sidebar--light {
|
|
951
|
+
.nav-group-label,
|
|
952
|
+
.nav-group-toggle {
|
|
953
|
+
display: flex;
|
|
954
|
+
align-items: center;
|
|
955
|
+
min-height: 2.2rem;
|
|
956
|
+
margin-bottom: 0;
|
|
957
|
+
padding-left: 1rem;
|
|
958
|
+
padding-right: 0.9rem;
|
|
959
|
+
background-color: var(--dx-sidebar-group-bg);
|
|
960
|
+
border-radius: $border-radius;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.nav-group-label + .nav-group-items > .nav > .nav-item:first-child,
|
|
964
|
+
.nav-group-toggle + .nav-group-items > .nav > .nav-item:first-child {
|
|
965
|
+
margin-top: 0.25rem;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
&:not(.sidebar-collapsed) .nav-link {
|
|
969
|
+
padding-left: 1rem;
|
|
970
|
+
border-radius: $border-radius;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Phones (below `sm`, where DModal also goes full screen): the sidebar
|
|
975
|
+
// DXDashboard owns (`phoneMenu`, class `dashboard-sidebar--phone-menu`) is a
|
|
976
|
+
// full-screen menu over the page rather than a rail squeezing it, and hidden
|
|
977
|
+
// until opened. DXDashboard pairs this with its phone behaviour (starts
|
|
978
|
+
// closed, closes on a link or Escape, holds focus) and puts
|
|
979
|
+
// `dx-dashboard-menu-open` on <html> while it is open, so the page behind
|
|
980
|
+
// cannot scroll. A standalone DXDashboardSidebar keeps its rail. The JS reads
|
|
981
|
+
// the width from `--dx-dashboard-phone-max-width`, published here from
|
|
982
|
+
// `$grid-breakpoints`, so a custom `sm` moves both together. Below the modal
|
|
983
|
+
// layer (1055), so a modal opened from the menu still sits on top.
|
|
984
|
+
:root {
|
|
985
|
+
--dx-dashboard-phone-max-width: #{breakpoint-max(sm, $grid-breakpoints)};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
@include media-breakpoint-down(sm) {
|
|
989
|
+
// `aside` outranks the component's scoped sticky/100vh/flex rules (same
|
|
990
|
+
// class count) whatever order the two land in the stylesheet. The open
|
|
991
|
+
// class is set only once mounted, so a server-rendered page never paints an
|
|
992
|
+
// open menu before hydration.
|
|
993
|
+
aside.dashboard-sidebar--phone-menu:not(.dashboard-sidebar--phone-menu-open) {
|
|
994
|
+
display: none;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
aside.dashboard-sidebar--phone-menu.dashboard-sidebar--phone-menu-open {
|
|
998
|
+
position: fixed;
|
|
999
|
+
inset: 0;
|
|
1000
|
+
z-index: $zindex-offcanvas;
|
|
1001
|
+
width: 100%;
|
|
1002
|
+
height: auto;
|
|
1003
|
+
min-height: 0;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
html.dx-dashboard-menu-open,
|
|
1007
|
+
html.dx-dashboard-menu-open body {
|
|
1008
|
+
overflow: hidden;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
777
1012
|
.dashboard-navbar {
|
|
778
1013
|
// Published so slot content can size itself to the budget rather than guess:
|
|
779
1014
|
// .my-navbar-action { height: var(--dx-navbar-content-height); }
|
|
780
|
-
--dx-navbar-height:
|
|
1015
|
+
--dx-navbar-height: var(--dx-dashboard-header-height);
|
|
781
1016
|
--dx-navbar-content-height: #{$dashboard-navbar-content-height};
|
|
782
1017
|
|
|
783
1018
|
// Floor, not a fixed height: below `md` the search (and actions) drop to
|
|
784
1019
|
// their own rows and the bar must grow to contain them (#93). Single-row
|
|
785
1020
|
// content is shorter than this, so desktop stays exactly this height,
|
|
786
1021
|
// aligned with the sidebar header.
|
|
787
|
-
min-height:
|
|
1022
|
+
min-height: var(--dx-dashboard-header-height);
|
|
788
1023
|
// A light grey top bar over the white content panel (higher contrast than a
|
|
789
|
-
// grey-everywhere layout)
|
|
790
|
-
//
|
|
791
|
-
|
|
1024
|
+
// grey-everywhere layout), from the band token the light sidebar's brand row
|
|
1025
|
+
// shares. The bottom line is a token too (none by default); the markup has no
|
|
1026
|
+
// `border-bottom` utility, whose `!important` would pin it.
|
|
1027
|
+
background-color: var(--dx-dashboard-header-bg);
|
|
1028
|
+
border-bottom: var(--dx-dashboard-header-border-width) var(--bs-border-style) var(--bs-border-color);
|
|
792
1029
|
}
|
|
793
1030
|
|
|
794
1031
|
// The avatar dropdown trigger (DXDashboardNavbar sets this via toggle-class, so
|
|
@@ -1084,6 +1321,173 @@ $dx-display-only-row: '.dx-form--horizontal .row:has(> .col-form-label):not(:has
|
|
|
1084
1321
|
vertical-align: middle;
|
|
1085
1322
|
}
|
|
1086
1323
|
|
|
1324
|
+
// ----------------------------------------------------------------------------
|
|
1325
|
+
// DXStackingTable — a hand-built table that becomes one card per body row
|
|
1326
|
+
// ----------------------------------------------------------------------------
|
|
1327
|
+
//
|
|
1328
|
+
// Global, not scoped: the table is the consumer's slot content, which a
|
|
1329
|
+
// component's scoped styles cannot reach. The component toggles
|
|
1330
|
+
// `.dx-stacking-table--stacked` from its MEASURED width and writes each cell's
|
|
1331
|
+
// column label to `data-dx-stack-label`; everything visual is here. Only the
|
|
1332
|
+
// wrapper's DIRECT child table is affected, so a table nested in a cell keeps
|
|
1333
|
+
// its own layout.
|
|
1334
|
+
|
|
1335
|
+
// ----------------------------------------------------------------------------
|
|
1336
|
+
// DXCurrencyInput — keep the amount usable in a narrow container
|
|
1337
|
+
// ----------------------------------------------------------------------------
|
|
1338
|
+
//
|
|
1339
|
+
// Bootstrap gives an input-group's control `min-width: 0`, so a long unit
|
|
1340
|
+
// ("10L Bag in Box") crushed the amount to ~32px. The amount keeps room for a
|
|
1341
|
+
// price, the group still does not wrap (a wrapped affix would land on its own line
|
|
1342
|
+
// with broken corners: the theme already makes every `.input-group` nowrap),
|
|
1343
|
+
// and the affix text wraps onto two lines instead.
|
|
1344
|
+
.dx-currency-input {
|
|
1345
|
+
> .form-control {
|
|
1346
|
+
min-width: 5.5rem;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
> .input-group-text {
|
|
1350
|
+
white-space: normal;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// What a stacked card line stretches: a form control, or an element wrapping
|
|
1355
|
+
// one. Not buttons, which keep their size.
|
|
1356
|
+
$dx-stack-controls: 'input:not([type="hidden"]), select, textarea, .form-control, .form-select, .input-group, [contenteditable]';
|
|
1357
|
+
$dx-stack-fill: ':is(#{$dx-stack-controls}):not(button, .btn), :not(button, .btn):has(#{$dx-stack-controls})';
|
|
1358
|
+
|
|
1359
|
+
// Wide layout: the wrapper is the old `.table-responsive` (a horizontal scroll
|
|
1360
|
+
// container), which also keeps its measured width equal to the space it has.
|
|
1361
|
+
.dx-stacking-table {
|
|
1362
|
+
overflow-x: auto;
|
|
1363
|
+
-webkit-overflow-scrolling: touch;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.dx-stacking-table--stacked > table {
|
|
1367
|
+
display: block;
|
|
1368
|
+
width: 100%;
|
|
1369
|
+
|
|
1370
|
+
> thead {
|
|
1371
|
+
display: none;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
> tbody,
|
|
1375
|
+
> tfoot,
|
|
1376
|
+
> tbody > tr,
|
|
1377
|
+
> tfoot > tr {
|
|
1378
|
+
display: block;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Each body row is a card.
|
|
1382
|
+
> tbody > tr {
|
|
1383
|
+
border: var(--bs-border-width) solid var(--bs-border-color);
|
|
1384
|
+
border-radius: var(--bs-border-radius);
|
|
1385
|
+
padding: 0.25rem $table-cell-padding-x;
|
|
1386
|
+
margin-bottom: 0.75rem;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// A `.dx-stack-continue` row (a note under a line) joins the card above it.
|
|
1390
|
+
> tbody > tr:has(+ tr.dx-stack-continue) {
|
|
1391
|
+
margin-bottom: 0;
|
|
1392
|
+
border-bottom-width: 0;
|
|
1393
|
+
border-bottom-left-radius: 0;
|
|
1394
|
+
border-bottom-right-radius: 0;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
> tbody > tr.dx-stack-continue {
|
|
1398
|
+
border-top-width: 0;
|
|
1399
|
+
border-top-left-radius: 0;
|
|
1400
|
+
border-top-right-radius: 0;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Footer rows (a total) are not cards: they line up with the cards' content.
|
|
1404
|
+
// The transparent border matches the cards' box exactly, so the 33% label
|
|
1405
|
+
// column resolves to the same width and the total sits on the same edge.
|
|
1406
|
+
> tfoot > tr {
|
|
1407
|
+
padding: 0 $table-cell-padding-x;
|
|
1408
|
+
border: var(--bs-border-width) solid transparent;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
> tbody > tr > *,
|
|
1412
|
+
> tfoot > tr > * {
|
|
1413
|
+
display: block;
|
|
1414
|
+
width: 100%;
|
|
1415
|
+
padding: 0.5rem 0;
|
|
1416
|
+
border-bottom-width: 0;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// A labelled cell is a row of the card: a fixed label column, then its
|
|
1420
|
+
// content, so every control in a card starts at the same left edge (like a
|
|
1421
|
+
// horizontal form's label column). The column is a third of the card,
|
|
1422
|
+
// clamped so short labels waste no space and long ones wrap instead of
|
|
1423
|
+
// pushing the control. Tune it with `--dx-stacking-table-label-width` on the
|
|
1424
|
+
// wrapper or any ancestor (deliberately NOT declared here, so an ancestor's
|
|
1425
|
+
// value is not shadowed).
|
|
1426
|
+
//
|
|
1427
|
+
// Values start at that edge too (text-align: start, overriding a cell's
|
|
1428
|
+
// `.text-end`/`.text-center`, which only meant something in a column):
|
|
1429
|
+
// a total reads as one more line of the same column, not a stray figure at
|
|
1430
|
+
// the far right.
|
|
1431
|
+
//
|
|
1432
|
+
// `.dx-stack-hide` is excluded HERE rather than out-ranked below: this
|
|
1433
|
+
// selector is (0,2,0) to the hide rule's (0,1,0), so a hidden cell that has a
|
|
1434
|
+
// label would otherwise stay on screen as a flex line.
|
|
1435
|
+
> tbody > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span, .dx-stack-hide),
|
|
1436
|
+
> tfoot > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span, .dx-stack-hide) {
|
|
1437
|
+
display: flex;
|
|
1438
|
+
align-items: center;
|
|
1439
|
+
justify-content: flex-start;
|
|
1440
|
+
gap: 1rem;
|
|
1441
|
+
text-align: start !important;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// A VALUE (text, a currency span) keeps its natural width, so it starts at
|
|
1445
|
+
// the label edge even when it, or something inside it, aligns itself to the
|
|
1446
|
+
// end (`.text-end`, `d-block text-end`): stretched, that end-alignment would
|
|
1447
|
+
// carry it to the card's right edge.
|
|
1448
|
+
> tbody > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > *,
|
|
1449
|
+
> tfoot > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > * {
|
|
1450
|
+
flex: 0 1 auto;
|
|
1451
|
+
min-width: 0;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// A CONTROL (or a wrapper holding one) fills the rest of the line: an input
|
|
1455
|
+
// group or a number input stretches to the card's edge. Buttons keep their
|
|
1456
|
+
// own size. Same control list as the horizontal form's display-only rule.
|
|
1457
|
+
> tbody > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > :is(#{$dx-stack-fill}),
|
|
1458
|
+
> tfoot > tr > :is([data-dx-stack-label], [data-label]):not(.dx-stack-span) > :is(#{$dx-stack-fill}) {
|
|
1459
|
+
flex: 1 1 0;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
> tbody > tr > [data-dx-stack-label]:not(.dx-stack-span)::before,
|
|
1463
|
+
> tfoot > tr > [data-dx-stack-label]:not(.dx-stack-span)::before {
|
|
1464
|
+
content: attr(data-dx-stack-label);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// A consumer `data-label` wins over the derived one (same specificity, later).
|
|
1468
|
+
> tbody > tr > [data-label]:not(.dx-stack-span)::before,
|
|
1469
|
+
> tfoot > tr > [data-label]:not(.dx-stack-span)::before {
|
|
1470
|
+
content: attr(data-label);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// The label reads as the header it replaces.
|
|
1474
|
+
> tbody > tr > *::before,
|
|
1475
|
+
> tfoot > tr > *::before {
|
|
1476
|
+
flex: 0 0 var(--dx-stacking-table-label-width, clamp(5rem, 33%, 10rem));
|
|
1477
|
+
text-align: start;
|
|
1478
|
+
overflow-wrap: anywhere;
|
|
1479
|
+
font-weight: $table-th-font-weight;
|
|
1480
|
+
color: var(--dx-table-header-color);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Hidden when stacked: opted out, or an empty cell with no label (the
|
|
1484
|
+
// ` ` filler beside a footer total).
|
|
1485
|
+
> tbody > tr > :is(.dx-stack-hide, [data-dx-stack-empty]),
|
|
1486
|
+
> tfoot > tr > :is(.dx-stack-hide, [data-dx-stack-empty]) {
|
|
1487
|
+
display: none;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1087
1491
|
// PL1: a magnifier glyph on DXTable's text/number filter inputs, so the filter
|
|
1088
1492
|
// row reads as "this searches" at a glance (legacy lt-table carried one).
|
|
1089
1493
|
//
|
|
@@ -1463,17 +1867,21 @@ $dx-chart-edge-dark: $body-bg-dark;
|
|
|
1463
1867
|
// utilities and not the media query, leaving a band of widths where the two
|
|
1464
1868
|
// disagree (#101).
|
|
1465
1869
|
//
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
// wraps
|
|
1870
|
+
// At every width the search sits inline between the toggle (and title, from
|
|
1871
|
+
// `md` up) and the user menu, filling the middle: one row on a phone, with the
|
|
1872
|
+
// region never narrower than $dashboard-navbar-search-min-width (below that it
|
|
1873
|
+
// wraps onto a row of its own rather than becoming unusable).
|
|
1470
1874
|
//
|
|
1471
|
-
//
|
|
1472
|
-
// (
|
|
1473
|
-
//
|
|
1474
|
-
//
|
|
1475
|
-
|
|
1875
|
+
// Mobile-first: the actions take their own full-width row below that one
|
|
1876
|
+
// (flex-basis 100% forces the wrap; the bar and the sticky header grow to
|
|
1877
|
+
// contain it — #93). The actions row itself wraps, so a wide button group
|
|
1878
|
+
// stacks instead of overflowing the viewport. From `md` up the actions go
|
|
1879
|
+
// inline too, right-aligned next to the user menu.
|
|
1880
|
+
|
|
1881
|
+
// The bar fills the header (less its bottom border), so its single row is
|
|
1882
|
+
// centred in the same height as the sidebar's brand row.
|
|
1476
1883
|
.dashboard-navbar__bar {
|
|
1884
|
+
min-height: calc(var(--dx-dashboard-header-height) - var(--dx-dashboard-header-border-width));
|
|
1477
1885
|
padding: $dashboard-navbar-bar-padding-y 0;
|
|
1478
1886
|
}
|
|
1479
1887
|
|
|
@@ -1481,24 +1889,32 @@ $dx-chart-edge-dark: $body-bg-dark;
|
|
|
1481
1889
|
margin-left: auto;
|
|
1482
1890
|
}
|
|
1483
1891
|
|
|
1484
|
-
.dashboard-
|
|
1892
|
+
.dashboard-navbar__search {
|
|
1893
|
+
order: 1;
|
|
1894
|
+
flex: 1 1 0;
|
|
1895
|
+
min-width: $dashboard-navbar-search-min-width;
|
|
1896
|
+
container: dx-navbar-search / inline-size;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
@container dx-navbar-search (max-width: #{$dashboard-navbar-search-hint-min-width}) {
|
|
1900
|
+
.dashboard-navbar__search kbd {
|
|
1901
|
+
display: none;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
.dashboard-navbar__end {
|
|
1485
1906
|
order: 2;
|
|
1486
|
-
flex: 0 0 100%;
|
|
1487
|
-
flex-wrap: wrap;
|
|
1488
1907
|
}
|
|
1489
1908
|
|
|
1490
|
-
.dashboard-
|
|
1909
|
+
.dashboard-navbar__actions {
|
|
1491
1910
|
order: 3;
|
|
1492
1911
|
flex: 0 0 100%;
|
|
1912
|
+
flex-wrap: wrap;
|
|
1493
1913
|
}
|
|
1494
1914
|
|
|
1495
1915
|
@include media-breakpoint-up(md) {
|
|
1496
|
-
.dashboard-navbar__search {
|
|
1497
|
-
order: 1;
|
|
1498
|
-
flex: 1 1 auto;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
1916
|
.dashboard-navbar__actions {
|
|
1917
|
+
order: 2;
|
|
1502
1918
|
flex: 0 1 auto;
|
|
1503
1919
|
flex-wrap: nowrap;
|
|
1504
1920
|
margin-left: auto;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Wrapper around bootstrap-vue-next's BModal. Two defaults differ from bvn's:
|
|
4
|
+
`size="lg"` (room for horizontal form labels on desktop) and
|
|
5
|
+
`fullscreen="sm"`: below 576px the modal covers the whole screen, with the
|
|
6
|
+
header and footer pinned and the body scrolling between them (Bootstrap's
|
|
7
|
+
`.modal-fullscreen-sm-down`). A `size="sm"` modal (a short confirmation)
|
|
8
|
+
keeps the centred small dialog on phones too. Pass `fullscreen` explicitly to
|
|
9
|
+
override either way, e.g. `:fullscreen="false"` to opt out.
|
|
10
|
+
-->
|
|
1
11
|
<template>
|
|
2
|
-
<BModal size="lg" v-bind="$attrs">
|
|
12
|
+
<BModal size="lg" :fullscreen="defaultFullscreen()" v-bind="$attrs">
|
|
3
13
|
<template #default="slotProps">
|
|
4
14
|
<slot v-bind="slotProps" />
|
|
5
15
|
<DXModalActions v-if="$slots.actions" class="mt-3">
|
|
@@ -13,10 +23,18 @@
|
|
|
13
23
|
</template>
|
|
14
24
|
|
|
15
25
|
<script setup lang="ts">
|
|
16
|
-
import { computed, useSlots } from "vue";
|
|
26
|
+
import { computed, useAttrs, useSlots } from "vue";
|
|
17
27
|
import { BModal } from "bootstrap-vue-next";
|
|
18
28
|
import DXModalActions from "../extended/DXModalActions.vue";
|
|
19
29
|
|
|
20
30
|
const slots = useSlots();
|
|
31
|
+
const attrs = useAttrs();
|
|
21
32
|
const forwardedSlots = computed(() => Object.keys(slots).filter(name => name !== "default" && name !== "actions"));
|
|
33
|
+
|
|
34
|
+
// Full screen below `sm` unless the modal is a small dialog. An explicit
|
|
35
|
+
// `fullscreen` from the consumer wins: it is in $attrs, which also fall
|
|
36
|
+
// through onto the root BModal after its own bindings. A function, not a
|
|
37
|
+
// computed: `useAttrs()` is not reactive, but the template re-runs it on every
|
|
38
|
+
// render, which an attrs change triggers.
|
|
39
|
+
const defaultFullscreen = (): "sm" | undefined => (attrs.size === "sm" ? undefined : "sm");
|
|
22
40
|
</script>
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
it binds to this prop like any other.
|
|
27
27
|
-->
|
|
28
28
|
<template>
|
|
29
|
-
|
|
29
|
+
<!-- `.dx-currency-input` (theme.scss) keeps the amount usable in a narrow
|
|
30
|
+
container: a minimum width, and a long affix wraps instead. -->
|
|
31
|
+
<DInputGroup class="dx-currency-input">
|
|
30
32
|
<template #prepend>
|
|
31
33
|
<span class="input-group-text">{{ currencySymbol }}</span>
|
|
32
34
|
</template>
|