@bpmn-nova/studio 0.3.4-preview → 0.3.5-preview

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.
@@ -0,0 +1,195 @@
1
+ const SIDES = ['left', 'right'];
2
+ let sidebarId = 0;
3
+
4
+ /** Default-Shell layout only. No model, history, routing or viewport mutations. */
5
+ export class StudioSidebars {
6
+ constructor(shell, elements) {
7
+ this.shell = shell;
8
+ this.elements = elements;
9
+ this.width = shell.container.getBoundingClientRect().width;
10
+ this.band = this.width <= 720 ? 'narrow' : 'wide';
11
+ this.collapsed = {
12
+ wide: Object.fromEntries(SIDES.map((side) => [side, this.config(side).collapsible && this.config(side).defaultCollapsed])),
13
+ narrow: Object.fromEntries(SIDES.map((side) => [side, this.config(side).collapsible])),
14
+ };
15
+ this.buttons = {};
16
+ this.inactive = {};
17
+ this.scrollPositions = {};
18
+ this.listeners = new Set();
19
+ this.destroyed = false;
20
+ this.motion = shell.container.ownerDocument.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)');
21
+ this.onMotionChange = () => { if (this.motion.matches) this.finishMotion(); };
22
+ this.motion?.addEventListener('change', this.onMotionChange);
23
+ elements.body.classList.add('is-managed-sidebars');
24
+ for (const side of SIDES) {
25
+ const panel = elements[side];
26
+ panel.id ||= `nova-sidebar-${++sidebarId}`;
27
+ const button = shell.container.ownerDocument.createElement('button');
28
+ button.type = 'button';
29
+ button.className = `nova-studio-sidebar-toggle is-${side}`;
30
+ button.dataset.sidebar = side;
31
+ button.setAttribute('aria-controls', panel.id);
32
+ button.addEventListener('click', () => this.setCollapsed(side, !this.getState()[side].collapsed, 'button'));
33
+ elements.body.appendChild(button);
34
+ this.buttons[side] = button;
35
+ }
36
+ this.onTransitionEnd = (event) => {
37
+ if (event.target === elements.body && event.propertyName === 'grid-template-columns') this.finishMotion();
38
+ };
39
+ elements.body.addEventListener('transitionend', this.onTransitionEnd);
40
+ this.apply({ animate: false });
41
+ this.observer = typeof ResizeObserver === 'function' ? new ResizeObserver((entries) => {
42
+ const nextWidth = entries[0]?.contentRect.width ?? shell.container.getBoundingClientRect().width;
43
+ if (this.destroyed || Math.abs(nextWidth - this.width) < 0.01) return;
44
+ const previous = this.getState();
45
+ this.width = nextWidth;
46
+ this.band = nextWidth <= 720 ? 'narrow' : 'wide';
47
+ this.apply({ animate: false });
48
+ this.notifyChanges(previous, 'responsive');
49
+ }) : null;
50
+ this.observer?.observe(shell.container);
51
+ }
52
+
53
+ config(side) { return this.shell._config.ui[`${side}Panel`]; }
54
+
55
+ getState() {
56
+ return Object.freeze(Object.fromEntries(SIDES.map((side) => [side, Object.freeze({
57
+ collapsed: this.config(side).collapsible && this.collapsed[this.band][side],
58
+ hidden: this.shell._isSidebarHidden(side),
59
+ collapsible: this.config(side).collapsible,
60
+ })])));
61
+ }
62
+
63
+ subscribe(listener) {
64
+ if (typeof listener !== 'function') throw new TypeError('subscribeSidebarChange() requires a listener function.');
65
+ if (this.destroyed) return () => {};
66
+ this.listeners.add(listener);
67
+ return () => this.listeners.delete(listener);
68
+ }
69
+
70
+ setCollapsed(side, collapsed, source = 'api') {
71
+ if (!SIDES.includes(side)) throw new TypeError('Sidebar side must be left or right.');
72
+ if (typeof collapsed !== 'boolean') throw new TypeError('Sidebar collapsed must be a boolean.');
73
+ if (this.destroyed || this.shell._destroyed) return false;
74
+ const previous = this.getState();
75
+ const state = previous[side];
76
+ if (state.hidden || !state.collapsible || state.collapsed === collapsed) return false;
77
+ this.collapsed[this.band][side] = collapsed;
78
+ this.apply();
79
+ this.notifyChanges(previous, source);
80
+ return true;
81
+ }
82
+
83
+ updateConfig(previous) {
84
+ for (const side of SIDES) {
85
+ if (!this.config(side).collapsible) {
86
+ this.collapsed.wide[side] = false;
87
+ this.collapsed.narrow[side] = false;
88
+ }
89
+ }
90
+ this.apply();
91
+ this.notifyChanges(previous, 'config');
92
+ }
93
+
94
+ notifyChanges(previous, source) {
95
+ const next = this.getState();
96
+ for (const side of SIDES) {
97
+ if (next[side].collapsed === previous[side].collapsed) continue;
98
+ const event = Object.freeze({ side, collapsed: next[side].collapsed, previousCollapsed: previous[side].collapsed, mode: this.shell.mode, source });
99
+ for (const listener of [...this.listeners]) {
100
+ try { listener(event); } catch (error) { console.error('BpmnStudioShell sidebar listener failed.', error); }
101
+ }
102
+ }
103
+ }
104
+
105
+ finishMotion({ resume = true } = {}) {
106
+ clearTimeout(this.motionTimeout);
107
+ this.motionTimeout = null;
108
+ if (resume) this.motionViewer?._setSidebarTransitionActive(false);
109
+ this.motionViewer = null;
110
+ }
111
+
112
+ apply({ animate = true } = {}) {
113
+ if (this.destroyed) return;
114
+ const { body } = this.elements;
115
+ const state = this.getState();
116
+ const desired = Object.fromEntries(SIDES.map((side) => [side,
117
+ state[side].hidden || state[side].collapsed ? 0 : this.shell._config.ui.sidebarWidth[side],
118
+ ]));
119
+ const total = desired.left + desired.right;
120
+ const available = Math.max(0, this.width - Math.min(96, this.width));
121
+ const scale = total > available ? available / total : 1;
122
+ const widths = { left: desired.left * scale, right: desired.right * scale };
123
+ const changed = !this.appliedWidths || SIDES.some((side) => widths[side] !== this.appliedWidths[side]);
124
+ const moving = animate && !this.motion?.matches
125
+ && (changed ? Boolean(this.appliedWidths) : Boolean(this.motionTimeout));
126
+ if (moving && changed) {
127
+ clearTimeout(this.motionTimeout);
128
+ if (this.motionViewer !== this.shell.viewer) this.finishMotion();
129
+ this.motionViewer = this.shell.viewer;
130
+ this.motionViewer?._setSidebarTransitionActive(true);
131
+ // Fallback also covers transition cancellation, reduced motion and detached hosts.
132
+ this.motionTimeout = setTimeout(() => this.finishMotion(), 240);
133
+ }
134
+ body.classList.toggle('is-sidebar-animated', Boolean(moving));
135
+ body.classList.toggle('is-sidebar-narrow', this.band === 'narrow');
136
+ for (const side of SIDES) {
137
+ const panel = this.elements[side];
138
+ const content = this.elements[`${side}Content`];
139
+ const button = this.buttons[side];
140
+ const inactive = state[side].hidden || state[side].collapsed;
141
+ const buttonHidden = state[side].hidden || !state[side].collapsible;
142
+ if (inactive && this.inactive[side] === false) {
143
+ this.scrollPositions[side] = [content, ...content.querySelectorAll('*')]
144
+ .filter((element) => element.scrollTop || element.scrollLeft)
145
+ .map((element) => ({ element, top: element.scrollTop, left: element.scrollLeft }));
146
+ }
147
+ const activeElement = panel.ownerDocument.activeElement;
148
+ if ((inactive && panel.contains(activeElement)) || (buttonHidden && activeElement === button)) {
149
+ if (!buttonHidden) {
150
+ button.hidden = false;
151
+ button.focus({ preventScroll: true });
152
+ }
153
+ else {
154
+ this.shell._canvasHost?.setAttribute('tabindex', '-1');
155
+ this.shell._canvasHost?.focus({ preventScroll: true });
156
+ }
157
+ }
158
+ panel.inert = inactive;
159
+ panel.setAttribute('aria-hidden', String(inactive));
160
+ panel.hidden = state[side].hidden;
161
+ button.hidden = buttonHidden;
162
+ button.setAttribute('aria-expanded', String(!state[side].collapsed));
163
+ const label = `${state[side].collapsed ? '展开' : '收起'}${side === 'left' ? '左侧节点面板' : '右侧面板'}`;
164
+ button.setAttribute('aria-label', label);
165
+ button.title = label;
166
+ button.textContent = (side === 'left') !== state[side].collapsed ? '‹' : '›';
167
+ body.classList.toggle(`is-${side}-collapsed`, state[side].collapsed);
168
+ body.style.setProperty(`--nova-sidebar-${side}-track`, `${widths[side]}px`);
169
+ if (!inactive || !content.style.width) content.style.width = `${inactive ? this.shell._config.ui.sidebarWidth[side] : widths[side]}px`;
170
+ if (!inactive && this.inactive[side]) {
171
+ for (const { element, top, left } of this.scrollPositions[side] || []) {
172
+ if (!content.contains(element) && element !== content) continue;
173
+ element.scrollTop = top;
174
+ element.scrollLeft = left;
175
+ }
176
+ delete this.scrollPositions[side];
177
+ }
178
+ this.inactive[side] = inactive;
179
+ }
180
+ this.elements.rightContent.dataset.layout = this.config('right').layout;
181
+ this.appliedWidths = widths;
182
+ // Resolve auto projection against the final layout, not an intermediate width.
183
+ if (!moving) this.finishMotion();
184
+ }
185
+
186
+ destroy() {
187
+ this.destroyed = true;
188
+ this.observer?.disconnect();
189
+ this.motion?.removeEventListener('change', this.onMotionChange);
190
+ this.elements.body.removeEventListener('transitionend', this.onTransitionEnd);
191
+ this.listeners.clear();
192
+ clearTimeout(this.motionTimeout);
193
+ this.motionViewer = null;
194
+ }
195
+ }
package/dist/styles.css CHANGED
@@ -536,6 +536,14 @@
536
536
 
