@bpmn-nova/react 0.3.3-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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ BPMN Nova 的 React 18+ Adapter,提供 Studio、Designer、Viewer、审批轨
4
4
 
5
5
  > **English summary:** React 18+ components and refs for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
6
6
 
7
- > 当前版本为 `0.3.3-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
7
+ > 当前版本为 `0.3.5-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
8
8
 
9
9
  ![BPMN Nova React 流程工作台](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/docs/assets/bpmn-nova-designer.jpg)
10
10
 
@@ -20,6 +20,20 @@ import '@bpmn-nova/react/styles.css'
20
20
 
21
21
  只安装本包;`@bpmn-nova/studio` 会作为内部依赖自动解析。容器必须有明确高度。
22
22
 
23
+ TypeScript 类型也从 React 入口导入,不要为了模型类型额外声明 Studio 直依赖:
24
+
25
+ ```ts
26
+ import type {
27
+ BpmnNode,
28
+ EdgeType,
29
+ ElementSelection,
30
+ NodeType,
31
+ ProcessModel,
32
+ } from '@bpmn-nova/react'
33
+ ```
34
+
35
+ 公共类型门面还包含 `BpmnEdge`、`EngineId` 和 `LayoutOptions`。
36
+
23
37
  使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
24
38
 
25
39
  ## 流程设计
@@ -29,6 +43,8 @@ import { useRef } from 'react'
29
43
  import { BpmnStudio } from '@bpmn-nova/react'
30
44
  import '@bpmn-nova/react/styles.css'
31
45
 
