@wpmoo/ui 1.0.0-rc.6 → 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 (46) hide show
  1. package/ASSET_LICENSE.md +1 -1
  2. package/README.md +4 -4
  3. package/certification.json +1 -1
  4. package/dist/assets/css/moo-ui.css +7742 -8126
  5. package/dist/assets/css/moo-ui.min.css +2 -2
  6. package/dist/assets/css/moo.css +242 -290
  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 +168 -51
  16. package/dist/js/moo-ui.min.js +3 -3
  17. package/dist/js/sidebar.js +121 -174
  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/_dialog.scss +16 -0
  24. package/scss/components/_dropdown.scss +2 -7
  25. package/scss/components/_switch.scss +13 -5
  26. package/scss/components/_tabs.scss +1 -1
  27. package/scss/components/sidebar/_collapsed.scss +20 -23
  28. package/scss/components/sidebar/_inset.scss +14 -44
  29. package/scss/components/sidebar/_layout.scss +86 -11
  30. package/scss/foundations/_backdrop.scss +128 -0
  31. package/scss/foundations/_scope.scss +5 -0
  32. package/scss/mixins/_banner.scss +3 -0
  33. package/scss/moo-core.scss +6 -34
  34. package/scss/moo-ui.scss +6 -6
  35. package/scss/settings/_bootstrap_overrides.scss +4 -0
  36. package/scss/settings/_component_variables.scss +6 -1
  37. package/scss/settings/_options.scss +2 -0
  38. package/scss/settings/_palette.scss +1 -1
  39. package/scss/themes/_forms.scss +15 -0
  40. package/scss/themes/_root.scss +16 -0
  41. package/scss/themes/_standalone.scss +5 -0
  42. package/scss/themes/{_scoped_core.scss → _theme.scss} +1 -0
  43. package/scss/foundations/_core_state_layer.scss +0 -25
  44. package/scss/foundations/_overlay_backdrop.scss +0 -19
  45. package/scss/themes/_standalone_root.scss +0 -119
  46. /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
 
@@ -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;
@@ -217,8 +217,8 @@
217
217
  [data-slot="sidebar-footer"] [data-sidebar-dropdown-positioned] > .dropdown-menu {
218
218
  position: fixed !important;
219
219
  z-index: $zindex-dropdown;
220
- inset-inline-start: var(--moo-sidebar-dropdown-inline-start, auto) !important;
221
- inset-inline-end: auto !important;
220
+ left: var(--moo-sidebar-dropdown-left, auto) !important;
221
+ right: var(--moo-sidebar-dropdown-right, auto) !important;
222
222
  width: var(--moo-dropdown-sidebar-min-width);
223
223
  min-width: var(--moo-dropdown-sidebar-min-width);
224
224
  margin: 0;
@@ -234,9 +234,4 @@
234
234
  inset-block-end: var(--moo-sidebar-dropdown-block-end) !important;
235
235
  }
236
236
 
237
- [data-slot="sidebar"][data-side="right"] [data-slot="sidebar-header"] [data-sidebar-dropdown-positioned] > .dropdown-menu,
238
- [data-slot="sidebar"][data-side="right"] [data-slot="sidebar-footer"] [data-sidebar-dropdown-positioned] > .dropdown-menu {
239
- inset-inline-start: auto !important;
240
- inset-inline-end: var(--moo-sidebar-dropdown-inline-end, auto) !important;
241
- }
242
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
  }
@@ -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,28 +39,27 @@
37
39
  position: relative;
38
40
  }
39
41
 
