@wpmoo/ui 1.0.0-rc.5 → 1.0.0-rc.7

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.
Files changed (48) hide show
  1. package/ASSET_LICENSE.md +1 -1
  2. package/README.md +5 -6
  3. package/certification.json +1 -1
  4. package/dist/assets/css/moo-ui.css +7748 -8075
  5. package/dist/assets/css/moo-ui.min.css +2 -2
  6. package/dist/assets/css/moo.css +289 -285
  7. package/dist/assets/css/moo.min.css +2 -2
  8. package/dist/js/chart.js +57 -5
  9. package/dist/js/chart.min.js +3 -3
  10. package/dist/js/combobox.js +1 -1
  11. package/dist/js/context-menu.js +1 -1
  12. package/dist/js/datatable.js +114 -213
  13. package/dist/js/datepicker.js +79 -37
  14. package/dist/js/datepicker.min.js +2 -2
  15. package/dist/js/moo-ui.js +193 -51
  16. package/dist/js/moo-ui.min.js +3 -3
  17. package/dist/js/sidebar.js +145 -170
  18. package/dist/js/slider.js +1 -1
  19. package/package.json +8 -8
  20. package/scss/components/_alert.scss +1 -2
  21. package/scss/components/_card.scss +5 -5
  22. package/scss/components/_close_button.scss +8 -3
  23. package/scss/components/_datatable.scss +27 -3
  24. package/scss/components/_dialog.scss +16 -0
  25. package/scss/components/_dropdown.scss +38 -3
  26. package/scss/components/_switch.scss +13 -5
  27. package/scss/components/_table.scss +6 -0
  28. package/scss/components/_tabs.scss +1 -1
  29. package/scss/components/sidebar/_collapsed.scss +28 -19
  30. package/scss/components/sidebar/_inset.scss +14 -44
  31. package/scss/components/sidebar/_layout.scss +86 -11
  32. package/scss/foundations/_backdrop.scss +128 -0
  33. package/scss/foundations/_scope.scss +5 -0
  34. package/scss/mixins/_banner.scss +3 -0
  35. package/scss/moo-core.scss +6 -34
  36. package/scss/moo-ui.scss +6 -6
  37. package/scss/settings/_bootstrap_overrides.scss +4 -0
  38. package/scss/settings/_component_variables.scss +13 -1
  39. package/scss/settings/_options.scss +2 -0
  40. package/scss/settings/_palette.scss +1 -1
  41. package/scss/themes/_forms.scss +15 -0
  42. package/scss/themes/_root.scss +16 -0
  43. package/scss/themes/_standalone.scss +5 -0
  44. package/scss/themes/{_scoped_core.scss → _theme.scss} +1 -0
  45. package/scss/foundations/_core_state_layer.scss +0 -25
  46. package/scss/foundations/_overlay_backdrop.scss +0 -19
  47. package/scss/themes/_standalone_root.scss +0 -119
  48. /package/scss/foundations/{_core_global_primitives.scss → _globals.scss} +0 -0
@@ -14,7 +14,9 @@
14
14
  border-width: var(--bs-border-width);
15
15
  border-style: var(--bs-border-style);
16
16
  border-color: $moo-close-button-border-color;
17
- border-radius: var(--bs-border-radius-xl);
17
+ // Keep the square hit area visually circular in every context, including
18
+ // modal and offcanvas headers.
19
+ border-radius: var(--bs-border-radius-pill);
18
20
  color: var(--bs-btn-close-color);
19
21
  background-image: none;
20
22
  transition: $btn-transition;
@@ -31,8 +33,11 @@
31
33
 
32
34
  // Bootstrap's dark rule filters the whole element to recolor its background
33
35
  // image. The glyph above uses currentColor through a mask, so filtering would