46
+ const STUDIO_CONFIG = { ui: { controlSize: 'medium' } }
47
+
32
48
  export function WorkflowEditor({ initialXml }) {
33
49
  const studioRef = useRef(null)
34
50
 
@@ -38,6 +54,7 @@ export function WorkflowEditor({ initialXml }) {
38
54
  ref={studioRef}
39
55
  xml={initialXml}
40
56
  engine="flowable"
57
+ config={STUDIO_CONFIG}
41
58
  mode="design"
42
59
  allowedModes={['design']}
43
60
  theme="auto"
@@ -56,7 +73,7 @@ Ref 提供 `actions`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`
56
73
 
57
74
  ## 组合宿主头部与业务属性面板
58
75
 
59
- 默认 `BpmnStudio` 是完整工作台。通过 `regions={{ right: 'hidden' }}` 隐藏 Nova Properties 后,Canvas 会获得该轨道的全部宽度;宿主属性面板应作为外部兄弟区域渲染。
76
+ 默认 `BpmnStudio` 是完整工作台。下面展示宿主拥有外部面板的接法:通过 `config.ui.regions.right = 'hidden'` 隐藏 Nova PropertiesCanvas 获得该轨道的全部宽度。若希望复用 Nova 侧栏布局,使用下文的 `right` Render Prop,不必放到外部。
60
77
 
61
78
  ```jsx
62
79
  <BpmnStudio
@@ -65,14 +82,15 @@ Ref 提供 `actions`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`
65
82
  engine="activiti"
66
83
  mode="design"
67
84
  allowedModes={['design']}
68
- allowedNodeTypes={supportedNodeTypes}
69
- allowedEdgeTypes={['sequenceFlow']}
70
- regions={{ right: 'hidden' }}
85
+ config={{
86
+ modeling: { allowedNodeTypes: supportedNodeTypes, allowedEdgeTypes: ['sequenceFlow'] },
87
+ ui: { controlSize: 'medium', regions: { right: 'hidden' } },
88
+ }}
71
89
  theme="auto"
72
90
  headerStart={({ state, actions }) => (
73
91
  <HostProcessIdentity onBack={back} />
74
92
  )}
75
- headerActions={({ actions, mode }) => (
93
+ headerActions={({ actions, mode, ui }) => (
76
94
  <HostWorkflowActions
77
95
  disabled={mode !== 'design'}
78
96
  onValidate={() => actions.validate()}
@@ -102,6 +120,52 @@ async function publishProcess(actions) {
102
120
 
103
121
  宿主属性面板以 `onSelectionChange` 为选择状态来源;它覆盖节点、连线、多选、清空与键盘选择。`onElementClick` 只是点击观察事件,不能代替选择状态。宿主通过稳定 BPMN Element ID 关联业务配置,并自行负责保存、发布、权限与服务端事务。
104
122
 
123
+ `config` 收拢 `modeling/ui/viewer/export` 静态选项。顶部控件尺寸使用 `config.ui.controlSize`,默认 `medium`,支持 `small`、`medium`、`large` 和 `24px`–`48px`。Render Context 的只读 `ui.controlHeight` 可交给宿主按钮;Element Plus `default`、Ant Design `middle` 都映射到 Nova `medium`。自定义 px 时也可以在宿主 Header Actions CSS 中读取 `--nova-control-height` 等公开变量。
124
+
125
+ ```jsx
126
+ <BpmnStudio
127
+ ref={studioRef}
128
+ config={{ ui: { controlSize: '36px' } }}
129
+ headerActions={({ actions, ui }) => (
130
+ <HostActions actions={actions} buttonHeight={ui.controlHeight} />
131
+ )}
132
+ />
133
+ ```
134
+
135
+ Ref 同步提供 `getConfig()` / `setConfig()`;运行时更新 UI、Viewer 或 Export 不重建 Canvas。旧顶层配置继续兼容并标记 Deprecated,同项冲突时显式旧值优先。`config.modeling` 只在内部 Controller 创建时建立节点和连线约束。
136
+
137
+ ## 原生右侧内容与折叠
138
+
139
+ `right` Render Prop 仅替换侧栏内容,保留 Nova 的布局与折叠按钮;它高于 Core `slots.right`,通过 Portal 保留宿主 Context。不要同时配置 `regions.right: 'hidden'`,否则自定义内容也隐藏。
140
+
141
+ ```jsx
142
+ <BpmnStudio
143
+ ref={studioRef}
144
+ xml={xml}
145
+ config={{ ui: {
146
+ leftPanel: { collapsible: true, defaultCollapsed: false },
147
+ rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
148
+ } }}
149
+ onSidebarChange={(event) => console.log(event.side, event.collapsed, event.source)}
150
+ right={({ panelSelection, mode }) => (
151
+ <>
152
+ <header style={{ flex: 'none', padding: 12 }}>业务详情 · {mode}</header>
153
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: 12 }}>
154
+ {panelSelection.selectedElement?.id ?? '未选择元素'}
155
+ {/* 填入宿主表单 */}
156
+ </div>
157
+ <footer style={{ flex: 'none', padding: 12 }}>宿主固定操作区</footer>
158
+ </>
159
+ )}
160
+ />
161
+ ```
162
+
163
+ 默认 `flex` 由宿主安排内部滚动;`scroll` 由 Nova 滚动整个内容,宿主只填普通内容。父容器仍需可计算高度。收起不卸载内容、不丢失表单/滚动状态、不自动 Fit;左右按钮位于分隔线中点,支持键盘和尊重减少动态效果的 200ms 动画。Shell 容器 ≤720px 时首次默认收起,保留按钮,展开挤压画布而非抽屉,宽窄屏分别记忆状态。
164
+
165
+ Ref 增加 `getSidebarState()`、`setSidebarCollapsed(side, collapsed)`、`subscribeSidebarChange()`、`getPanelSelection()`、`subscribePanelSelection()`;设置仅在实际切换时返回 `true`。Header/Right Render Context 共享响应式 `panelSelection`(`selection`、`selectedElement`、`trace`),只读轨迹选择不回写设计选择。
166
+
167
+ Instance 默认隐藏右侧且没有展开按钮;需要时显式设置 `config.ui.regions.right: 'default'`。新增能力自 `0.3.5-preview` 提供。完整示例见[侧栏指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/CUSTOMIZATION.md#侧栏布局与平滑折叠)或包内 `llms-full.txt`。
168
+
105
169
  ## Mode 双向同步与副标题刷新
106
170
 
107
171
  ```jsx
@@ -138,6 +202,8 @@ Ref 的 `mode` Getter 与 `setMode()` 始终反映 Shell 实际状态。Resolver
138
202
 
139
203
  ## 只读展示与审批轨迹
140
204
 