40
- .sidebar-wrapper:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar {
42
+ .wrapper[data-layout="app"]:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar {
41
43
  position: relative;
42
- z-index: $zindex-dropdown;
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
51
  // The sticky inner surface is its own stacking context. Elevate that
46
52
  // context while an identity menu is open so the rail's hover indicator
47
53
  // remains behind the fixed dropdown instead of painting across it.
48
- .sidebar-wrapper:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar > .sidebar-inner {
49
- z-index: $zindex-dropdown;
54
+ .wrapper[data-layout="app"]:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar > .sidebar-inner {
55
+ z-index: $zindex-fixed + 1;
50
56
  }
51
57
 
52
- .sidebar-wrapper[data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] {
58
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] {
53
59
  flex-basis: var(--moo-sidebar-width-icon);
54
60
  width: var(--moo-sidebar-width-icon);
55
61
  }
56
62
 
57
- .sidebar-wrapper[data-sidebar-state="collapsed"] .sidebar-inset {
58
- position: relative;
59
- z-index: 0;
60
- }
61
-
62
63
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-group-label,
63
64
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-menu-button__text,
64
65
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-menu-button__subtitle,
@@ -74,14 +75,18 @@
74
75
  }
75
76
 
76
77
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"]:has(.sidebar-menu-button[data-bs-toggle="dropdown"][aria-expanded="true"]) {
77
- 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;
78
82
  }
79
83
 
80
84
  .sidebar-menu-flyout {
81
85
  position: fixed;
82
86
  z-index: $zindex-dropdown;
83
87
  inset-block-start: var(--moo-sidebar-flyout-block-start);
84
- inset-inline-start: var(--moo-sidebar-flyout-inline-start);
88
+ left: var(--moo-sidebar-flyout-left);
89
+ right: auto;
85
90
  display: flex;
86
91
  flex-direction: column;
87
92
  min-width: $spacer * 10;
@@ -174,15 +179,7 @@
174
179
  display: none;
175
180
  }
176
181
 
177
- // Contained demos stay inside their preview instead of becoming viewport drawers.
178
- .sidebar-wrapper--contained {
179
- flex-direction: column;
180
- height: auto;
181
- min-height: 34rem;
182
- overflow: visible;
183
- }
184
-
185
- .sidebar-wrapper--contained .sidebar {
182
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar {
186
183
  position: relative;
187
184
  inset: auto;
188
185
  flex: 0 0 auto;
@@ -192,7 +189,7 @@
192
189
  transform: none;
193
190
  }
194
191
 
195
- .sidebar-wrapper--contained .sidebar-inner {
192
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar-inner {
196
193
  height: auto;
197
194
  max-height: none;
198
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;
@@ -0,0 +1,128 @@
1
+ // Bootstrap owns the overlay lifecycle for Dialog / Alert Dialog (Modal) and
2
+ // Sheet (Offcanvas). Moo only retunes the native backdrop appearance through a
3
+ // shared token primitive, so every opening surface gets the same treatment
4
+ // without component-specific backdrop selectors.
5
+ //
6
+ // Modal waits for the real `.modal-backdrop` transition before it starts the
7
+ // dialog panel. Keep that lifecycle element visually inert and animate a
8
+ // pseudo-element instead: the panel and visible blur/dim can then start from
9
+ // the same Bootstrap motion token, while the native backdrop is still retained
10
+ // long enough to fade the visual layer out on close. Use keyframe animation
11
+ // rather than a pseudo-element transition: transitionend events from a pseudo
12
+ // element bubble to its originating backdrop and would make Bootstrap wait for
13
+ // the visual layer before showing the panel.
14
+ @mixin moo-overlay-backdrop-appearance {
15
+ --#{$prefix}backdrop-opacity: var(--moo-overlay-backdrop-opacity, #{$moo-overlay-backdrop-opacity});
16
+ opacity: 1;
17
+ color: transparent;
18
+ background-color: transparent;
19
+
20
+ &.fade,
21
+ &.fade:not(.show) {
22
+ opacity: 1;
23
+ }
24
+
25
+ // Bootstrap reads this duration to decide when to reveal a Modal panel.
26
+ // Keep entry instant; the visual transition below belongs to ::before.
27
+ &.show {
28
+ transition: none;
29
+ }
30
+
31
+ // On close, Bootstrap keeps the lifecycle layer in the DOM for this token
32
+ // duration while ::before fades out. Color has no visible effect on the
33
+ // empty backdrop but supplies Bootstrap's native transition lifecycle
34
+ // without multiplying the pseudo-element's visual fade.
35
+ &.fade:not(.show) {
36
+ color: var(--bs-body-color, currentcolor);
37
+ transition: color var(--moo-overlay-motion-duration, #{$moo-overlay-motion-duration}) ease-out;
38
+ }
39
+
40
+ &::before {
41
+ position: absolute;
42
+ inset: 0;
43
+ content: "";
44
+ opacity: 0;
45
+ background-color: var(--moo-overlay-backdrop-bg, #{$moo-overlay-backdrop-bg});
46
+ -webkit-backdrop-filter: blur(0);
47
+ backdrop-filter: blur(0);
48
+ animation: moo-overlay-backdrop-exit var(--moo-overlay-motion-duration, #{$moo-overlay-motion-duration}) ease-out both;
49
+ }
50
+ }
51
+
52
+ @mixin moo-overlay-backdrop-shown {
53
+ // Bootstrap Modal routes opacity through --bs-backdrop-opacity, but
54
+ // Offcanvas 5.3 compiles the same state to a literal .5. Restate the shared
55
+ // token on both native show selectors so Dialog and Sheet dim equally.
56
+ &::before {
57
+ animation: moo-overlay-backdrop-enter var(--moo-overlay-motion-duration, #{$moo-overlay-motion-duration}) ease-out both;
58
+ }
59
+ }
60
+
61
+ @mixin moo-overlay-backdrop-reduced-motion {
62
+ transition: none;
63
+
64
+ &::before {
65
+ animation: none;
66
+ }
67
+ }
68
+
69
+ @keyframes moo-overlay-backdrop-enter {
70
+ from {
71
+ opacity: 0;
72
+ -webkit-backdrop-filter: blur(0);
73
+ backdrop-filter: blur(0);
74
+ }
75
+
76
+ to {
77
+ opacity: var(--moo-overlay-backdrop-opacity, #{$moo-overlay-backdrop-opacity});
78
+ -webkit-backdrop-filter: var(--moo-overlay-backdrop-filter, #{$moo-overlay-backdrop-filter});
79
+ backdrop-filter: var(--moo-overlay-backdrop-filter, #{$moo-overlay-backdrop-filter});
80
+ }
81
+ }
82
+
83
+ @keyframes moo-overlay-backdrop-exit {
84
+ from {
85
+ opacity: var(--moo-overlay-backdrop-opacity, #{$moo-overlay-backdrop-opacity});
86
+ -webkit-backdrop-filter: var(--moo-overlay-backdrop-filter, #{$moo-overlay-backdrop-filter});
87
+ backdrop-filter: var(--moo-overlay-backdrop-filter, #{$moo-overlay-backdrop-filter});
88
+ }
89
+
90
+ to {
91
+ opacity: 0;
92
+ -webkit-backdrop-filter: blur(0);
93
+ backdrop-filter: blur(0);
94
+ }
95
+ }
96
+
97
+ .modal-backdrop,
98
+ .offcanvas-backdrop {
99
+ @include moo-overlay-backdrop-appearance;
100
+ }
101
+
102
+ .modal-backdrop.show,
103
+ .offcanvas-backdrop.show {
104
+ @include moo-overlay-backdrop-shown;
105
+ }
106
+
107
+ // Backdrops that are portaled into a resolved owner still need to outrank
108
+ // Bootstrap's scoped `.fade` and `.show` rules. Entry points include this
109
+ // mixin inside their single native Moo scope; the global rules above remain
110
+ // available for hosts that intentionally keep Bootstrap's body-level portal.
111
+ @mixin moo-overlay-backdrop-scoped {
112
+ .modal-backdrop,
113
+ .offcanvas-backdrop {
114
+ @include moo-overlay-backdrop-appearance;
115
+ }
116
+
117
+ .modal-backdrop.show,
118
+ .offcanvas-backdrop.show {
119
+ @include moo-overlay-backdrop-shown;
120
+ }
121
+ }
122
+
123
+ @media (prefers-reduced-motion: reduce) {
124
+ .modal-backdrop,
125
+ .offcanvas-backdrop {
126
+ @include moo-overlay-backdrop-reduced-motion;
127
+ }
128
+ }
@@ -0,0 +1,5 @@
1
+ @scope (.moo-ui) to (:where(.moo-ui[data-bs-theme="light"], .moo-ui[data-bs-theme="dark"])) {
2
+ @import "../components";
3
+ @import "../themes/forms";
4
+ @include moo-overlay-backdrop-scoped;
5
+ }
@@ -0,0 +1,3 @@
1
+ @import "bootstrap/scss/mixins/banner";
2
+
3
+ @include bsBanner("");
@@ -4,9 +4,7 @@
4
4
  * Includes Bootstrap 5.3.3 component output
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
7
- $enable-dark-mode: false;
8
- $moo-spinner-animation-name: moo-spinner-rotate;
9
-
7
+ @import "settings/options";
10
8
  @import "bootstrap/scss/functions";
11
9
  @import "settings";
12
10
  @import "bootstrap/scss/variables";
@@ -14,34 +12,8 @@ $moo-spinner-animation-name: moo-spinner-rotate;
14
12
  @import "bootstrap/scss/maps";
15
13
  @import "bootstrap/scss/mixins";
16
14
  @import "bootstrap/scss/utilities";
17
- @import "themes/scoped_core";
18
- @import "foundations/core_global_primitives";
19
-
20
- .moo-ui {
21
- @include moo-core-scales;
22
- @include moo-core-shared;
23
- @include moo-core-light;
24
- }
25
-
26
- :where(html, body)[data-bs-theme="dark"] .moo-ui:not([data-bs-theme]) {
27
- @include moo-core-dark;
28
- }
29
-
30
- .moo-ui[data-bs-theme="light"] {
31
- @include moo-core-light;
32
- }
33
-
34
- .moo-ui[data-bs-theme="dark"] {
35
- @include moo-core-dark;
36
- }
37
-
38
- .moo-ui[dir="rtl"] {
39
- direction: rtl;
40
- }
41
-
42
- @scope (.moo-ui) {
43
- @import "components";
44
- }
45
-
46
- @import "foundations/core_state_layer";
47
- @import "foundations/overlay_backdrop";
15
+ @import "themes/theme";
16
+ @import "foundations/globals";
17
+ @import "foundations/backdrop";
18
+ @import "themes/root";
19
+ @import "foundations/scope";
package/scss/moo-ui.scss CHANGED
@@ -1,7 +1,5 @@
1
1
  @import "settings";
2
-
3
- @import "bootstrap/scss/mixins/banner";
4
- @include bsBanner("");
2
+ @import "mixins/banner";
5
3
 
6
4
  // Bootstrap configuration
7
5
  @import "bootstrap/scss/functions";
@@ -10,10 +8,12 @@
10
8
  @import "bootstrap/scss/maps";
11
9
  @import "bootstrap/scss/mixins";
12
10
  @import "bootstrap/scss/utilities";
11
+ @import "themes/theme";
13
12
 
14
13
  // Bootstrap layout and standalone-only components
15
14
  @import "bootstrap/scss/root";
16
- @import "themes/standalone_root";
15
+ @import "themes/root";
16
+ @import "themes/standalone";
17
17
  @import "bootstrap/scss/reboot";
18
18
  @import "bootstrap/scss/type";
19
19
  @import "bootstrap/scss/images";
@@ -46,5 +46,5 @@
46
46
  @import "bootstrap/scss/utilities/api";
47
47
 
48
48
  @import "utilities/scroll_fade_primitives";
49
- @import "components";
50
- @import "foundations/overlay_backdrop";
49
+ @import "foundations/backdrop";
50
+ @import "foundations/scope";