537
537
  /* Gateways */
538
538
  .mb-node-gateway { display: grid; place-items: center; z-index: 7; }
539
+ .mb-node .mb-node-scaled-shape {
540
+ position: absolute;
541
+ left: 50%;
542
+ top: 50%;
543
+ box-sizing: border-box;
544
+ transform: translate(-50%, -50%) rotate(var(--mb-node-shape-rotation, 0rad)) scale(var(--mb-node-shape-scale, 1));
545
+ transform-origin: center;
546
+ }
539
547
  .mb-gateway-shape {
540
548
  width: 49px;
541
549
  height: 49px;
@@ -699,6 +707,8 @@
699
707
  .mb-port-right:hover { transform: translateY(-50%) scale(1.2); }
700
708
  .mb-port-top:hover,
701
709
  .mb-port-bottom:hover { transform: translateX(-50%) scale(1.2); }
710
+ .mb-port.mb-port-geometry { transform: translate(-50%, -50%); }
711
+ .mb-port.mb-port-geometry:hover { transform: translate(-50%, -50%) scale(1.2); }
702
712
  .mb-node.is-connecting-source .mb-port-right { opacity: 1; border-color: var(--mb-primary); background: var(--mb-primary); box-shadow: 0 0 0 4px color-mix(in srgb, var(--nova-color-primary) 12%, transparent); }
703
713
  .mb-port-target {
704
714
  width: 12px;
@@ -1140,7 +1150,7 @@ textarea.property-control { resize: vertical; }
1140
1150
  .property-branch-target small { color: var(--nova-color-text-muted); font-size: var(--nova-font-size-xs, 10px); line-height: 14px; }.property-branch-target strong { overflow: hidden; color: var(--nova-color-text-secondary); font-size: var(--nova-font-size-sm, 12px); line-height: 18px; font-weight: var(--nova-font-weight-semibold, 600); text-overflow: ellipsis; white-space: nowrap; }.property-branch-target > span { color: var(--nova-color-primary); font-size: var(--nova-font-size-sm, 12px); }
1141
1151
 
1142
1152
  /* @bpmn-nova/internal/studio */
1143
- .nova-studio-shell { --nova-left-width: 244px; --nova-right-width: 360px; --nova-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; --nova-font-size-xs: 10px; --nova-font-size-sm: 12px; --nova-font-size-md: 14px; --nova-font-size-lg: 16px; --nova-font-weight-regular: 400; --nova-font-weight-medium: 500; --nova-font-weight-semibold: 600; --nova-font-weight-bold: 700; position: relative; isolation: isolate; width: 100%; height: 100%; min-height: 520px; overflow: hidden; display: grid; grid-template-rows: 58px minmax(0, 1fr); color: var(--nova-color-text, #263148); background: var(--nova-color-canvas, #f6f7fb); font-family: var(--nova-font-family); }
1153
+ .nova-studio-shell { --nova-left-width: 244px; --nova-right-width: 360px; --nova-control-height: 32px; --nova-header-height: 58px; --nova-control-font-size: 12px; --nova-control-padding-inline: 10px; --nova-mode-control-padding-inline: 12px; --nova-control-icon-size: 14px; --nova-control-radius: 7px; --nova-mode-control-height: 36px; --nova-mode-button-height: 28px; --nova-projection-button-height: 26px; --nova-icon-button-size: 31px; --nova-split-caret-width: 27px; --nova-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; --nova-font-size-xs: 10px; --nova-font-size-sm: 12px; --nova-font-size-md: 14px; --nova-font-size-lg: 16px; --nova-font-weight-regular: 400; --nova-font-weight-medium: 500; --nova-font-weight-semibold: 600; --nova-font-weight-bold: 700; position: relative; isolation: isolate; width: 100%; height: 100%; min-height: 520px; overflow: hidden; display: grid; grid-template-rows: var(--nova-header-height) minmax(0, 1fr); color: var(--nova-color-text, #263148); background: var(--nova-color-canvas, #f6f7fb); font-family: var(--nova-font-family); }
1144
1154
  .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1145
1155
  .nova-studio-header[hidden], .nova-studio-left[hidden], .nova-studio-right[hidden], .nova-studio-statusbar[hidden] { display: none !important; }
1146
1156
  .nova-studio-header { min-width: 0; border-bottom: 1px solid var(--nova-color-divider, #e4e8f0); background: var(--nova-color-surface, #fff); display: grid; grid-template-columns: minmax(180px, auto) auto minmax(0, 1fr); align-items: center; gap: 18px; padding: 0 12px 0 14px; }
@@ -1150,20 +1160,22 @@ textarea.property-control { resize: vertical; }
1150
1160
  .nova-studio-brand-copy strong { color: var(--nova-tone-primary-foreground, #3443b8); font-size: var(--nova-font-size-md); line-height: 20px; font-weight: var(--nova-font-weight-bold); letter-spacing: -.01em; }
1151
1161
  .nova-studio-brand-copy span { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--nova-color-text-muted, #7c8799); font-size: var(--nova-font-size-xs); line-height: 14px; }
1152
1162
  .nova-studio-center-controls { min-width: 0; justify-self: center; display: flex; align-items: center; gap: 8px; }
1153
- .nova-studio-mode-switch { height: 36px; border: 1px solid var(--nova-color-divider, #e6e8ef); border-radius: 10px; padding: 3px; display: flex; align-items: center; gap: 2px; background: var(--nova-color-surface-muted, #f4f5f8); box-shadow: inset 0 1px 2px rgba(35,43,68,.03); }
1154
- .nova-studio-mode-switch button { height: 28px; border: 0; border-radius: 7px; padding: 0 12px; display: flex; align-items: center; gap: 5px; color: var(--nova-color-text-muted, #8a93a4); background: transparent; font-size: var(--nova-font-size-sm); line-height: 18px; white-space: nowrap; cursor: pointer; }
1163
+ .nova-studio-mode-switch { height: var(--nova-mode-control-height); border: 1px solid var(--nova-color-divider, #e6e8ef); border-radius: calc(var(--nova-control-radius) + 3px); padding: 3px; display: flex; align-items: center; gap: 2px; background: var(--nova-color-surface-muted, #f4f5f8); box-shadow: inset 0 1px 2px rgba(35,43,68,.03); }
1164
+ .nova-studio-mode-switch button { height: var(--nova-mode-button-height); border: 0; border-radius: var(--nova-control-radius); padding: 0 var(--nova-mode-control-padding-inline); display: flex; align-items: center; gap: 5px; color: var(--nova-color-text-muted, #8a93a4); background: transparent; font-size: var(--nova-control-font-size); line-height: 18px; white-space: nowrap; cursor: pointer; }
1155
1165
  .nova-studio-mode-switch button.is-active { color: var(--nova-tone-primary-foreground, #4f5dd1); background: var(--nova-color-surface, #fff); font-weight: var(--nova-font-weight-semibold); box-shadow: var(--nova-shadow-sm); }
1156
- .nova-studio-projection-switch { height: 32px; border: 1px solid var(--nova-color-border, #e2e6ef); border-radius: 8px; padding: 2px; display: flex; align-items: center; gap: 2px; background: var(--nova-color-surface, #fff); }
1166
+ .nova-studio-projection-switch { height: var(--nova-control-height); border: 1px solid var(--nova-color-border, #e2e6ef); border-radius: calc(var(--nova-control-radius) + 1px); padding: 2px; display: flex; align-items: center; gap: 2px; background: var(--nova-color-surface, #fff); }
1157
1167
  .nova-studio-projection-switch.is-hidden { display: none; }
1158
- .nova-studio-projection-switch button { height: 26px; border: 0; border-radius: 6px; padding: 0 9px; color: var(--nova-color-text-muted, #7c8799); background: transparent; font-size: var(--nova-font-size-sm); line-height: 18px; white-space: nowrap; cursor: pointer; }
1168
+ .nova-studio-projection-switch button { height: var(--nova-projection-button-height); border: 0; border-radius: max(0px, calc(var(--nova-control-radius) - 1px)); padding: 0 max(0px, calc(var(--nova-control-padding-inline) - 1px)); color: var(--nova-color-text-muted, #7c8799); background: transparent; font-size: var(--nova-control-font-size); line-height: 18px; white-space: nowrap; cursor: pointer; }
1159
1169
  .nova-studio-projection-switch button:hover { color: var(--nova-tone-primary-foreground, #4d59ca); background: var(--nova-color-primary-soft, #f4f5ff); }.nova-studio-projection-switch button.is-active { color: var(--nova-tone-primary-foreground, #4654c8); background: var(--nova-color-primary-soft, #eef0ff); font-weight: var(--nova-font-weight-semibold); }
1160
1170
  .nova-studio-tools { min-width: 0; overflow: visible; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
1161
1171
  .nova-studio-tools::-webkit-scrollbar { display: none; }
1162
- .nova-studio-header-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
1163
- .nova-studio-tool-divider { flex: none; width: 1px; height: 20px; margin: 0 2px; background: var(--nova-color-divider, #e4e8f0); }
1164
- .nova-studio-tool { flex: none; min-height: 32px; border: 1px solid var(--nova-color-border, #e2e6ef); border-radius: 7px; padding: 0 10px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; color: var(--nova-color-text-secondary, #4c586d); background: var(--nova-color-surface, #fff); font-size: var(--nova-font-size-sm); line-height: 18px; white-space: nowrap; cursor: pointer; }
1172
+ .nova-studio-header-actions { min-width: 0; min-height: var(--nova-control-height); display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
1173
+ .nova-studio-header .nova-icon { width: var(--nova-control-icon-size); height: var(--nova-control-icon-size); }
1174
+ .nova-studio-header .nova-icon.nova-icon-xs { width: max(12px, calc(var(--nova-control-icon-size) - 2px)); height: max(12px, calc(var(--nova-control-icon-size) - 2px)); }
1175
+ .nova-studio-tool-divider { flex: none; width: 1px; height: calc(var(--nova-control-height) - 12px); margin: 0 2px; background: var(--nova-color-divider, #e4e8f0); }
1176
+ .nova-studio-tool { flex: none; height: var(--nova-control-height); min-height: var(--nova-control-height); border: 1px solid var(--nova-color-border, #e2e6ef); border-radius: var(--nova-control-radius); padding: 0 var(--nova-control-padding-inline); display: inline-flex; align-items: center; justify-content: center; gap: 5px; color: var(--nova-color-text-secondary, #4c586d); background: var(--nova-color-surface, #fff); font-size: var(--nova-control-font-size); line-height: 18px; white-space: nowrap; cursor: pointer; }
1165
1177
  .nova-studio-tool:focus { outline: none; }.nova-studio-tool:focus-visible { outline: 2px solid var(--nova-color-focus-ring, #aeb7ff); outline-offset: 1px; }
1166
- .nova-studio-tool.is-icon { width: 31px; padding: 0; font-size: 14px; }
1178
+ .nova-studio-tool.is-icon { width: var(--nova-icon-button-size); padding: 0; font-size: calc(var(--nova-control-font-size) + 2px); }
1167
1179
  .nova-studio-tool:hover, .nova-studio-tool.is-active { border-color: var(--nova-tone-primary-border, #bec6ff); color: var(--nova-tone-primary-foreground, #3948c5); background: var(--nova-color-primary-soft, #f2f4ff); }
1168
1180
  .nova-studio-tool.is-magic { border-color: var(--nova-tone-primary-border, #d9ddff); color: var(--nova-tone-primary-foreground, #4654c8); background: var(--nova-color-primary-soft, #f4f5ff); font-weight: var(--nova-font-weight-semibold); }
1169
1181
  .nova-studio-tool.is-primary { border-color: var(--nova-color-primary); color: var(--nova-color-text-inverse, #fff); background: var(--nova-color-primary, #5361d6); }
@@ -1182,8 +1194,8 @@ textarea.property-control { resize: vertical; }
1182
1194
  .nova-studio-export-option.is-hidden { display: none; }
1183
1195
  .nova-studio-import-input { display: none; }
1184
1196
  .nova-studio-beautify-split { position: relative; flex: none; display: flex; }
1185
- .nova-studio-beautify-split .nova-studio-beautify { border-radius: 7px 0 0 7px; }
1186
- .nova-studio-beautify-split .nova-studio-beautify-caret { width: 27px; border-left: 0; border-radius: 0 7px 7px 0; padding: 0; }
1197
+ .nova-studio-beautify-split .nova-studio-beautify { border-radius: var(--nova-control-radius) 0 0 var(--nova-control-radius); }
1198
+ .nova-studio-beautify-split .nova-studio-beautify-caret { width: var(--nova-split-caret-width); border-left: 0; border-radius: 0 var(--nova-control-radius) var(--nova-control-radius) 0; padding: 0; }
1187
1199
  .nova-studio-beautify-menu { position: absolute; z-index: 30; top: calc(100% + 8px); right: 0; width: 224px; border: 1px solid var(--nova-color-border, #e5e7ee); border-radius: 12px; padding: 12px; color: var(--nova-color-text-secondary, #3d4659); background: var(--nova-color-surface, #fff); box-shadow: var(--nova-shadow-lg); }
1188
1200
  .nova-studio-beautify-menu.is-hidden, .nova-studio-canvas-floating-head.is-hidden, .is-hidden-by-mode { display: none !important; }
1189
1201
  .nova-studio-beautify-title { margin-bottom: 9px; font-size: 12px; font-weight: 700; line-height: 18px; }
@@ -1429,7 +1441,7 @@ textarea.property-control { resize: vertical; }
1429
1441
  .nova-studio-brand-copy span { display: none; }
1430
1442
  }
1431
1443
  @media (max-width: 720px) {
1432
- .nova-studio-shell { grid-template-rows: 54px minmax(0,1fr); }
1444
+ .nova-studio-shell { grid-template-rows: var(--nova-header-height) minmax(0,1fr); }
1433
1445
  .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1434
1446
  .nova-studio-header { grid-template-columns: auto minmax(0,1fr); padding-left: 9px; }
1435
1447
  .nova-studio-brand-copy { display: none; }
@@ -1442,3 +1454,51 @@ textarea.property-control { resize: vertical; }
1442
1454
  .nova-studio-body.is-timeline .nova-studio-statusbar { display: none; }
1443
1455
  }
1444
1456
 
1457
+ /* The managed Shell keeps three tracks, including zero-width collapsed tracks.
1458
+ Legacy base layout above remains usable without the sidebar controller. */
1459
+ .nova-studio-shell .nova-studio-body.is-managed-sidebars {
1460
+ position: relative;
1461
+ grid-template-columns: var(--nova-sidebar-left-track, 0px) minmax(0, 1fr) var(--nova-sidebar-right-track, 0px);
1462
+ }
1463
+ .nova-studio-body.is-managed-sidebars.is-sidebar-animated {
1464
+ transition: grid-template-columns 200ms cubic-bezier(0.2, 0, 0, 1);
1465
+ }
1466
+ .nova-studio-shell .is-managed-sidebars > .nova-studio-left, .nova-studio-shell .is-managed-sidebars > .nova-studio-right {
1467
+ display: block; min-width: 0; min-height: 0; overflow: hidden; border: 0;
1468
+ }
1469
+ .is-managed-sidebars > .nova-studio-left { grid-column: 1; grid-row: 1; }
1470
+ .is-managed-sidebars > .nova-studio-canvas-column { grid-column: 2; grid-row: 1; }
1471
+ .is-managed-sidebars > .nova-studio-right { grid-column: 3; grid-row: 1; }
1472
+ .nova-studio-sidebar-content {
1473
+ box-sizing: border-box; height: 100%; min-width: 0; min-height: 0;
1474
+ scrollbar-width: thin; scrollbar-color: var(--nova-color-border-strong, #d9dee7) transparent;
1475
+ }
1476
+ .nova-studio-left-content { overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; border-right: 1px solid var(--nova-color-divider); }
1477
+ .nova-studio-right-content { border-left: 1px solid var(--nova-color-divider); }
1478
+ .nova-studio-right-content[data-layout="flex"] { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
1479
+ .nova-studio-right-content[data-layout="scroll"] { display: block; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; }
1480
+ .nova-studio-right-content[data-layout="flex"].nova-properties > .properties-head { flex: none; position: relative; }
1481
+ .nova-studio-right-content[data-layout="flex"].nova-properties > .properties-body { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; }
1482
+ .nova-studio-right-content[data-layout="scroll"].nova-properties > .properties-head { position: relative; }
1483
+ .nova-studio-right-content[data-layout="flex"] > .nova-studio-readonly-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; }
1484
+ .nova-studio-right-content[data-layout="flex"] .nova-studio-readonly-header { flex: none; }
1485
+ .nova-studio-right-content[data-layout="flex"] .nova-studio-readonly-body { flex: 1; min-height: 0; overflow: auto; align-content: start; overscroll-behavior: contain; }
1486
+ .nova-studio-sidebar-content::-webkit-scrollbar, .nova-studio-right-content .properties-body::-webkit-scrollbar, .nova-studio-readonly-body::-webkit-scrollbar { width: 4px; height: 4px; }
1487
+ .nova-studio-sidebar-content::-webkit-scrollbar-thumb, .nova-studio-right-content .properties-body::-webkit-scrollbar-thumb, .nova-studio-readonly-body::-webkit-scrollbar-thumb { border-radius: 999px; background: var(--nova-color-border-strong, #d9dee7); }
1488
+ .nova-studio-sidebar-toggle {
1489
+ position: absolute; z-index: 8; top: 50%; transform: translateY(-50%);
1490
+ box-sizing: border-box; width: 22px; height: 44px; padding: 0;
1491
+ display: grid; place-items: center; border: 1px solid var(--nova-color-border);
1492
+ border-radius: 7px; background: var(--nova-color-surface); color: var(--nova-color-text-muted);
1493
+ box-shadow: var(--nova-shadow-sm); font: 22px/1 sans-serif; cursor: pointer;
1494
+ }
1495
+ .nova-studio-sidebar-toggle[hidden] { display: none; }
1496
+ .nova-studio-sidebar-toggle.is-left { left: max(0px, calc(var(--nova-sidebar-left-track, 0px) - 11px)); }
1497
+ .nova-studio-sidebar-toggle.is-right { right: max(0px, calc(var(--nova-sidebar-right-track, 0px) - 11px)); }
1498
+ .is-sidebar-animated > .nova-studio-sidebar-toggle { transition: left 200ms cubic-bezier(0.2, 0, 0, 1), right 200ms cubic-bezier(0.2, 0, 0, 1); }
1499
+ .nova-studio-sidebar-toggle:hover { color: var(--nova-color-primary); background: var(--nova-color-primary-soft); }
1500
+ .nova-studio-sidebar-toggle:focus-visible { outline: 2px solid var(--nova-color-focus-ring); outline-offset: 2px; }
1501
+ @media (prefers-reduced-motion: reduce) {
1502
+ .nova-studio-body.is-managed-sidebars.is-sidebar-animated, .is-sidebar-animated > .nova-studio-sidebar-toggle { transition: none; }
1503
+ }
1504
+