34
- // incorrectly invert the token-driven hover surface as well.
35
- [data-bs-theme="dark"] .btn-close {
36
+ // incorrectly invert the token-driven hover surface as well. Both resolved
37
+ // owner modes override it so an outer dark owner cannot affect an inner light
38
+ // owner through Bootstrap's global descendant selector.
39
+ :scope[data-bs-theme="light"] .btn-close,
40
+ :scope[data-bs-theme="dark"] .btn-close {
36
41
  filter: none;
37
42
  }
38
43
 
@@ -12,6 +12,7 @@
12
12
  // visible content changed.
13
13
  .datatable {
14
14
  --moo-datatable-min-width: #{$moo-datatable-min-width};
15
+ --moo-datatable-actions-cell-bg: #{$moo-datatable-actions-cell-bg};
15
16
  --moo-datatable-actions-cell-fade-width: #{$moo-datatable-actions-cell-fade-width};
16
17
  --moo-datatable-card-label-width: #{$moo-datatable-card-label-width};
17
18
  --moo-datatable-frame-header-min-height: #{$moo-datatable-frame-header-min-height};
@@ -24,6 +25,10 @@
24
25
  --moo-datatable-bulk-actions-bottom: #{$moo-datatable-bulk-actions-bottom};
25
26
  }
26
27
 
28
+ .card .datatable {
29
+ --moo-datatable-actions-cell-bg: var(--bs-card-bg);
30
+ }
31
+
27
32
  .datatable-table {
28
33
  table-layout: fixed;
29
34
  }
@@ -75,7 +80,7 @@
75
80
  background: linear-gradient(
76
81
  90deg,
77
82
  transparent 0,
78
- var(--bs-body-bg) var(--moo-datatable-actions-cell-fade-width)
83
+ var(--moo-datatable-actions-cell-bg) var(--moo-datatable-actions-cell-fade-width)
79
84
  );
80
85
  }
81
86
 
@@ -84,7 +89,7 @@
84
89
  background: linear-gradient(
85
90
  270deg,
86
91
  transparent 0,
87
- var(--bs-body-bg) var(--moo-datatable-actions-cell-fade-width)
92
+ var(--moo-datatable-actions-cell-bg) var(--moo-datatable-actions-cell-fade-width)
88
93
  );
89
94
  }
90
95
 
@@ -172,6 +177,15 @@
172
177
  width: 100%;
173
178
  }
174
179
 
180
+ // A filter menu is a direct child so Bootstrap can anchor it to the trigger.
181
+ // Clip the joined control while the menu is closed, matching the normal
182
+ // input-group surface; release that clipping only while the menu is open so
183
+ // the positioned dropdown can escape the control without leaking focus or
184
+ // child paint past its rounded border.
185
+ .datatable-searchbar:not(:has(> .dropdown-menu.show)) {
186
+ overflow: hidden;
187
+ }
188
+
175
189
  .datatable-searchbar .datatable-search {
176
190
  flex: 1 1 auto;
177
191
  max-width: none;
@@ -184,7 +198,9 @@
184
198
  position: absolute;
185
199
  inset-block-start: 50%;
186
200
  inset-inline-start: 0.625rem;
187
- z-index: 2;
201
+ // Bootstrap raises a focused form control to z-index: 5. Keep the
202
+ // decorative icon above that opaque surface so focus does not hide it.
203
+ z-index: 6;
188
204
  display: inline-flex;
189
205
  align-items: center;
190
206
  justify-content: center;
@@ -735,6 +751,14 @@
735
751
  background-color: var(--moo-muted-surface);
736
752
  }
737
753
 
754
+ // Sticky action cells paint a gradient instead of using the row's
755
+ // background-color, so selected rows need the same surface at the opaque end
756
+ // of that fade.
757
+ .datatable-row-selected > [data-datatable-column="actions"] {
758
+ --moo-datatable-actions-cell-bg: var(--moo-muted-surface);
759
+ background: var(--moo-muted-surface);
760
+ }
761
+
738
762
  .datatable-card.datatable-row-selected {
739
763
  border-color: var(--moo-border);
740
764
  background-color: var(--moo-muted-surface);
@@ -34,6 +34,22 @@
34
34
  gap: calc(var(--bs-modal-padding) * 0.75);
35
35
  }
36
36
 
