@bpmn-nova/vue 0.3.4-preview → 0.3.6-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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Fragment, Teleport, createApp, defineComponent, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
1
+ import { Fragment, Teleport, createApp, defineComponent, h, onBeforeUnmount, onMounted, onUpdated, reactive, readonly, ref, shallowRef, watch } from 'vue';
2
2
  import { BpmnDesigner as CoreDesigner } from '@bpmn-nova/studio/designer';
3
3
  import { BpmnViewer as CoreViewer } from '@bpmn-nova/studio/viewer';
4
4
  import { importBpmn, exportBpmn } from '@bpmn-nova/studio';
@@ -6,6 +6,15 @@ import { createEmptyProcess } from '@bpmn-nova/studio';
6
6
  import { BpmnCanvas as CoreCanvas, BpmnStudioShell as CoreStudioShell, createInteractionController, createStudioController, createTemplateRegistry } from '@bpmn-nova/studio';
7
7
  import { PalettePanel as CorePalettePanel, createDefaultPaletteRegistry as createCoreDefaultPaletteRegistry } from '@bpmn-nova/studio';
8
8
 
9
+ export {
10
+ activityState,
11
+ createRuntimePresentation,
12
+ formatRuntimeInstant,
13
+ inspectRuntime,
14
+ normalizeRuntime,
15
+ parseRuntimeInstant,
16
+ } from '@bpmn-nova/studio/runtime';
17
+
9
18
  export {
10
19
  createContextMenuRegistry,
11
20
  createDefaultContextMenuRegistry,
@@ -49,7 +58,7 @@ function resolveModel(props) {
49
58
  }
50
59
 
51
60
  export function useBpmnStudio(options = {}) {
52
- const studio = options.studio || createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
61
+ const studio = options.studio || createStudioController({ model: resolveModel(options), config: options.config, propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
53
62
  const state = reactive(studio.getState());
54
63
  const off = studio.subscribe((event) => Object.assign(state, event.state));
55
64
  onBeforeUnmount(() => { off(); if (!options.studio) studio.destroy(); });
@@ -126,33 +135,41 @@ export const BpmnPalettePanel = defineComponent({
126
135
  export const BpmnStudio = defineComponent({
127
136
  name: 'BpmnNovaStudio',
128
137
  props: {
129
- studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, propertiesProfile: String, allowedNodeTypes: Array, allowedEdgeTypes: Array,
138
+ studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, config: Object, propertiesProfile: String, allowedNodeTypes: Array, allowedEdgeTypes: Array,
130
139
  iconRegistry: Object, paletteRegistry: Object, propertiesRegistry: Object, templateRegistry: Object, contextMenuRegistry: Object,
131
140
  nodeRenderers: Object, nodeRenderer: Function, nodeSubtitleResolver: Function, slotsConfig: Object, layout: Function, regions: Object, leftWidth: Number, rightWidth: Number,
132
141
  runtime: Object, runtimePresenter: Function, runtimeTraceProjector: Function, runtimeAssetResolver: Function, runtimeTimelineRenderer: Function, onRuntimeTraceItemClick: Function, runtimeDetailsRenderer: Function, onRuntimeDetailsOpen: Function,
133
142
  runtimeTransitionDetailsRenderer: Function, onRuntimeTransitionDetailsOpen: Function,
134
143
  timeline: Object, runtimeDetails: Object, runtimeTraceOptions: Object, onTraceClick: Function, onElementClick: Function,
135
- mode: String, allowedModes: Array, projection: String, responsive: { type: Boolean, default: false }, projectionOptions: Array,
144
+ mode: String, allowedModes: Array, projection: String, responsive: { type: Boolean, default: undefined }, projectionOptions: Array,
136
145
  theme: [Object, String], runtimeAppearance: Object, svgExport: Object, onThemeChange: Function,
137
146
  },
138
- emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click', 'update:mode', 'mode-change', 'validation'],
147
+ emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click', 'update:mode', 'mode-change', 'validation', 'sidebar-change'],
139
148
  setup(props, { emit, expose, attrs, slots }) {
140
149
  const host = ref(null);
141
150
  const headerTarget = ref(null);
142
151
  const headerStartTarget = ref(null);
143
152
  const headerActionsTarget = ref(null);
153
+ const rightTarget = shallowRef(null);
144
154
  const headerContext = ref(null);
145
155
  const headerStartContext = ref(null);
146
156
  const headerActionsContext = ref(null);
157
+ const rightContext = shallowRef(null);
147
158
  const owned = !props.studio;
148
- const studio = props.studio || createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
159
+ const studio = props.studio || createStudioController({ model: resolveModel(props), config: props.config, propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
149
160
  const state = reactive(studio.getState());
150
161
  const actualMode = ref(props.mode || props.allowedModes?.[0] || 'design');
162
+ const actualUi = reactive({ controlSize: 'medium', controlHeight: '32px' });
163
+ const readonlyUi = readonly(actualUi);
164
+ const panelSelection = shallowRef(Object.freeze({ selection: studio.selection, selectedElement: studio.getSelectedElement(), trace: null }));
151
165
  const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
152
166
  let shell = null;
153
167
  let off = null;
154
168
  let offMode = null;
155
169
  let offValidation = null;
170
+ let offSidebar = null;
171
+ let offPanelSelection = null;
172
+ let mountedRightSlot = null;
156
173
 
157
174
  const createTeleportSlot = (target, contextRef) => (context) => {
158
175
  target.value = context.container;
@@ -162,6 +179,8 @@ export const BpmnStudio = defineComponent({
162
179
  if (contextRef.value === context) contextRef.value = null;
163
180
  };
164
181
  };
182
+ const nativeRightSlot = createTeleportSlot(rightTarget, rightContext);
183
+ const resolveRightSlot = () => slots.right ? nativeRightSlot : props.slotsConfig?.right;
165
184
 
166
185
  onMounted(() => {
167
186
  const shellSlots = { ...(props.slotsConfig || {}) };
@@ -170,14 +189,20 @@ export const BpmnStudio = defineComponent({
170
189
  if (slots['header-start']) shellSlots.headerStart = createTeleportSlot(headerStartTarget, headerStartContext);
171
190
  if (slots['header-actions']) shellSlots.headerActions = createTeleportSlot(headerActionsTarget, headerActionsContext);
172
191
  }
173
- shell = new CoreStudioShell({ container: host.value, studio, iconRegistry: props.iconRegistry, paletteRegistry: props.paletteRegistry, propertiesRegistry: props.propertiesRegistry, templateRegistry: props.templateRegistry, contextMenuRegistry: props.contextMenuRegistry, rendererOptions: { nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, nodeSubtitleResolver, runtimePresenter: props.runtimePresenter, runtimeTraceProjector: props.runtimeTraceProjector, runtimeAssetResolver: props.runtimeAssetResolver, runtimeTraceOptions: props.runtimeTraceOptions, runtimeTimelineRenderer: props.runtimeTimelineRenderer, timeline: props.timeline, runtimeDetails: props.runtimeDetails, onRuntimeTraceItemClick: (payload) => props.onRuntimeTraceItemClick?.(payload), runtimeDetailsRenderer: props.runtimeDetailsRenderer, onRuntimeDetailsOpen: (payload) => props.onRuntimeDetailsOpen?.(payload), runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer, onRuntimeTransitionDetailsOpen: (payload) => props.onRuntimeTransitionDetailsOpen?.(payload), onElementClick: (payload) => emit('element-click', payload), onTraceClick: (payload) => emit('trace-click', payload) }, slots: shellSlots, layout: props.layout, regions: props.regions, runtime: props.runtime, mode: props.mode, allowedModes: props.allowedModes, projection: props.projection, responsive: props.responsive, projectionOptions: props.projectionOptions, leftWidth: props.leftWidth, rightWidth: props.rightWidth, theme: props.theme, runtimeAppearance: props.runtimeAppearance, svgExport: props.svgExport, onThemeChange: props.onThemeChange });
192
+ mountedRightSlot = resolveRightSlot();
193
+ shellSlots.right = mountedRightSlot;
194
+ shell = new CoreStudioShell({ container: host.value, studio, config: props.config, iconRegistry: props.iconRegistry, paletteRegistry: props.paletteRegistry, propertiesRegistry: props.propertiesRegistry, templateRegistry: props.templateRegistry, contextMenuRegistry: props.contextMenuRegistry, rendererOptions: { nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, nodeSubtitleResolver, runtimePresenter: props.runtimePresenter, runtimeTraceProjector: props.runtimeTraceProjector, runtimeAssetResolver: props.runtimeAssetResolver, runtimeTraceOptions: props.runtimeTraceOptions, runtimeTimelineRenderer: props.runtimeTimelineRenderer, timeline: props.timeline, runtimeDetails: props.runtimeDetails, onRuntimeTraceItemClick: (payload) => props.onRuntimeTraceItemClick?.(payload), runtimeDetailsRenderer: props.runtimeDetailsRenderer, onRuntimeDetailsOpen: (payload) => props.onRuntimeDetailsOpen?.(payload), runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer, onRuntimeTransitionDetailsOpen: (payload) => props.onRuntimeTransitionDetailsOpen?.(payload), onElementClick: (payload) => emit('element-click', payload), onTraceClick: (payload) => emit('trace-click', payload) }, slots: shellSlots, layout: props.layout, regions: props.regions, runtime: props.runtime, mode: props.mode, allowedModes: props.allowedModes, projection: props.projection, responsive: props.responsive, projectionOptions: props.projectionOptions, leftWidth: props.leftWidth, rightWidth: props.rightWidth, timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions, theme: props.theme, runtimeAppearance: props.runtimeAppearance, svgExport: props.svgExport, onThemeChange: props.onThemeChange });
174
195
  actualMode.value = shell.getMode();
196
+ Object.assign(actualUi, shell.ui);
175
197
  offMode = shell.subscribeMode((event) => {
176
198
  actualMode.value = event.mode;
177
199
  emit('update:mode', event.mode);
178
200
  emit('mode-change', event);
179
201
  });
180
202
  offValidation = shell.subscribeValidation((event) => emit('validation', event));
203
+ panelSelection.value = shell.getPanelSelection();
204
+ offPanelSelection = shell.subscribePanelSelection((value) => { panelSelection.value = value; });
205
+ offSidebar = shell.subscribeSidebarChange((event) => emit('sidebar-change', event));
181
206
  off = studio.subscribe((event) => {
182
207
  Object.assign(state, event.state);
183
208
  if (event.type === 'modelChanged') emit('change', studio.model, event.reason, studio.exportXml());
@@ -185,6 +210,14 @@ export const BpmnStudio = defineComponent({
185
210
  if (event.type === 'scopeChanged') emit('scope-change', event.activeScopeId, event.scopePath, studio.getState());
186
211
  });
187
212
  });
213
+ // Vue slot membership is not reactive. Reconcile it after a parent update,
214
+ // keeping the same Teleport mount for ordinary slot-content changes.
215
+ onUpdated(() => {
216
+ const nextSlot = resolveRightSlot();
217
+ if (!shell || nextSlot === mountedRightSlot) return;
218
+ shell._setRightSlot(nextSlot);
219
+ mountedRightSlot = nextSlot;
220
+ });
188
221
  watch(() => props.model, (value) => { if (value && value !== studio.model) studio.setModel(value); });
189
222
  watch(() => props.xml, (value) => {
190
223
  if (value && value !== studio.exportXml(props.engine)) studio.importXml(value, props.engine);
@@ -196,19 +229,47 @@ export const BpmnStudio = defineComponent({
196
229
  if (mode) shell.setMode(mode);
197
230
  }, { deep: true });
198
231
  watch(() => props.projection, (value) => { if (value) shell?.setProjection(value); });
232
+ watch(() => props.config, (value) => {
233
+ if (!shell) return;
234
+ shell.setConfig(value);
235
+ Object.assign(actualUi, shell.ui);
236
+ }, { deep: true });
199
237
  watch(() => props.regions, (value) => { if (value !== undefined) shell?.setRegions(value); }, { deep: true });
200
238
  watch(() => props.theme, (value) => { if (value !== undefined) shell?.setTheme(value); });
201
239
  watch(() => props.runtimeAppearance, (value) => { if (value !== undefined) shell?.setRuntimeAppearance(value); });
202
240
  watch(() => props.nodeSubtitleResolver, () => shell?.refreshPresentation());
203
241
  watch(() => [props.timeline, props.runtimeDetails, props.runtimeTraceOptions], ([timeline, runtimeDetails, runtimeTraceOptions]) => {
204
242
  if (!shell) return;
243
+ if (timeline === undefined && runtimeDetails === undefined && runtimeTraceOptions === undefined) return;
205
244
  shell.rendererOptions.timeline = timeline;
206
245
  shell.rendererOptions.runtimeDetails = runtimeDetails;
207
246
  shell.rendererOptions.runtimeTraceOptions = runtimeTraceOptions;
208
247
  shell.viewer?.setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions });
209
248
  });
210
- onBeforeUnmount(() => { offMode?.(); offValidation?.(); off?.(); shell?.destroy(); if (owned) studio.destroy(); });
211
- expose({ getStudio: () => studio, getShell: () => shell, getActions: () => shell?.actions || null, getMode: () => shell?.getMode(), getAllowedModes: () => shell?.getAllowedModes() || [], setMode: (value) => shell?.setMode(value) || false, setAllowedModes: (value) => shell?.setAllowedModes(value) || [], refreshPresentation: () => shell?.refreshPresentation(), validate: () => shell?.validate() || [], exportXml: (engine) => studio.exportXml(engine), exportSvg: (options) => shell?.exportSvg(options), openSvgExportPreview: (options) => shell?.openSvgExportPreview(options), fitView: () => shell?.fitView(), setTheme: (value) => shell?.setTheme(value) });
249
+ onBeforeUnmount(() => { offMode?.(); offValidation?.(); offSidebar?.(); offPanelSelection?.(); off?.(); shell?.destroy(); if (owned) studio.destroy(); });
250
+ expose({
251
+ getStudio: () => studio,
252
+ getShell: () => shell,
253
+ getActions: () => shell?.actions || null,
254
+ getConfig: () => shell?.getConfig(),
255
+ setConfig: (value) => { const result = shell?.setConfig(value); if (shell) Object.assign(actualUi, shell.ui); return result; },
256
+ getMode: () => shell?.getMode(),
257
+ getAllowedModes: () => shell?.getAllowedModes() || [],
258
+ setMode: (value) => shell?.setMode(value) || false,
259
+ setAllowedModes: (value) => shell?.setAllowedModes(value) || [],
260
+ getSidebarState: () => shell?.getSidebarState(),
261
+ setSidebarCollapsed: (side, collapsed) => shell?.setSidebarCollapsed(side, collapsed) || false,
262
+ subscribeSidebarChange: (listener) => shell?.subscribeSidebarChange(listener) || (() => {}),
263
+ getPanelSelection: () => shell?.getPanelSelection(),
264
+ subscribePanelSelection: (listener) => shell?.subscribePanelSelection(listener) || (() => {}),
265
+ refreshPresentation: () => shell?.refreshPresentation(),
266
+ validate: () => shell?.validate() || [],
267
+ exportXml: (engine) => studio.exportXml(engine),
268
+ exportSvg: (options) => shell?.exportSvg(options),
269
+ openSvgExportPreview: (options) => shell?.openSvgExportPreview(options),
270
+ fitView: () => shell?.fitView(),
271
+ setTheme: (value) => shell?.setTheme(value),
272
+ });
212
273
  const renderSlot = (name, target, contextRef) => {
213
274
  if (!slots[name] || !target.value || !contextRef.value) return null;
214
275
  return h(Teleport, { to: target.value }, slots[name]({
@@ -217,6 +278,8 @@ export const BpmnStudio = defineComponent({
217
278
  actions: contextRef.value.actions,
218
279
  state,
219
280
  mode: actualMode.value,
281
+ ui: readonlyUi,
282
+ panelSelection: panelSelection.value,
220
283
  }));
221
284
  };
222
285
  return () => h(Fragment, null, [
@@ -224,6 +287,7 @@ export const BpmnStudio = defineComponent({
224
287
  renderSlot('header', headerTarget, headerContext),
225
288
  renderSlot('header-start', headerStartTarget, headerStartContext),
226
289
  renderSlot('header-actions', headerActionsTarget, headerActionsContext),
290
+ renderSlot('right', rightTarget, rightContext),
227
291
  ]);
228
292
  },
229
293
  });
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
+