205
+ 真实后端的数据格式与接入步骤见[审批轨迹接入指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/RUNTIME-INTEGRATION.md),完整正文也包含在包内 `llms-full.txt`。React 根入口的 `ProcessInstanceSnapshot`、Activity/Action/Transition、`RuntimeTraceClickEvent` 与资源 Resolver 等 14 个 Runtime 类型出口自 `0.3.5-preview` 提供;旧版可按指南从 `BpmnStudioProps['runtime']` 推导,无需增加 Studio 直依赖。
206
+
141
207
  ```jsx
142
208
  import { BpmnViewer } from '@bpmn-nova/react'
143
209
  import '@bpmn-nova/react/styles.css'
@@ -268,6 +334,7 @@ studioRef.current?.openSvgExportPreview({
268
334
  - [完整项目能力](https://github.com/daxiangme/bpmn-nova)
269
335
  - [React/Vue 组件说明](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/COMPONENTS.md)
270
336
  - [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
337
+ - [审批轨迹接入指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/RUNTIME-INTEGRATION.md)
271
338
 
272
339
  ## License
273
340
 
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ import type { ProcessInstanceSnapshot } from '@bpmn-nova/studio/runtime'
25
25
  import type { SvgExportArtifact, SvgExportOptions, SvgExportPreviewController } from '@bpmn-nova/studio/export-svg'
26
26
  import type {
27
27
  BpmnCanvas as CanvasInstance,
28
+ BpmnStudioConfig,
28
29
  BpmnStudioController,
29
30
  BpmnStudioShell,
30
31
  ContextMenuRegistry,
@@ -34,12 +35,17 @@ import type {
34
35
  StudioShellActions,
35
36
  StudioMode,
36
37
  StudioModeChangeEvent,
38
+ StudioPanelSelection,
39
+ StudioSidebarChangeEvent,
40
+ StudioSidebarSide,
41
+ StudioSidebarState,
37
42
  StudioValidationEvent,
38
43
  StudioValidationIssue,
39
44
  StudioShellOptions,
40
45
  StudioShellRegions,
41
46
  StudioShellSlots,
42
47
  StudioState,
48
+ StudioUiContext,
43
49
  TemplateRegistry,
44
50
  } from '@bpmn-nova/studio'
45
51
  import type { NovaThemeInput, NovaThemeState, RuntimeAppearanceOptions } from '@bpmn-nova/studio/theme'
@@ -53,6 +59,16 @@ import type {
53
59
  ViewerProjection,
54
60
  } from '@bpmn-nova/studio/viewer'
55
61
 
62
+ export type {
63
+ BpmnEdge,
64
+ BpmnNode,
65
+ EdgeType,
66
+ ElementSelection,
67
+ EngineId,
68
+ LayoutOptions,
69
+ NodeType,
70
+ ProcessModel,
71
+ } from '@bpmn-nova/studio'
56
72
  export {
57
73
  createContextMenuRegistry,
58
74
  createDefaultContextMenuRegistry,
@@ -73,13 +89,47 @@ export {
73
89
  registerFlowableProperties,
74
90
  } from '@bpmn-nova/studio'
75
91
  export type {
92
+ BpmnStudioConfig,
76
93
  NodeSubtitleResolver,
77
94
  NodeSubtitleResolverContext,
95
+ StudioControlSize,
96
+ StudioModelingConfig,
97
+ StudioPanelSelection,
98
+ StudioRightPanelConfig,
99
+ StudioSidebarChangeEvent,
100
+ StudioSidebarChangeSource,
101
+ StudioSidebarConfig,
102
+ StudioSidebarSide,
103
+ StudioSidebarState,
104
+ StudioSvgExportOptions,
105
+ StudioUiConfig,
106
+ StudioUiContext,
78
107
  StudioValidationEvent,
79
108
  StudioValidationIssue,
80
109
  StudioValidationSource,
110
+ StudioViewerConfig,
81
111
  } from '@bpmn-nova/studio'
82
112
 
113
+ /** Runtime integration facade. Added in 0.3.5-preview; no runtime JavaScript exports. */
114
+ export type {
115
+ ActivityInstance,
116
+ ActivityStatus,
117
+ ProcessInstanceSnapshot,
118
+ RuntimeApprovalAction,
119
+ RuntimeApprovalActionType,
120
+ RuntimeApprovalContent,
121
+ RuntimeApprovalContentBlock,
122
+ RuntimeAssetRef,
123
+ RuntimeEdgeVisit,
124
+ RuntimeParticipant,
125
+ RuntimeTransition,
126
+ } from '@bpmn-nova/studio/runtime'
127
+ export type {
128
+ RuntimeAssetPurpose,
129
+ RuntimeAssetResolver,
130
+ RuntimeTraceClickEvent,
131
+ } from '@bpmn-nova/studio/viewer'
132
+
83
133
  export interface BpmnNovaVisualProps {
84
134
  theme?: NovaThemeInput
85
135
  runtimeAppearance?: RuntimeAppearanceOptions
@@ -94,8 +144,12 @@ export interface BpmnModelProps {
94
144
  }
95
145
  export interface UseBpmnStudioOptions extends BpmnModelProps {
96
146
  studio?: BpmnStudioController
147
+ config?: BpmnStudioConfig
148
+ /** @deprecated Use config.modeling.propertiesProfile. */
97
149
  propertiesProfile?: 'business' | 'developer'
150
+ /** @deprecated Use config.modeling.allowedNodeTypes. */
98
151
  allowedNodeTypes?: NodeType[]
152
+ /** @deprecated Use config.modeling.allowedEdgeTypes. */
99
153
  allowedEdgeTypes?: EdgeType[]
100
154
  }
101
155
  export interface UseBpmnStudioResult { studio: BpmnStudioController; state: StudioState; commands: StudioCommands }
@@ -142,8 +196,12 @@ export const BpmnPalettePanel: ForwardRefExoticComponent<BpmnPalettePanelProps &
142
196
 
143
197
  export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
144
198
  studio?: BpmnStudioController
199
+ config?: BpmnStudioConfig
200
+ /** @deprecated Use config.modeling.propertiesProfile. */
145
201
  propertiesProfile?: 'business' | 'developer'
202
+ /** @deprecated Use config.modeling.allowedNodeTypes. */
146
203
  allowedNodeTypes?: NodeType[]
204
+ /** @deprecated Use config.modeling.allowedEdgeTypes. */
147
205
  allowedEdgeTypes?: EdgeType[]
148
206
  iconRegistry?: IconRegistry
149
207
  paletteRegistry?: PaletteRegistry
@@ -155,25 +213,35 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
155
213
  nodeSubtitleResolver?: NodeSubtitleResolver
156
214
  slots?: StudioShellSlots
157
215
  layout?: StudioShellOptions['layout']
216
+ /** @deprecated Use config.ui.regions. */
158
217
  regions?: StudioShellRegions
159
218
  headerStart?: BpmnStudioRenderSlot
160
219
  headerActions?: BpmnStudioRenderSlot
161
220
  header?: BpmnStudioRenderSlot
221
+ right?: BpmnStudioRenderSlot
162
222
  runtime?: ProcessInstanceSnapshot | null
163
223
  mode?: StudioMode
164
224
  allowedModes?: readonly StudioMode[]
165
225
  projection?: ViewerProjection
226
+ /** @deprecated Use config.viewer.responsive. */
166
227
  responsive?: boolean
228
+ /** @deprecated Use config.viewer.projectionOptions. */
167
229
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
230
+ /** @deprecated Use config.ui.sidebarWidth.left. */
168
231
  leftWidth?: number
232
+ /** @deprecated Use config.ui.sidebarWidth.right. */
169
233
  rightWidth?: number
234
+ /** @deprecated Use config.viewer.timeline. */
170
235
  timeline?: ViewerOptions['timeline']
236
+ /** @deprecated Use config.viewer.runtimeDetails. */
171
237
  runtimeDetails?: ViewerOptions['runtimeDetails']
238
+ /** @deprecated Use config.viewer.runtimeTraceOptions. */
172
239
  runtimeTraceOptions?: ViewerOptions['runtimeTraceOptions']
173
240
  runtimePresenter?: ViewerOptions['runtimePresenter']
174
241
  runtimeTraceProjector?: ViewerOptions['runtimeTraceProjector']
175
242
  runtimeAssetResolver?: ViewerOptions['runtimeAssetResolver']
176
243
  runtimeTimelineRenderer?: RuntimeTimelineRenderer
244
+ /** @deprecated Use config.export. */
177
245
  svgExport?: ViewerOptions['svgExport']
178
246
  runtimeDetailsRenderer?: RuntimeDetailsRenderer | null
179
247
  runtimeTransitionDetailsRenderer?: RuntimeTransitionDetailsRenderer | null
@@ -187,6 +255,7 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
187
255
  onScopeChange?: (activeScopeId: string, scopePath: StudioState['scopePath'], state: StudioState) => void
188
256
  onModeChange?: (event: StudioModeChangeEvent) => void
189
257
  onValidation?: (event: StudioValidationEvent) => void
258
+ onSidebarChange?: (event: StudioSidebarChangeEvent) => void
190
259
  }
191
260
  export interface BpmnStudioRenderContext {
192
261
  readonly studio: BpmnStudioController
@@ -194,6 +263,8 @@ export interface BpmnStudioRenderContext {
194
263
  readonly actions: StudioShellActions
195
264
  readonly state: Readonly<StudioState>
196
265
  readonly mode: StudioMode
266
+ readonly ui: StudioUiContext
267
+ readonly panelSelection: StudioPanelSelection
197
268
  }
198
269
  export type BpmnStudioRenderSlot = ReactNode | ((context: BpmnStudioRenderContext) => ReactNode)
199
270
  export interface BpmnStudioHandle {
@@ -201,10 +272,17 @@ export interface BpmnStudioHandle {
201
272
  readonly shell: BpmnStudioShell | null
202
273
  readonly actions: StudioShellActions | null
203
274
  readonly mode: StudioMode | undefined
275
+ getConfig(): Readonly<BpmnStudioConfig> | undefined
276
+ setConfig(config?: BpmnStudioConfig): Readonly<BpmnStudioConfig> | undefined
204
277
  getMode(): StudioMode | undefined
205
278
  getAllowedModes(): readonly StudioMode[]
206
279
  setMode(mode: StudioMode): boolean
207
280
  setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
281
+ getSidebarState(): StudioSidebarState | undefined
282
+ setSidebarCollapsed(side: StudioSidebarSide, collapsed: boolean): boolean
283
+ subscribeSidebarChange(listener: (event: StudioSidebarChangeEvent) => void): () => void
284
+ getPanelSelection(): StudioPanelSelection | undefined
285
+ subscribePanelSelection(listener: (selection: StudioPanelSelection) => void): () => void
208
286
  refreshPresentation(): void
209
287
  validate(): StudioValidationIssue[]
210
288
  exportXml(engine?: EngineId): string
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ function resolveModel({ model, xml, engine = 'flowable' }) {
51
51
 
52
52
  export function useBpmnStudio(options = {}) {
53
53
  const ownedRef = useRef(null);
54
- if (!options.studio && !ownedRef.current) ownedRef.current = createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
54
+ if (!options.studio && !ownedRef.current) ownedRef.current = createStudioController({ model: resolveModel(options), config: options.config, propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
55
55
  const studio = options.studio || ownedRef.current;
56
56
  const [state, setState] = useState(() => studio.getState());
57
57
  useEffect(() => studio.subscribe((event) => setState(event.state)), [studio]);
@@ -111,12 +111,21 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
111
111
  const [headerPortal, setHeaderPortal] = useState(null);
112
112
  const [headerStartPortal, setHeaderStartPortal] = useState(null);
113
113
  const [headerActionsPortal, setHeaderActionsPortal] = useState(null);
114
+ const [rightPortal, setRightPortal] = useState(null);
115
+ const nativeRightSlotRef = useRef(null);
116
+ if (!nativeRightSlotRef.current) nativeRightSlotRef.current = (context) => {
117
+ const portal = { target: context.container, context };
118
+ setRightPortal(portal);
119
+ return () => setRightPortal((current) => current === portal ? null : current);
120
+ };
114
121
  const callbacksRef = useRef({});
115
122
  callbacksRef.current = props;
116
- if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
123
+ if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), config: props.config, propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
117
124
  const studio = props.studio || ownedStudioRef.current;
118
125
  const [studioState, setStudioState] = useState(() => studio.getState());
119
126
  const [actualMode, setActualMode] = useState(() => props.mode || props.allowedModes?.[0] || 'design');
127
+ const [actualUi, setActualUi] = useState(() => Object.freeze({ controlSize: 'medium', controlHeight: '32px' }));
128
+ const [panelSelection, setPanelSelection] = useState(() => Object.freeze({ selection: studio.selection, selectedElement: studio.getSelectedElement(), trace: null }));
120
129
  const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
121
130
  subtitleResolverRef.current = props.nodeSubtitleResolver;
122
131
  const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
@@ -124,11 +133,22 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
124
133
  studio,
125
134
  get shell() { return shellRef.current; },
126
135
  get actions() { return shellRef.current?.actions || null; },
136
+ getConfig: () => shellRef.current?.getConfig(),
137
+ setConfig: (config) => {
138
+ const result = shellRef.current?.setConfig(config);
139
+ if (shellRef.current) setActualUi(Object.freeze({ ...shellRef.current.ui }));
140
+ return result;
141
+ },
127
142
  get mode() { return shellRef.current?.getMode(); },
128
143
  getMode: () => shellRef.current?.getMode(),
129
144
  getAllowedModes: () => shellRef.current?.getAllowedModes() || [],
130
145
  setMode: (mode) => shellRef.current?.setMode(mode) || false,
131
146
  setAllowedModes: (modes) => shellRef.current?.setAllowedModes(modes) || [],
147
+ getSidebarState: () => shellRef.current?.getSidebarState(),
148
+ setSidebarCollapsed: (side, collapsed) => shellRef.current?.setSidebarCollapsed(side, collapsed) || false,
149
+ subscribeSidebarChange: (listener) => shellRef.current?.subscribeSidebarChange(listener) || (() => {}),
150
+ getPanelSelection: () => shellRef.current?.getPanelSelection(),
151
+ subscribePanelSelection: (listener) => shellRef.current?.subscribePanelSelection(listener) || (() => {}),
132
152
  refreshPresentation: () => shellRef.current?.refreshPresentation(),
133
153
  validate: () => shellRef.current?.validate() || [],
134
154
  exportXml: (engine) => studio.exportXml(engine),
@@ -140,6 +160,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
140
160
  const hasNativeHeader = props.header !== undefined && props.header !== null;
141
161
  const hasNativeHeaderStart = props.headerStart !== undefined && props.headerStart !== null;
142
162
  const hasNativeHeaderActions = props.headerActions !== undefined && props.headerActions !== null;
163
+ const hasNativeRight = props.right !== undefined && props.right !== null;
143
164
  useEffect(() => {
144
165
  if (!hostRef.current) return undefined;
145
166
  setStudioState(studio.getState());
@@ -154,9 +175,11 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
154
175
  if (hasNativeHeaderStart) shellSlots.headerStart = createPortalSlot(setHeaderStartPortal);
155
176
  if (hasNativeHeaderActions) shellSlots.headerActions = createPortalSlot(setHeaderActionsPortal);
156
177
  }
178
+ if (hasNativeRight) shellSlots.right = nativeRightSlotRef.current;
157
179
  const shell = new CoreStudioShell({
158
180
  container: hostRef.current,
159
181
  studio,
182
+ config: props.config,
160
183
  iconRegistry: props.iconRegistry,
161
184
  paletteRegistry: props.paletteRegistry,
162
185
  propertiesRegistry: props.propertiesRegistry,
@@ -192,6 +215,9 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
192
215
  projectionOptions: props.projectionOptions,
193
216
  leftWidth: props.leftWidth,
194
217
  rightWidth: props.rightWidth,
218
+ timeline: props.timeline,
219
+ runtimeDetails: props.runtimeDetails,
220
+ runtimeTraceOptions: props.runtimeTraceOptions,
195
221
  theme: props.theme,
196
222
  runtimeAppearance: props.runtimeAppearance,
197
223
  svgExport: props.svgExport,
@@ -199,19 +225,28 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
199
225
  });
200
226
  shellRef.current = shell;
201
227
  setActualMode(shell.getMode());
228
+ setActualUi(Object.freeze({ ...shell.ui }));
202
229
  const offMode = shell.subscribeMode((event) => {
203
230
  setActualMode(event.mode);
204
231
  callbacksRef.current.onModeChange?.(event);
205
232
  });
206
233
  const offValidation = shell.subscribeValidation((event) => callbacksRef.current.onValidation?.(event));
234
+ setPanelSelection(shell.getPanelSelection());
235
+ const offPanelSelection = shell.subscribePanelSelection(setPanelSelection);
236
+ const offSidebar = shell.subscribeSidebarChange((event) => callbacksRef.current.onSidebarChange?.(event));
207
237
  const off = studio.subscribe((event) => {
208
238
  setStudioState(event.state);
209
239
  if (event.type === 'modelChanged') callbacksRef.current.onChange?.(studio.model, event.reason, studio.exportXml());
210
240
  if (event.type === 'selectionChanged') callbacksRef.current.onSelectionChange?.(studio.selection, studio.getSelectedElement());
211
241
  if (event.type === 'scopeChanged') callbacksRef.current.onScopeChange?.(event.activeScopeId, event.scopePath, studio.getState());
212
242
  });
213
- return () => { offMode(); offValidation(); off(); shell.destroy(); shellRef.current = null; };
243
+ return () => { offMode(); offValidation(); offPanelSelection(); offSidebar(); off(); shell.destroy(); shellRef.current = null; };
214
244
  }, [studio, hasNativeHeader, hasNativeHeaderStart, hasNativeHeaderActions]);
245
+ // The content render function may change each render; only slot ownership
246
+ // changes replace the right mount. Neither case reconstructs the Shell.
247
+ useEffect(() => {
248
+ shellRef.current?._setRightSlot(hasNativeRight ? nativeRightSlotRef.current : props.slots?.right);
249
+ }, [studio, hasNativeRight, props.slots?.right, hasNativeHeader, hasNativeHeaderStart, hasNativeHeaderActions]);
215
250
  useEffect(() => () => {
216
251
  if (!props.studio && ownedStudioRef.current === studio) {
217
252
  studio.destroy();
@@ -234,6 +269,12 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
234
269
  if (props.mode) shell.setMode(props.mode);
235
270
  }, [props.allowedModes, props.mode]);
236
271
  useEffect(() => { if (props.projection) shellRef.current?.setProjection(props.projection); }, [props.projection]);
272
+ useEffect(() => {
273
+ const shell = shellRef.current;
274
+ if (!shell) return;
275
+ shell.setConfig(props.config);
276
+ setActualUi(Object.freeze({ ...shell.ui }));
277
+ }, [props.config]);
237
278
  useEffect(() => { if (props.regions !== undefined) shellRef.current?.setRegions(props.regions); }, [props.regions]);
238
279
  useEffect(() => { if (props.theme !== undefined) shellRef.current?.setTheme(props.theme); }, [props.theme]);
239
280
  useEffect(() => { if (props.runtimeAppearance !== undefined) shellRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
@@ -241,6 +282,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
241
282
  useEffect(() => {
242
283
  const shell = shellRef.current;
243
284
  if (!shell) return;
285
+ if (props.timeline === undefined && props.runtimeDetails === undefined && props.runtimeTraceOptions === undefined) return;
244
286
  shell.rendererOptions.timeline = props.timeline;
245
287
  shell.rendererOptions.runtimeDetails = props.runtimeDetails;
246
288
  shell.rendererOptions.runtimeTraceOptions = props.runtimeTraceOptions;
@@ -254,6 +296,8 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
254
296
  actions: portal.context.actions,
255
297
  state: studioState,
256
298
  mode: actualMode,
299
+ ui: actualUi,
300
+ panelSelection,
257
301
  };
258
302
  return createPortal(typeof slot === 'function' ? slot(context) : slot, portal.target, key);
259
303
  };
@@ -262,6 +306,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
262
306
  renderSlot(props.header, headerPortal, 'bpmn-nova-header'),
263
307
  renderSlot(props.headerStart, headerStartPortal, 'bpmn-nova-header-start'),
264
308
  renderSlot(props.headerActions, headerActionsPortal, 'bpmn-nova-header-actions'),
309
+ renderSlot(props.right, rightPortal, 'bpmn-nova-right'),
265
310
  );
266
311
  });
267
312
 
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
+