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

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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI sidebar.js v1.0.0-rc.7 (https://ui.wpmoo.org/)
2
+ * Moo UI sidebar.js v1.0.0-rc.8 (https://ui.wpmoo.org/)
3
3
  * Copyright 2026 WPMoo (https://wpmoo.org)
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -115,7 +115,7 @@ var _Sidebar = class _Sidebar {
115
115
  return this._window.matchMedia(this._config.breakpoint).matches;
116
116
  }
117
117
  _isCollapsed() {
118
- return this._isDesktop() && this._element.dataset.sidebarState === "collapsed";
118
+ return this._isDesktop() && this._sidebar?.dataset?.collapsible === "icon" && this._element.dataset.sidebarState === "collapsed";
119
119
  }
120
120
  _trigger(name, detail = {}) {
121
121
  return this._element.dispatchEvent(
package/dist/js/slider.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI slider.js v1.0.0-rc.7 (https://ui.wpmoo.org/)
2
+ * Moo UI slider.js v1.0.0-rc.8 (https://ui.wpmoo.org/)
3
3
  * Copyright 2026 WPMoo (https://wpmoo.org)
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -0,0 +1,91 @@
1
+ /*!
2
+ * Moo UI theme prepaint
3
+ * Copyright 2026 WPMoo (https://wpmoo.org)
4
+ * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
+ */
6
+ (() => {
7
+ const ownerDocument =
8
+ typeof document === "undefined"
9
+ ? null
10
+ : document;
11
+ const owner = ownerDocument?.currentScript?.parentElement;
12
+ const ownerSelector =
13
+ '.moo-ui[data-bs-theme="light"], .moo-ui[data-bs-theme="dark"]';
14
+
15
+ if (!owner?.matches?.(ownerSelector)) return;
16
+
17
+ const body = ownerDocument?.body;
18
+ const documentOwner =
19
+ owner.dataset?.mooDocumentOwner === "true" &&
20
+ owner.parentElement === body &&
21
+ owner === body?.firstElementChild;
22
+ const view =
23
+ ownerDocument?.defaultView ||
24
+ (typeof window === "undefined" ? null : window);
25
+ const normalizeDirection = (value) =>
26
+ value === "ltr" || value === "rtl" ? value : null;
27
+ const ownerDirection = normalizeDirection(
28
+ owner.getAttribute?.("dir") || owner.dir,
29
+ );
30
+ const documentDirection = normalizeDirection(
31
+ ownerDocument?.documentElement?.getAttribute?.("dir") ||
32
+ ownerDocument?.documentElement?.dir,
33
+ );
34
+ owner.__mooPrepaintBaseline = {
35
+ theme: owner.dataset.bsTheme === "dark" ? "dark" : "light",
36
+ direction: documentOwner ? documentDirection || "ltr" : ownerDirection,
37
+ };
38
+
39
+ const readPreference = (axis, allowed) => {
40
+ const dataKey = axis === "theme" ? "mooThemeKey" : "mooDirectionKey";
41
+ const explicit = owner.dataset?.[dataKey]?.trim();
42
+ const key = explicit || (documentOwner ? `moo:${axis}` : null);
43
+ if (!key) return null;
44
+
45
+ try {
46
+ const value = view?.localStorage?.getItem(key);
47
+ return allowed.includes(value) ? value : null;
48
+ } catch (_) {
49
+ return null;
50
+ }
51
+ };
52
+
53
+ const systemPrefersDark = () => {
54
+ try {
55
+ const matchMedia = view?.matchMedia;
56
+ if (typeof matchMedia !== "function") return null;
57
+ return Boolean(
58
+ matchMedia.call(view, "(prefers-color-scheme: dark)")?.matches,
59
+ );
60
+ } catch (_) {
61
+ return null;
62
+ }
63
+ };
64
+
65
+ try {
66
+ const storedTheme = readPreference("theme", ["light", "dark", "system"]);
67
+ const mediaPrefersDark =
68
+ storedTheme === "system" ? systemPrefersDark() : null;
69
+ const resolvedTheme =
70
+ storedTheme === "dark"
71
+ ? "dark"
72
+ : storedTheme === "light"
73
+ ? "light"
74
+ : storedTheme === "system" && mediaPrefersDark !== null
75
+ ? mediaPrefersDark
76
+ ? "dark"
77
+ : "light"
78
+ : owner.dataset.bsTheme === "dark"
79
+ ? "dark"
80
+ : "light";
81
+ owner.dataset.bsTheme = resolvedTheme;
82
+
83
+ const storedDirection = readPreference("direction", ["ltr", "rtl"]);
84
+ if (storedDirection) {
85
+ if (documentOwner) ownerDocument.documentElement.dir = storedDirection;
86
+ else owner.dir = storedDirection;
87
+ }
88
+ } finally {
89
+ owner.dataset.mooPrepaint = "ready";
90
+ }
91
+ })();
@@ -0,0 +1,24 @@
1
+ {
2
+ "artifacts": [
3
+ {
4
+ "export": "./moo.css",
5
+ "path": "dist/assets/css/moo.css",
6
+ "sha256": "29c92bd75db1e4990745796aeef4d6a0b3313d1b9d2e3c929f559f8687a5fe6f"
7
+ },
8
+ {
9
+ "export": "./moo-ui.css",
10
+ "path": "dist/assets/css/moo-ui.css",
11
+ "sha256": "00db81fad96811182874b9207746a3154adb0f457c965a08e639a04af83f95a4"
12
+ },
13
+ {
14
+ "export": "./theme-prepaint.js",
15
+ "path": "dist/js/theme-prepaint.js",
16
+ "sha256": "e40ce7caa83112498c0819719e63ee8d0f23f58236f02ec038ccabf7a128121a"
17
+ }
18
+ ],
19
+ "package": {
20
+ "name": "@wpmoo/ui",
21
+ "version": "1.0.0-rc.8"
22
+ },
23
+ "schemaVersion": 1
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/ui",
3
- "version": "1.0.0-rc.7",
3
+ "version": "1.0.0-rc.8",
4
4
  "description": "Bootstrap markup. shadcn feel. A frontend-framework-free component system for server-rendered apps.",
5
5
  "license": "MIT",
6
6
  "author": "WPMoo (https://wpmoo.org)",
@@ -42,6 +42,8 @@
42
42
  "./chart.min.js": "./dist/js/chart.min.js",
43
43
  "./datepicker.js": "./dist/js/datepicker.js",
44
44
  "./datepicker.min.js": "./dist/js/datepicker.min.js",
45
+ "./theme-prepaint.js": "./dist/js/theme-prepaint.js",
46
+ "./release-manifest.json": "./dist/release-manifest.json",
45
47
  "./scss/config": "./scss/_config.scss",
46
48
  "./scss/moo-ui": "./scss/moo-ui.scss",
47
49
  "./scss/moo-core": "./scss/moo-core.scss",
@@ -66,6 +68,8 @@
66
68
  "dist/js/chart.min.js",
67
69
  "dist/js/datepicker.js",
68
70
  "dist/js/datepicker.min.js",
71
+ "dist/js/theme-prepaint.js",
72
+ "dist/release-manifest.json",
69
73
  "scss/*.scss",
70
74
  "scss/**/*.scss",
71
75
  "certification.json",
@@ -25,10 +25,6 @@
25
25
  @import "bootstrap/scss/offcanvas";
26
26
  @import "bootstrap/scss/helpers/color-bg";
27
27
 
28
- // Moo component support
29
- @import "foundations/focus";
30
- @import "utilities/scroll_fade";
31
-
32
28
  // Moo components
33
29
  @import "components/kbd";
34
30
  @import "components/input";
@@ -36,6 +32,7 @@
36
32
  @import "components/select";
37
33
  @import "components/input_group";
38
34
  @import "components/button";
35
+ @import "components/search_trigger";
39
36
  @import "components/close_button";
40
37
  @import "components/button_group";
41
38
  @import "components/toggle_group";
@@ -167,6 +167,16 @@
167
167
  min-width: #{unquote("max(100%, var(--moo-dropdown-sidebar-min-width))")};
168
168
  }
169
169
 
170
+ // Header identity menus reuse the Sidebar account-menu body. Keep the same
171
+ // comfortable width when the shared body is hosted by the global navbar
172
+ // instead of a Sidebar slot; ordinary Header dropdowns remain content-sized.
173
+ // The trigger marker is the integration contract, so Dropdown does not reach
174
+ // into Sidebar's private class namespace to identify the menu body.
175
+ [data-identity-trigger="1"] + .dropdown-menu {
176
+ --moo-dropdown-sidebar-min-width: #{$moo-dropdown-sidebar-min-width};
177
+ min-width: var(--moo-dropdown-sidebar-min-width);
178
+ }
179
+
170
180
  @include media-breakpoint-down(sm) {
171
181
  [data-slot="sidebar-header"] .dropend > .dropdown-menu,
172
182
  [data-slot="sidebar-footer"] .dropend > .dropdown-menu {
@@ -0,0 +1,62 @@
1
+ // Shared command/search trigger composition. The host supplies the semantic
2
+ // button markup and behavior; Core owns only the reusable visual contract.
3
+ .search-trigger {
4
+ width: $moo-search-trigger-width;
5
+ max-width: 100%;
6
+ flex: 0 1 $moo-search-trigger-width;
7
+ min-width: 0;
8
+ justify-content: space-between;
9
+ height: $moo-search-trigger-height;
10
+ padding: $moo-search-trigger-padding-y $moo-search-trigger-padding-x;
11
+ color: var(--bs-secondary-color);
12
+ border: var(--bs-border-width) solid $moo-search-trigger-border-color;
13
+ border-radius: var(--bs-border-radius);
14
+ background: color-mix(in srgb, var(--bs-secondary-bg) 55%, var(--bs-body-bg));
15
+ font-size: $font-size-base;
16
+ font-weight: $font-weight-normal;
17
+ box-shadow: none;
18
+ }
19
+
20
+ .search-trigger:hover {
21
+ color: var(--bs-secondary-color);
22
+ border-color: $moo-search-trigger-border-color;
23
+ background: color-mix(in srgb, var(--bs-secondary-bg) 78%, var(--bs-body-bg));
24
+ }
25
+
26
+ .search-trigger:focus-visible {
27
+ color: var(--bs-secondary-color);
28
+ border-color: $moo-search-trigger-border-color;
29
+ background: color-mix(in srgb, var(--bs-secondary-bg) 78%, var(--bs-body-bg));
30
+ }
31
+
32
+ .search-trigger__label {
33
+ display: inline-flex;
34
+ min-width: 0;
35
+ align-items: center;
36
+ gap: $moo-search-trigger-label-gap;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ white-space: nowrap;
40
+ }
41
+
42
+ .search-trigger__shortcut {
43
+ display: inline-flex;
44
+ flex: 0 0 auto;
45
+ align-items: center;
46
+ gap: $moo-search-trigger-shortcut-gap;
47
+ margin-inline-start: auto;
48
+ }
49
+
50
+ @include media-breakpoint-up(lg) {
51
+ .search-trigger {
52
+ width: $moo-search-trigger-width-lg;
53
+ flex-basis: $moo-search-trigger-width-lg;
54
+ }
55
+ }
56
+
57
+ @include media-breakpoint-up(xxl) {
58
+ .search-trigger {
59
+ width: $moo-search-trigger-width-xxl;
60
+ flex-basis: $moo-search-trigger-width-xxl;
61
+ }
62
+ }
@@ -1,7 +1,6 @@
1
1
  // Bootstrap has no native sidebar, so this owned selector namespace is split
2
2
  // by responsibility while preserving one deterministic emission order.
3
- @import "sidebar/layout";
3
+ @import "sidebar/base";
4
4
  @import "sidebar/menus";
5
5
  @import "sidebar/identity";
6
- @import "sidebar/inset";
7
6
  @import "sidebar/collapsed";
@@ -0,0 +1,93 @@
1
+ // Bootstrap has no native sidebar, so this owned sidebar selector namespace
2
+ // provides the inner surface and anatomy. The App layout owns the outer
3
+ // <aside>, placement, width, visual variant, and collapse mode.
4
+
5
+ .sidebar-inner {
6
+ position: sticky;
7
+ top: 0;
8
+ display: flex;
9
+ flex-direction: column;
10
+ width: 100%;
11
+ height: 100svh;
12
+ max-height: 100svh;
13
+ background: var(--moo-sidebar);
14
+ }
15
+
16
+ .sidebar-header,
17
+ .sidebar-footer {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: $spacer * 0.25;
21
+ padding: $spacer * 0.5;
22
+ }
23
+
24
+ .sidebar-content {
25
+ display: flex;
26
+ flex: 1 1 auto;
27
+ flex-direction: column;
28
+ gap: $spacer * 0.5;
29
+ min-height: 0;
30
+ overflow-y: auto;
31
+ }
32
+
33
+ .sidebar-input {
34
+ width: 100%;
35
+ padding: $spacer * 0.25;
36
+ }
37
+
38
+ .sidebar-separator {
39
+ display: flex;
40
+ }
41
+
42
+ .sidebar-menu-skeleton {
43
+ display: flex;
44
+ align-items: center;
45
+ gap: $spacer * 0.25;
46
+ min-height: $spacer * 2;
47
+ padding-inline: $spacer * 0.5;
48
+ }
49
+
50
+ .sidebar-menu-skeleton__icon {
51
+ width: $spacer;
52
+ height: $spacer;
53
+ }
54
+
55
+ .sidebar-menu-skeleton__line {
56
+ flex: 1 1 auto;
57
+ }
58
+
59
+ .sidebar-group {
60
+ position: relative;
61
+ display: flex;
62
+ flex-direction: column;
63
+ padding: $spacer * 0.5;
64
+ }
65
+
66
+ .sidebar-group-content {
67
+ display: block;
68
+ }
69
+
70
+ .sidebar-group-content--inline {
71
+ display: flex;
72
+ align-items: center;
73
+ flex-wrap: wrap;
74
+ gap: $spacer * 0.25;
75
+ }
76
+
77
+ .sidebar-group-label {
78
+ display: flex;
79
+ align-items: center;
80
+ height: $spacer * 2;
81
+ padding-inline: $spacer * 0.5;
82
+ color: var(--moo-muted-foreground);
83
+ font-size: $font-size-base * 0.857;
84
+ font-weight: $font-weight-medium;
85
+ }
86
+
87
+ // Reserve room so a long label doesn't run under the absolutely positioned
88
+ // group action button (see .sidebar-group-action below). A descendant match
89
+ // covers both a group action placed directly in .sidebar-group and one
90
+ // nested inside .sidebar-group-content alongside the label.
91
+ .sidebar-group:has(.sidebar-group-action) .sidebar-group-label {
92
+ padding-inline-end: $spacer * 2.5;
93
+ }
@@ -1,65 +1,8 @@
1
- // Rail: a thin toggle strip straddling the sidebar's inline-end edge.
2
- .sidebar-rail {
3
- position: absolute;
4
- inset-block: 0;
5
- inset-inline-end: 0;
6
- z-index: 20;
7
- width: $spacer;
8
- padding: 0;
9
- transform: translateX(50%);
10
- border: 0;
11
- background: transparent;
12
- cursor: pointer;
13
- }
14
-
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 {
18
- transform: translateX(-50%);
19
- }
20
-
21
- .sidebar-rail::after {
22
- content: "";
23
- position: absolute;
24
- inset-block: 0;
25
- inset-inline-start: 50%;
26
- width: var(--bs-border-width);
27
- background: transparent;
28
- transition: background 0.15s ease;
29
- }
30
-
31
- .sidebar-rail:hover::after {
32
- background: var(--moo-sidebar-border);
33
- }
1
+ // Collapsed-to-icon inner state (desktop). App owns the outer shell width,
2
+ // rail, stacking, and responsive placement; Sidebar owns the inner content
3
+ // visibility and flyout anatomy.
34
4
 
35
- // Collapsed-to-icon rail (desktop). Text, labels, and the rail's disclosure
36
- // chevrons hide; every remaining control centers on its icon.
37
5
  @include media-breakpoint-up(lg) {
38
- .sidebar {
39
- position: relative;
40
- }
41
-
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;
49
- }
50
-
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);
61
- }
62
-
63
6
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-group-label,
64
7
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-menu-button__text,
65
8
  [data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] .sidebar-menu-button__subtitle,
@@ -173,24 +116,3 @@
173
116
  display: none;
174
117
  }
175
118
  }
176
-
177
- @include media-breakpoint-down(lg) {
178
- .sidebar-rail {
179
- display: none;
180
- }
181
-
182
- .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar {
183
- position: relative;
184
- inset: auto;
185
- flex: 0 0 auto;
186
- width: 100%;
187
- height: auto;
188
- visibility: visible;
189
- transform: none;
190
- }
191
-
192
- .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar-inner {
193
- height: auto;
194
- max-height: none;
195
- }
196
- }
@@ -1,5 +1,8 @@
1
1
  @scope (.moo-ui) to (:where(.moo-ui[data-bs-theme="light"], .moo-ui[data-bs-theme="dark"])) {
2
2
  @import "../components";
3
+ @import "../foundations/focus";
4
+ @import "../utilities/scroll_fade";
5
+ @import "../layouts/app";
3
6
  @import "../themes/forms";
4
7
  @include moo-overlay-backdrop-scoped;
5
8
  }
@@ -0,0 +1,77 @@
1
+ // Application layout owns the shell and page geometry.
2
+ // It is intentionally separate from component surfaces such as Sidebar.
3
+
4
+ @import "app/sidebar/base";
5
+ @import "app/sidebar/default";
6
+ @import "app/sidebar/floating";
7
+ @import "app/sidebar/inset";
8
+
9
+ .wrapper[data-layout="app"] {
10
+ display: flex;
11
+ width: 100%;
12
+ height: 100svh;
13
+ min-height: 0;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .wrapper[data-layout="app"][data-shell-mode="contained"] {
18
+ min-height: 0;
19
+ height: 34rem;
20
+ overflow: hidden;
21
+ border: var(--bs-border-width) solid var(--moo-border);
22
+ border-radius: var(--bs-border-radius-xl);
23
+ }
24
+
25
+ .wrapper[data-layout="app"] > [data-slot="page"] {
26
+ display: flex;
27
+ min-width: 0;
28
+ min-height: 0;
29
+ flex: 1 1 auto;
30
+ flex-direction: column;
31
+ overflow-y: auto;
32
+ }
33
+
34
+ .wrapper[data-layout="app"] > [data-slot="page"] > header,
35
+ .wrapper[data-layout="app"] > [data-slot="page"] > main,
36
+ .wrapper[data-layout="app"] > [data-slot="page"] > footer {
37
+ flex: 0 0 auto;
38
+ }
39
+
40
+ // The page main region owns the space between the optional header and footer.
41
+ // Keep the Bootstrap rail as the flex column too, so app previews and real
42
+ // shells can give their caller content a full-height surface without adding
43
+ // layout-specific height values.
44
+ .wrapper[data-layout="app"] > [data-slot="page"] > main {
45
+ display: flex;
46
+ min-height: 0;
47
+ flex: 1 1 auto;
48
+ flex-direction: column;
49
+ }
50
+
51
+ .wrapper[data-layout="app"] > [data-slot="page"] > main > :is(
52
+ .container,
53
+ .container-sm,
54
+ .container-md,
55
+ .container-lg,
56
+ .container-xl,
57
+ .container-xxl,
58
+ .container-fluid
59
+ ) {
60
+ display: flex;
61
+ min-height: 0;
62
+ flex: 1 1 auto;
63
+ flex-direction: column;
64
+ }
65
+
66
+ @include media-breakpoint-down(lg) {
67
+ .wrapper[data-layout="app"][data-shell-mode="contained"] {
68
+ flex-direction: column;
69
+ height: auto;
70
+ min-height: 0;
71
+ overflow: visible;
72
+ }
73
+
74
+ .wrapper[data-layout="app"][data-shell-mode="contained"] > [data-slot="page"] {
75
+ overflow: visible;
76
+ }
77
+ }
@@ -0,0 +1,117 @@
1
+ // Shared App-owned Sidebar shell geometry. Sidebar's component partials own
2
+ // only the inner surface and navigation anatomy.
3
+
4
+ .sidebar {
5
+ --moo-sidebar-width: #{$moo-sidebar-width};
6
+ --moo-sidebar-width-icon: #{$moo-sidebar-width-icon};
7
+ --bs-offcanvas-width: var(--moo-sidebar-width);
8
+ flex: 0 0 var(--moo-sidebar-width);
9
+ width: var(--moo-sidebar-width);
10
+ color: var(--moo-sidebar-foreground);
11
+ }
12
+
13
+ // Rail: a thin toggle strip straddling the Sidebar's inline-end edge.
14
+ .sidebar-rail {
15
+ position: absolute;
16
+ inset-block: 0;
17
+ inset-inline-end: 0;
18
+ z-index: 20;
19
+ width: $spacer;
20
+ padding: 0;
21
+ transform: translateX(50%);
22
+ border: 0;
23
+ background: transparent;
24
+ cursor: pointer;
25
+ }
26
+
27
+ // Read the Sidebar's computed direction so this remains valid inside the
28
+ // resolved-owner @scope as well as under a full-document html[dir] owner.
29
+ .sidebar:dir(rtl) .sidebar-rail {
30
+ transform: translateX(-50%);
31
+ }
32
+
33
+ .sidebar-rail::after {
34
+ content: "";
35
+ position: absolute;
36
+ inset-block: 0;
37
+ inset-inline-start: 50%;
38
+ width: var(--bs-border-width);
39
+ background: transparent;
40
+ transition: background 0.15s ease;
41
+ }
42
+
43
+ .sidebar-rail:hover::after {
44
+ background: var(--moo-sidebar-border);
45
+ }
46
+
47
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar-inner {
48
+ position: relative;
49
+ height: 100%;
50
+ max-height: 100%;
51
+ }
52
+
53
+ // The outer shell owns stacking, collapse width, and the contained-shell
54
+ // geometry. Inner collapsed content remains in components/sidebar/_collapsed.
55
+ @include media-breakpoint-up(lg) {
56
+ .sidebar {
57
+ position: relative;
58
+ }
59
+
60
+ // `offcanvas` is a collapsible mode, not a visual Sidebar variant. On
61
+ // desktop the App keeps the shell in normal flow and uses the shared App
62
+ // state attribute to collapse the column; below lg Bootstrap's native
63
+ // Offcanvas plugin continues to own the drawer transition.
64
+ .sidebar.offcanvas-lg[data-collapsible="offcanvas"] {
65
+ visibility: visible;
66
+ transform: none;
67
+ }
68
+
69
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"] > .sidebar.offcanvas-lg[data-collapsible="offcanvas"] {
70
+ flex-basis: 0;
71
+ width: 0;
72
+ overflow: hidden;
73
+ visibility: hidden;
74
+ }
75
+
76
+ .wrapper[data-layout="app"]:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar {
77
+ position: relative;
78
+ // Catalog page headers are sticky at $zindex-fixed. The Sidebar root must
79
+ // sit above that page-level stacking context while an identity menu is
80
+ // open, otherwise the workspace menu is painted underneath the header
81
+ // even though the menu itself is fixed and has dropdown z-index.
82
+ z-index: $zindex-fixed + 1;
83
+ }
84
+
85
+ // The sticky inner surface is its own stacking context. Elevate that
86
+ // context while an identity menu is open so the rail's hover indicator
87
+ // remains behind the fixed dropdown instead of painting across it.
88
+ .wrapper[data-layout="app"]:has([data-sidebar-dropdown-positioned] > .dropdown-menu.show) > .sidebar > .sidebar-inner {
89
+ z-index: $zindex-fixed + 1;
90
+ }
91
+
92
+ .wrapper[data-layout="app"][data-sidebar-state="collapsed"] .sidebar[data-collapsible="icon"] {
93
+ flex-basis: var(--moo-sidebar-width-icon);
94
+ width: var(--moo-sidebar-width-icon);
95
+ }
96
+ }
97
+
98
+ @include media-breakpoint-down(lg) {
99
+ .sidebar-rail {
100
+ display: none;
101
+ }
102
+
103
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar {
104
+ position: relative;
105
+ inset: auto;
106
+ flex: 0 0 auto;
107
+ width: 100%;
108
+ height: auto;
109
+ visibility: visible;
110
+ transform: none;
111
+ }
112
+
113
+ .wrapper[data-layout="app"][data-shell-mode="contained"] .sidebar-inner {
114
+ height: auto;
115
+ max-height: none;
116
+ }
117
+ }