37
+ .modal-header:has(> .btn-close) {
38
+ // Keep the dismiss control in a stable top/end corner, like the reference
39
+ // dialog surface, while reserving room so long titles cannot run beneath it.
40
+ position: relative;
41
+ padding-inline-end: calc(
42
+ var(--bs-modal-padding) + #{$btn-close-width} + #{$btn-close-padding-x * 2} + 0.5rem
43
+ );
44
+ }
45
+
46
+ .modal-header:has(> .btn-close) > .btn-close {
47
+ position: absolute;
48
+ inset-block-start: 0.5rem;
49
+ inset-inline-end: 0.5rem;
50
+ margin: 0;
51
+ }
52
+
37
53
  .modal--alert {
38
54
  --bs-modal-width: 24rem;
39
55
  --bs-modal-padding: 1rem;
@@ -188,15 +188,50 @@
188
188
  }
189
189
  }
190
190
 
191
+ // The lg Offcanvas breakpoint still renders the sidebar as a drawer on
192
+ // tablets. Keep identity menus inside that drawer until the desktop fixed
193
+ // overlay contract becomes active.
194
+ @include media-breakpoint-between(sm, lg) {
195
+ [data-slot="sidebar-header"] .dropend > .dropdown-menu,
196
+ [data-slot="sidebar-footer"] .dropend > .dropdown-menu {
197
+ left: 0 !important;
198
+ right: 0 !important;
199
+ min-width: 100%;
200
+ margin: 0;
201
+ transform: none !important;
202
+ }
203
+
204
+ [data-slot="sidebar-header"] .dropend > .dropdown-menu {
205
+ top: calc(100% + #{$spacer * 0.25}) !important;
206
+ bottom: auto !important;
207
+ }
208
+
209
+ [data-slot="sidebar-footer"] .dropend > .dropdown-menu {
210
+ top: auto !important;
211
+ bottom: calc(100% + #{$spacer * 0.25}) !important;
212
+ }
213
+ }
214
+
191
215
  @include media-breakpoint-up(lg) {
192
- [data-slot="sidebar-header"] [data-sidebar-dropdown-positioned] > .dropdown-menu {
216
+ [data-slot="sidebar-header"] [data-sidebar-dropdown-positioned] > .dropdown-menu,
217
+ [data-slot="sidebar-footer"] [data-sidebar-dropdown-positioned] > .dropdown-menu {
193
218
  position: fixed !important;
194
219
  z-index: $zindex-dropdown;
195
- inset-block-start: var(--moo-sidebar-dropdown-block-start) !important;
196
- inset-inline-start: var(--moo-sidebar-dropdown-inline-start) !important;
220
+ left: var(--moo-sidebar-dropdown-left, auto) !important;
221
+ right: var(--moo-sidebar-dropdown-right, auto) !important;
197
222
  width: var(--moo-dropdown-sidebar-min-width);
198
223
  min-width: var(--moo-dropdown-sidebar-min-width);
199
224
  margin: 0;
200
225
  transform: none !important;
201
226
  }
227
+
228
+ [data-slot="sidebar-header"] [data-sidebar-dropdown-positioned] > .dropdown-menu {
229
+ inset-block-start: var(--moo-sidebar-dropdown-block-start) !important;
230
+ }
231
+
232
+ [data-slot="sidebar-footer"] [data-sidebar-dropdown-positioned] > .dropdown-menu {
233
+ inset-block-start: auto !important;
234
+ inset-block-end: var(--moo-sidebar-dropdown-block-end) !important;
235
+ }
236
+
202
237
  }
@@ -6,7 +6,15 @@
6
6
  height: $moo-switch-height;
7
7
  }
8
8
 
9
- :where([data-bs-theme="dark"]) .form-switch .form-check-input:checked {
9
+ :scope[data-bs-theme="light"] .form-switch .form-check-input:checked {
10
+ --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)};
11
+
12
+ background-color: var(--moo-primary);
13
+ border-color: var(--moo-primary);
14
+ filter: none;
15
+ }
16
+
17
+ :scope[data-bs-theme="dark"] .form-switch .form-check-input:checked {
10
18
  --#{$prefix}form-switch-bg: #{escape-svg($moo-switch-checked-bg-image-dark)};
11
19
 
12
20
  background-color: var(--moo-primary);
@@ -14,12 +22,12 @@
14
22
  filter: none;
15
23
  }
16
24
 
17
- .form-switch .form-check-input:focus:active:not(:checked),
18
- .form-switch .form-check-input:focus:not(:focus-visible):not(:checked) {
25
+ :scope[data-bs-theme="light"] .form-switch .form-check-input:focus:active:not(:checked),
26
+ :scope[data-bs-theme="light"] .form-switch .form-check-input:focus:not(:focus-visible):not(:checked) {
19
27
  --#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image)};
20
28
  }
21
29
 
22
- :where([data-bs-theme="dark"]) .form-switch .form-check-input:focus:active:not(:checked),
23
- :where([data-bs-theme="dark"]) .form-switch .form-check-input:focus:not(:focus-visible):not(:checked) {
30
+ :scope[data-bs-theme="dark"] .form-switch .form-check-input:focus:active:not(:checked),
31
+ :scope[data-bs-theme="dark"] .form-switch .form-check-input:focus:not(:focus-visible):not(:checked) {
24
32
  --#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image-dark)};
25
33
  }
@@ -1,3 +1,9 @@
1
+ .card .table {
2
+ // Keep the table surface transparent so a table composed inside a card
3
+ // inherits the card's light/dark surface.
4
+ --bs-table-bg: #{$moo-table-contained-bg};
5
+ }
6
+
1
7
  .table-responsive:not(.scroll-fade-x):has(.table-row-actions > [aria-expanded="true"]) {
2
8
  overflow: visible;
3
9
  }
@@ -21,7 +21,7 @@
21
21
  // minimum for normal text (axe caught this in the certification fixture).
22
22
  // var(--bs-secondary-color) is not a usable substitute here -- Moo UI's
23
23
  // own theme sets --bs-secondary-color: var(--moo-muted-foreground)
24
- // (scss/themes/_standalone_root.scss), so it resolves to the exact same
24
+ // (scss/themes/_root.scss), so it resolves to the exact same
25
25
  // failing color. var(--bs-secondary-text-emphasis) is a different,
26
26
  // untouched Bootstrap token (Moo UI does not override it) built for
27
27
  // accessible colored text against a tinted "subtle" surface -- the same
@@ -12,7 +12,9 @@
12
12
  cursor: pointer;
13
13
  }
14
14
 
15
- [dir="rtl"] .sidebar-rail {
15
+ // Read the Sidebar's computed direction so this remains valid inside the
16
+ // resolved-owner @scope as well as under a full-document html[dir] owner.
17
+ .sidebar:dir(rtl) .sidebar-rail {
16
18
  transform: translateX(-50%);
17
19
  }
18
20
 
@@ -37,14 +39,25 @@
37
39
  position: relative;
38
40
  }
39
41
 
40
- .sidebar-wrapper[data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] {
41
- flex-basis: var(--moo-sidebar-width-icon);
42
- width: var(--moo-sidebar-width-icon);
42
+ .wrapper[data-layout="app"]:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar {
43
+ position: relative;
44
+ // Catalog page headers are sticky at $zindex-fixed. The Sidebar root must
45
+ // sit above that page-level stacking context while an identity menu is
46
+ // open, otherwise the workspace menu is painted underneath the header
47
+ // even though the menu itself is fixed and has dropdown z-index.
48
+ z-index: $zindex-fixed + 1;
43
49
  }
44
50
 
45
- .sidebar-wrapper[data-sidebar-state="collapsed"] .sidebar-inset {
46
- position: relative;
47
- z-index: 0;
51
+ // The sticky inner surface is its own stacking context. Elevate that
52
+ // context while an identity menu is open so the rail's hover indicator
53
+ // remains behind the fixed dropdown instead of painting across it.
54
+ .wrapper[data-layout="app"]:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar > .sidebar-inner {
55
+ z-index: $zindex-fixed + 1;
56
+ }
57
+
58
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] {
59
+ flex-basis: var(--moo-sidebar-width-icon);
60
+ width: var(--moo-sidebar-width-icon);
48
61
  }
49
62
 
50
63
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-group-label,
@@ -62,14 +75,18 @@
62
75
  }
63
76
 
64
77
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"]:has(.sidebar-menu-button[data-bs-toggle="dropdown"][aria-expanded="true"]) {
65
- z-index: $zindex-dropdown;
78
+ // Keep the collapsed identity trigger above sticky page chrome too. This
79
+ // selector is more specific than the app-root rule above and otherwise
80
+ // would lower the Sidebar back to the dropdown layer.
81
+ z-index: $zindex-fixed + 1;
66
82
  }
67
83
 
68
84
  .sidebar-menu-flyout {
69
85
  position: fixed;
70
86
  z-index: $zindex-dropdown;
71
87
  inset-block-start: var(--moo-sidebar-flyout-block-start);
72
- inset-inline-start: var(--moo-sidebar-flyout-inline-start);
88
+ left: var(--moo-sidebar-flyout-left);
89
+ right: auto;
73
90
  display: flex;
74
91
  flex-direction: column;
75
92
  min-width: $spacer * 10;
@@ -162,15 +179,7 @@
162
179
  display: none;
163
180
  }
164
181
 
165
- // Contained demos stay inside their preview instead of becoming viewport drawers.
166
- .sidebar-wrapper--contained {
167
- flex-direction: column;
168
- height: auto;
169
- min-height: 34rem;
170
- overflow: visible;
171
- }
172
-
173
- .sidebar-wrapper--contained .sidebar {
182
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar {
174
183
  position: relative;
175
184
  inset: auto;
176
185
  flex: 0 0 auto;
@@ -180,7 +189,7 @@
180
189
  transform: none;
181
190
  }
182
191
 
183
- .sidebar-wrapper--contained .sidebar-inner {
192
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar-inner {
184
193
  height: auto;
185
194
  max-height: none;
186
195
  }
@@ -1,59 +1,29 @@
1
- // Inset (content area) ---------------------------------------------------
2
- .sidebar-inset {
3
- display: flex;
4
- flex: 1 1 auto;
5
- flex-direction: column;
6
- min-width: 0;
7
- background: var(--bs-body-bg);
8
- }
9
-
10
- .sidebar-inset__header {
11
- position: sticky;
12
- top: 0;
13
- z-index: 20;
14
- display: flex;
15
- align-items: center;
16
- gap: $spacer * 0.5;
17
- height: $spacer * 4;
18
- padding-inline: $spacer;
19
- border-bottom: var(--bs-border-width) solid var(--bs-border-color);
20
- background: var(--bs-body-bg);
21
- }
22
-
23
- .sidebar-inset__content {
24
- flex: 1 1 auto;
25
- min-width: 0;
26
- }
27
-
28
- // Inset variant: the sidebar itself stays flush; the main content becomes a
29
- // rounded, shadowed card with a gap around it, and the page background
30
- // switches to the sidebar's own surface color so the card reads as inset.
31
- // Collapsing to the icon rail restores the inline-start gap. Scoped to
32
- // desktop, matching how the mobile offcanvas drawer ignores variant.
1
+ // Inset Sidebar variant --------------------------------------------------
2
+ // The Sidebar owns the navigation surface; the App root owns the sibling
3
+ // page geometry. This variant only supplies the page-side visual treatment.
33
4
  @include media-breakpoint-up(lg) {
34
- .sidebar-wrapper:has(.sidebar[data-variant="inset"]) {
5
+ .wrapper[data-layout="app"]:has(> .sidebar[data-variant="inset"]) {
35
6
  background: var(--moo-sidebar);
36
7
  }
37
8
 
38
- .sidebar-wrapper:has(.sidebar[data-variant="inset"]) .sidebar-inset {
9
+ .wrapper[data-layout="app"]:has(> .sidebar[data-variant="inset"]) > [data-slot="page"] {
39
10
  margin: $spacer * 0.5;
40
- margin-inline-start: 0;
11
+ margin-left: 0;
41
12
  border-radius: var(--bs-border-radius-xl);
42
13
  box-shadow: var(--bs-box-shadow-sm);
14
+ background: var(--bs-body-bg);
43
15
  }
44
16
 
45
- .sidebar-wrapper[data-sidebar-state="collapsed"]:has(.sidebar[data-variant="inset"]) .sidebar-inset {
46
- margin-inline-start: $spacer * 0.5;
17
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"]:has(> .sidebar[data-variant="inset"]) > [data-slot="page"] {
18
+ margin-left: $spacer * 0.5;
47
19
  }
48
20
 
49
- // A right-side sidebar sits on the opposite edge, so the content card must
50
- // flush against the end side instead of the start side.
51
- .sidebar-wrapper:has(.sidebar[data-variant="inset"][data-side="right"]) .sidebar-inset {
52
- margin-inline-start: $spacer * 0.5;
53
- margin-inline-end: 0;
21
+ .wrapper[data-layout="app"]:has(> .sidebar[data-variant="inset"][data-side="right"]) > [data-slot="page"] {
22
+ margin-left: $spacer * 0.5;
23
+ margin-right: 0;
54
24
  }
55
25
 
56
- .sidebar-wrapper[data-sidebar-state="collapsed"]:has(.sidebar[data-variant="inset"][data-side="right"]) .sidebar-inset {
57
- margin-inline-end: $spacer * 0.5;
26
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"]:has(> .sidebar[data-variant="inset"][data-side="right"]) > [data-slot="page"] {
27
+ margin-right: $spacer * 0.5;
58
28
  }
59
29
  }
@@ -3,15 +3,15 @@
3
3
  // Geometry rides the $moo-sidebar-* Sass knobs; every color consumes a
4
4
  // runtime --moo-* token.
5
5
 
6
- .sidebar-wrapper {
7
- --moo-sidebar-width: #{$moo-sidebar-width};
8
- --moo-sidebar-width-icon: #{$moo-sidebar-width-icon};
6
+ .wrapper[data-layout="app"] {
9
7
  display: flex;
10
8
  width: 100%;
11
- min-height: 100svh;
9
+ height: 100svh;
10
+ min-height: 0;
11
+ overflow: hidden;
12
12
  }
13
13
 
14
- .sidebar-wrapper--contained {
14
+ .wrapper[data-layout="app"][data-shell-mode="contained"] {
15
15
  min-height: 0;
16
16
  height: 34rem;
17
17
  overflow: hidden;
@@ -21,6 +21,8 @@
21
21
 
22
22
  // Sidebar column ---------------------------------------------------------
23
23
  .sidebar {
24
+ --moo-sidebar-width: #{$moo-sidebar-width};
25
+ --moo-sidebar-width-icon: #{$moo-sidebar-width-icon};
24
26
  --bs-offcanvas-width: var(--moo-sidebar-width);
25
27
  flex: 0 0 var(--moo-sidebar-width);
26
28
  width: var(--moo-sidebar-width);
@@ -28,15 +30,28 @@
28
30
  }
29
31
 
30
32
  .sidebar[data-side="left"] .sidebar-inner {
31
- border-inline-end: var(--bs-border-width) solid var(--moo-sidebar-border);
33
+ // `side` is a physical placement contract. Keep the divider on the
34
+ // Sidebar/Page boundary when the document direction is RTL as well.
35
+ border-right: var(--bs-border-width) solid var(--moo-sidebar-border);
32
36
  }
33
37
 
34
38
  .sidebar[data-side="right"] {
35
39
  order: 1;
36
40
  }
37
41
 
42
+ // `side` is a physical placement contract. `:dir()` reads the wrapper's
43
+ // computed direction inside the owner scope, whether it comes from html for a
44
+ // document owner or from an embedded owner's own dir attribute.
45
+ .wrapper[data-layout="app"]:dir(rtl) > .sidebar[data-side="left"] {
46
+ order: 1;
47
+ }
48
+
49
+ .wrapper[data-layout="app"]:dir(rtl) > .sidebar[data-side="right"] {
50
+ order: 0;
51
+ }
52
+
38
53
  .sidebar[data-side="right"] .sidebar-inner {
39
- border-inline-start: var(--bs-border-width) solid var(--moo-sidebar-border);
54
+ border-left: var(--bs-border-width) solid var(--moo-sidebar-border);
40
55
  }
41
56
 
42
57
  // Floating variant: the surface detaches from the viewport edge with a
@@ -58,7 +73,7 @@
58
73
  box-shadow: var(--bs-box-shadow-sm);
59
74
  }
60
75
 
61
- .sidebar-wrapper--contained .sidebar[data-variant="floating"] .sidebar-inner {
76
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar[data-variant="floating"] .sidebar-inner {
62
77
  height: calc(100% - #{$spacer});
63
78
  max-height: calc(100% - #{$spacer});
64
79
  }
@@ -69,16 +84,22 @@
69
84
  // (still a shared token, no new value) and widen the column by the same
70
85
  // margin the card's sides consume, so the icon rail itself keeps its full
71
86
  // width instead of shrinking.
72
- .sidebar-wrapper[data-sidebar-state="collapsed"] .sidebar[data-variant="floating"][data-collapsible="icon"] {
87
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"] .sidebar[data-variant="floating"][data-collapsible="icon"] {
73
88
  flex-basis: calc(var(--moo-sidebar-width-icon) + #{$spacer});
74
89
  width: calc(var(--moo-sidebar-width-icon) + #{$spacer});
75
90
  }
76
91
 
77
- .sidebar-wrapper[data-sidebar-state="collapsed"] .sidebar[data-variant="floating"][data-collapsible="icon"] .sidebar-inner {
92
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"] .sidebar[data-variant="floating"][data-collapsible="icon"] .sidebar-inner {
78
93
  box-shadow: var(--bs-box-shadow);
79
94
  }
80
95
  }
81
96
 
97
+ @media (prefers-reduced-motion: reduce) {
98
+ .sidebar {
99
+ transition: none;
100
+ }
101
+ }
102
+
82
103
  .sidebar-inner {
83
104
  position: sticky;
84
105
  top: 0;
@@ -90,12 +111,66 @@
90
111
  background: var(--moo-sidebar);
91
112
  }
92
113
 
93
- .sidebar-wrapper--contained .sidebar-inner {
114
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar-inner {
94
115
  position: relative;
95
116
  height: 100%;
96
117
  max-height: 100%;
97
118
  }
98
119
 
120
+ .wrapper[data-layout="app"] > [data-slot="page"] {
121
+ display: flex;
122
+ min-width: 0;
123
+ min-height: 0;
124
+ flex: 1 1 auto;
125
+ flex-direction: column;
126
+ overflow-y: auto;
127
+ }
128
+
129
+ .wrapper[data-layout="app"] > [data-slot="page"] > header,
130
+ .wrapper[data-layout="app"] > [data-slot="page"] > main,
131
+ .wrapper[data-layout="app"] > [data-slot="page"] > footer {
132
+ flex: 0 0 auto;
133
+ }
134
+
135
+ // The page main region owns the space between the optional header and footer.
136
+ // Keep the Bootstrap rail as the flex column too, so app previews and real
137
+ // shells can give their caller content a full-height surface without adding
138
+ // layout-specific height values.
139
+ .wrapper[data-layout="app"] > [data-slot="page"] > main {
140
+ display: flex;
141
+ min-height: 0;
142
+ flex: 1 1 auto;
143
+ flex-direction: column;
144
+ }
145
+
146
+ .wrapper[data-layout="app"] > [data-slot="page"] > main > :is(
147
+ .container,
148
+ .container-sm,
149
+ .container-md,
150
+ .container-lg,
151
+ .container-xl,
152
+ .container-xxl,
153
+ .container-fluid
154
+ ) {
155
+ display: flex;
156
+ min-height: 0;
157
+ flex: 1 1 auto;
158
+ flex-direction: column;
159
+ }
160
+
161
+ @include media-breakpoint-down(lg) {
162
+ .wrapper[data-layout="app"][data-shell-mode="contained"] {
163
+ flex-direction: column;
164
+ height: auto;
165
+ min-height: 0;
166
+ overflow: visible;
167
+ }
168
+
169
+ .wrapper[data-layout="app"][data-shell-mode="contained"] > [data-slot="page"] {
170
+ overflow: visible;
171
+ }
172
+ }
173
+
99
174
  .sidebar-header,
100
175
  .sidebar-footer {
101
176
  display: flex;