@bpmn-nova/react 0.3.1-preview → 0.3.3-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 +97 -6
- package/dist/index.d.ts +78 -6
- package/dist/index.js +122 -11
- package/dist/styles.css +12 -3
- package/llms-full.txt +3322 -0
- package/llms.txt +236 -0
- package/package.json +5 -3
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.
|
|
7
|
+
> 当前版本为 `0.3.3-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
@@ -20,6 +20,8 @@ import '@bpmn-nova/react/styles.css'
|
|
|
20
20
|
|
|
21
21
|
只安装本包;`@bpmn-nova/studio` 会作为内部依赖自动解析。容器必须有明确高度。
|
|
22
22
|
|
|
23
|
+
使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
|
|
24
|
+
|
|
23
25
|
## 流程设计
|
|
24
26
|
|
|
25
27
|
```jsx
|
|
@@ -27,16 +29,17 @@ import { useRef } from 'react'
|
|
|
27
29
|
import { BpmnStudio } from '@bpmn-nova/react'
|
|
28
30
|
import '@bpmn-nova/react/styles.css'
|
|
29
31
|
|
|
30
|
-
export function WorkflowEditor({
|
|
32
|
+
export function WorkflowEditor({ initialXml }) {
|
|
31
33
|
const studioRef = useRef(null)
|
|
32
34
|
|
|
33
35
|
return (
|
|
34
36
|
<div style={{ height: 720 }}>
|
|
35
37
|
<BpmnStudio
|
|
36
38
|
ref={studioRef}
|
|
37
|
-
xml={
|
|
39
|
+
xml={initialXml}
|
|
38
40
|
engine="flowable"
|
|
39
41
|
mode="design"
|
|
42
|
+
allowedModes={['design']}
|
|
40
43
|
theme="auto"
|
|
41
44
|
onChange={(model, reason, nextXml) => {
|
|
42
45
|
console.log(reason, nextXml)
|
|
@@ -47,7 +50,91 @@ export function WorkflowEditor({ xml }) {
|
|
|
47
50
|
}
|
|
48
51
|
```
|
|
49
52
|
|
|
50
|
-
Ref 提供 `exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
|
|
53
|
+
Ref 提供 `actions`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
|
|
54
|
+
|
|
55
|
+
`xml`/`model` 是外部替换输入,`onChange` 的 XML 是草稿输出。宿主回写完全相同的导出 XML 不会重新导入或清空撤销历史;切换服务器 revision 或流程时传入不同 XML 会替换模型。Activiti 宿主应显式使用 `engine="activiti"`。
|
|
56
|
+
|
|
57
|
+
## 组合宿主头部与业务属性面板
|
|
58
|
+
|
|
59
|
+
默认 `BpmnStudio` 是完整工作台。通过 `regions={{ right: 'hidden' }}` 隐藏 Nova Properties 后,Canvas 会获得该轨道的全部宽度;宿主属性面板应作为外部兄弟区域渲染。
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
<BpmnStudio
|
|
63
|
+
ref={studioRef}
|
|
64
|
+
xml={xml}
|
|
65
|
+
engine="activiti"
|
|
66
|
+
mode="design"
|
|
67
|
+
allowedModes={['design']}
|
|
68
|
+
allowedNodeTypes={supportedNodeTypes}
|
|
69
|
+
allowedEdgeTypes={['sequenceFlow']}
|
|
70
|
+
regions={{ right: 'hidden' }}
|
|
71
|
+
theme="auto"
|
|
72
|
+
headerStart={({ state, actions }) => (
|
|
73
|
+
<HostProcessIdentity onBack={back} />
|
|
74
|
+
)}
|
|
75
|
+
headerActions={({ actions, mode }) => (
|
|
76
|
+
<HostWorkflowActions
|
|
77
|
+
disabled={mode !== 'design'}
|
|
78
|
+
onValidate={() => actions.validate()}
|
|
79
|
+
onSave={() => saveDraft(actions.exportXml())}
|
|
80
|
+
onPublish={() => publishProcess(actions)}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
onChange={handleChange}
|
|
84
|
+
onValidation={(event) => handleValidation(event)}
|
|
85
|
+
onSelectionChange={(selection, element) => {
|
|
86
|
+
setSelectedElement(selection ? element : null)
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`headerStart` 只替换 Nova Brand;`headerActions` 只替换默认“校验 / 导入 / 导出”动作组,适合组合“校验 / 保存 / 发布”;`header` 可完整替换 Header。它们都通过 React Portal 在当前组件树内渲染,保留 Context 和生命周期。默认 Header 的最佳视图只保留在底部缩放区。
|
|
92
|
+
|
|
93
|
+
`actions.validate()` 以 `toolbar` 来源先更新 Nova 默认状态栏,再调用 `onValidation` 并返回 issues;Ref `validate()` 使用 `api` 来源。事件的 `valid` 仅由 error 决定,warning 会保留在 `issues` 中但不默认阻止发布。保存与发布仍由宿主负责。
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
async function publishProcess(actions) {
|
|
97
|
+
const issues = actions.validate()
|
|
98
|
+
if (issues.some((issue) => issue.level === 'error')) return
|
|
99
|
+
await publishXml(actions.exportXml())
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
宿主属性面板以 `onSelectionChange` 为选择状态来源;它覆盖节点、连线、多选、清空与键盘选择。`onElementClick` 只是点击观察事件,不能代替选择状态。宿主通过稳定 BPMN Element ID 关联业务配置,并自行负责保存、发布、权限与服务端事务。
|
|
104
|
+
|
|
105
|
+
## Mode 双向同步与副标题刷新
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
import { useCallback, useRef, useState } from 'react'
|
|
109
|
+
|
|
110
|
+
const [mode, setMode] = useState('design')
|
|
111
|
+
const summaries = useRef(new Map())
|
|
112
|
+
const resolveSubtitle = useCallback(
|
|
113
|
+
({ node }) => summaries.current.has(node.id)
|
|
114
|
+
? summaries.current.get(node.id)
|
|
115
|
+
: undefined,
|
|
116
|
+
[],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
<BpmnStudio
|
|
120
|
+
ref={studioRef}
|
|
121
|
+
xml={xml}
|
|
122
|
+
mode={mode}
|
|
123
|
+
allowedModes={['design', 'viewer']}
|
|
124
|
+
onModeChange={(event) => setMode(event.mode)}
|
|
125
|
+
nodeSubtitleResolver={resolveSubtitle}
|
|
126
|
+
headerStart={({ mode: actualMode }) => (
|
|
127
|
+
<HostHeader mode={actualMode} />
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
function updateArchiveSummary() {
|
|
132
|
+
summaries.current.set('ServiceTask_Archive', '归档到采购系统')
|
|
133
|
+
studioRef.current?.refreshPresentation()
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Ref 的 `mode` Getter 与 `setMode()` 始终反映 Shell 实际状态。Resolver 返回 `undefined` 保留默认值、`null` 移除副标题行、字符串替换显示;函数引用变化会自动刷新,稳定闭包内部数据变化需显式调用 `refreshPresentation()`。Resolver 只影响 Design/Viewer 标准卡片和 SVG,不覆盖 Instance Runtime Presentation,也不修改 XML、History、Selection 或 Viewport。
|
|
51
138
|
|
|
52
139
|
## 只读展示与审批轨迹
|
|
53
140
|
|
|
@@ -161,6 +248,10 @@ studioRef.current?.openSvgExportPreview({
|
|
|
161
248
|
- `createReactRuntimeDetailsComponent`
|
|
162
249
|
- `createReactRuntimeTransitionDetailsComponent`
|
|
163
250
|
- `createReactRuntimeTimelineComponent`
|
|
251
|
+
- `createStudioController`、`createEmptyProcess`、`importBpmn`、`exportBpmn`
|
|
252
|
+
- Palette、Properties、Context Menu、Icon 和 Template Registry 的公开创建函数
|
|
253
|
+
|
|
254
|
+
完整工作台可通过 `allowedNodeTypes` 与 `allowedEdgeTypes` 限制宿主支持的图元类型,通过 `allowedModes` 限制可切换的工作台模式。图元限制同时作用于 XML 导入、Palette、连接、快捷新增、模板和节点类型转换。
|
|
164
255
|
|
|
165
256
|
## 常见问题
|
|
166
257
|
|
|
@@ -172,11 +263,11 @@ studioRef.current?.openSvgExportPreview({
|
|
|
172
263
|
|
|
173
264
|
## 文档与 AI
|
|
174
265
|
|
|
266
|
+
- npm 包内 `llms.txt`:可执行安装入口
|
|
267
|
+
- npm 包内 `llms-full.txt`:完整 AI 上下文
|
|
175
268
|
- [完整项目能力](https://github.com/daxiangme/bpmn-nova)
|
|
176
269
|
- [React/Vue 组件说明](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/COMPONENTS.md)
|
|
177
270
|
- [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
|
|
178
|
-
- [AI 接入入口](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms.txt)
|
|
179
|
-
- [AI 完整上下文](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms-full.txt)
|
|
180
271
|
|
|
181
272
|
## License
|
|
182
273
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,12 +2,19 @@ import type {
|
|
|
2
2
|
ComponentType,
|
|
3
3
|
CSSProperties,
|
|
4
4
|
ForwardRefExoticComponent,
|
|
5
|
+
ReactNode,
|
|
5
6
|
RefAttributes,
|
|
6
7
|
} from 'react'
|
|
7
|
-
import type { BpmnEdge, BpmnNode, ElementSelection, EngineId, LayoutOptions, ProcessModel } from '@bpmn-nova/studio'
|
|
8
|
+
import type { BpmnEdge, BpmnNode, EdgeType, ElementSelection, EngineId, LayoutOptions, NodeType, ProcessModel } from '@bpmn-nova/studio'
|
|
8
9
|
import type { BpmnDesigner as DesignerInstance } from '@bpmn-nova/studio/designer'
|
|
9
10
|
import type { IconRegistry } from '@bpmn-nova/studio'
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
PaletteItemRenderer,
|
|
13
|
+
PalettePanel as PalettePanelInstance,
|
|
14
|
+
PaletteProvider,
|
|
15
|
+
PaletteRegistry,
|
|
16
|
+
PaletteSectionRenderer,
|
|
17
|
+
} from '@bpmn-nova/studio'
|
|
11
18
|
import type {
|
|
12
19
|
PropertiesContext,
|
|
13
20
|
PropertiesProvider,
|
|
@@ -22,8 +29,15 @@ import type {
|
|
|
22
29
|
BpmnStudioShell,
|
|
23
30
|
ContextMenuRegistry,
|
|
24
31
|
InteractionController,
|
|
32
|
+
NodeSubtitleResolver,
|
|
25
33
|
StudioCommands,
|
|
34
|
+
StudioShellActions,
|
|
35
|
+
StudioMode,
|
|
36
|
+
StudioModeChangeEvent,
|
|
37
|
+
StudioValidationEvent,
|
|
38
|
+
StudioValidationIssue,
|
|
26
39
|
StudioShellOptions,
|
|
40
|
+
StudioShellRegions,
|
|
27
41
|
StudioShellSlots,
|
|
28
42
|
StudioState,
|
|
29
43
|
TemplateRegistry,
|
|
@@ -39,6 +53,33 @@ import type {
|
|
|
39
53
|
ViewerProjection,
|
|
40
54
|
} from '@bpmn-nova/studio/viewer'
|
|
41
55
|
|
|
56
|
+
export {
|
|
57
|
+
createContextMenuRegistry,
|
|
58
|
+
createDefaultContextMenuRegistry,
|
|
59
|
+
createDefaultIconRegistry,
|
|
60
|
+
createDefaultPaletteRegistry,
|
|
61
|
+
createDefaultPropertiesRegistry,
|
|
62
|
+
createEmptyProcess,
|
|
63
|
+
createPaletteRegistry,
|
|
64
|
+
createPropertiesRegistry,
|
|
65
|
+
createStudioController,
|
|
66
|
+
createTemplateRegistry,
|
|
67
|
+
exportBpmn,
|
|
68
|
+
importBpmn,
|
|
69
|
+
propertyEntry,
|
|
70
|
+
propertyGroup,
|
|
71
|
+
registerActivitiProperties,
|
|
72
|
+
registerBpmnProperties,
|
|
73
|
+
registerFlowableProperties,
|
|
74
|
+
} from '@bpmn-nova/studio'
|
|
75
|
+
export type {
|
|
76
|
+
NodeSubtitleResolver,
|
|
77
|
+
NodeSubtitleResolverContext,
|
|
78
|
+
StudioValidationEvent,
|
|
79
|
+
StudioValidationIssue,
|
|
80
|
+
StudioValidationSource,
|
|
81
|
+
} from '@bpmn-nova/studio'
|
|
82
|
+
|
|
42
83
|
export interface BpmnNovaVisualProps {
|
|
43
84
|
theme?: NovaThemeInput
|
|
44
85
|
runtimeAppearance?: RuntimeAppearanceOptions
|
|
@@ -54,6 +95,8 @@ export interface BpmnModelProps {
|
|
|
54
95
|
export interface UseBpmnStudioOptions extends BpmnModelProps {
|
|
55
96
|
studio?: BpmnStudioController
|
|
56
97
|
propertiesProfile?: 'business' | 'developer'
|
|
98
|
+
allowedNodeTypes?: NodeType[]
|
|
99
|
+
allowedEdgeTypes?: EdgeType[]
|
|
57
100
|
}
|
|
58
101
|
export interface UseBpmnStudioResult { studio: BpmnStudioController; state: StudioState; commands: StudioCommands }
|
|
59
102
|
export function useBpmnStudio(options?: UseBpmnStudioOptions): UseBpmnStudioResult
|
|
@@ -65,11 +108,13 @@ export interface BpmnCanvasProps extends BpmnNovaVisualProps {
|
|
|
65
108
|
iconRegistry?: IconRegistry
|
|
66
109
|
nodeRenderers?: Record<string, Function>
|
|
67
110
|
nodeRenderer?: Function
|
|
111
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
68
112
|
svgExport?: ViewerOptions['svgExport']
|
|
69
113
|
}
|
|
70
114
|
export interface BpmnCanvasHandle {
|
|
71
115
|
readonly instance: CanvasInstance | null
|
|
72
116
|
fitView(padding?: number, options?: Record<string, unknown>): void
|
|
117
|
+
refreshPresentation(): void
|
|
73
118
|
clientToWorld(x: number, y: number): { x: number; y: number } | undefined
|
|
74
119
|
setTheme(theme: NovaThemeInput): NovaThemeState | undefined
|
|
75
120
|
exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
|
|
@@ -85,11 +130,11 @@ export interface BpmnPalettePanelProps extends BpmnNovaVisualProps {
|
|
|
85
130
|
providers?: PaletteProvider[]
|
|
86
131
|
canvas?: CanvasInstance | null
|
|
87
132
|
iconRegistry?: IconRegistry
|
|
88
|
-
renderItem?:
|
|
89
|
-
renderSection?:
|
|
133
|
+
renderItem?: PaletteItemRenderer
|
|
134
|
+
renderSection?: PaletteSectionRenderer
|
|
90
135
|
}
|
|
91
136
|
export interface BpmnPalettePanelHandle {
|
|
92
|
-
readonly panel:
|
|
137
|
+
readonly panel: PalettePanelInstance | null
|
|
93
138
|
render(): void
|
|
94
139
|
setTheme(theme: NovaThemeInput): NovaThemeState | undefined
|
|
95
140
|
}
|
|
@@ -98,6 +143,8 @@ export const BpmnPalettePanel: ForwardRefExoticComponent<BpmnPalettePanelProps &
|
|
|
98
143
|
export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
|
|
99
144
|
studio?: BpmnStudioController
|
|
100
145
|
propertiesProfile?: 'business' | 'developer'
|
|
146
|
+
allowedNodeTypes?: NodeType[]
|
|
147
|
+
allowedEdgeTypes?: EdgeType[]
|
|
101
148
|
iconRegistry?: IconRegistry
|
|
102
149
|
paletteRegistry?: PaletteRegistry
|
|
103
150
|
propertiesRegistry?: PropertiesRegistry
|
|
@@ -105,10 +152,16 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
|
|
|
105
152
|
contextMenuRegistry?: ContextMenuRegistry
|
|
106
153
|
nodeRenderers?: Record<string, Function>
|
|
107
154
|
nodeRenderer?: Function
|
|
155
|
+
nodeSubtitleResolver?: NodeSubtitleResolver
|
|
108
156
|
slots?: StudioShellSlots
|
|
109
157
|
layout?: StudioShellOptions['layout']
|
|
158
|
+
regions?: StudioShellRegions
|
|
159
|
+
headerStart?: BpmnStudioRenderSlot
|
|
160
|
+
headerActions?: BpmnStudioRenderSlot
|
|
161
|
+
header?: BpmnStudioRenderSlot
|
|
110
162
|
runtime?: ProcessInstanceSnapshot | null
|
|
111
|
-
mode?:
|
|
163
|
+
mode?: StudioMode
|
|
164
|
+
allowedModes?: readonly StudioMode[]
|
|
112
165
|
projection?: ViewerProjection
|
|
113
166
|
responsive?: boolean
|
|
114
167
|
projectionOptions?: Array<{ value: ViewerProjection; label: string }>
|
|
@@ -132,10 +185,28 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
|
|
|
132
185
|
onChange?: (model: ProcessModel, reason: string, xml: string) => void
|
|
133
186
|
onSelectionChange?: (selection: ElementSelection | null, element: StudioState['selectedElement']) => void
|
|
134
187
|
onScopeChange?: (activeScopeId: string, scopePath: StudioState['scopePath'], state: StudioState) => void
|
|
188
|
+
onModeChange?: (event: StudioModeChangeEvent) => void
|
|
189
|
+
onValidation?: (event: StudioValidationEvent) => void
|
|
190
|
+
}
|
|
191
|
+
export interface BpmnStudioRenderContext {
|
|
192
|
+
readonly studio: BpmnStudioController
|
|
193
|
+
readonly shell: BpmnStudioShell
|
|
194
|
+
readonly actions: StudioShellActions
|
|
195
|
+
readonly state: Readonly<StudioState>
|
|
196
|
+
readonly mode: StudioMode
|
|
135
197
|
}
|
|
198
|
+
export type BpmnStudioRenderSlot = ReactNode | ((context: BpmnStudioRenderContext) => ReactNode)
|
|
136
199
|
export interface BpmnStudioHandle {
|
|
137
200
|
readonly studio: BpmnStudioController
|
|
138
201
|
readonly shell: BpmnStudioShell | null
|
|
202
|
+
readonly actions: StudioShellActions | null
|
|
203
|
+
readonly mode: StudioMode | undefined
|
|
204
|
+
getMode(): StudioMode | undefined
|
|
205
|
+
getAllowedModes(): readonly StudioMode[]
|
|
206
|
+
setMode(mode: StudioMode): boolean
|
|
207
|
+
setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
|
|
208
|
+
refreshPresentation(): void
|
|
209
|
+
validate(): StudioValidationIssue[]
|
|
139
210
|
exportXml(engine?: EngineId): string
|
|
140
211
|
fitView(): void
|
|
141
212
|
setTheme(theme: NovaThemeInput): NovaThemeState | undefined
|
|
@@ -167,6 +238,7 @@ export interface BpmnViewerProps extends BpmnNovaVisualProps, BpmnModelProps, Om
|
|
|
167
238
|
export interface BpmnViewerHandle {
|
|
168
239
|
readonly instance: ViewerInstance | null
|
|
169
240
|
fitView(): void
|
|
241
|
+
refreshPresentation(): void
|
|
170
242
|
setProjection(projection: ViewerProjection): void
|
|
171
243
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
172
244
|
setDisplayOptions(options: Parameters<ViewerInstance['setDisplayOptions']>[0]): void
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import { BpmnDesigner as CoreDesigner } from '@bpmn-nova/studio/designer';
|
|
3
4
|
import { BpmnViewer as CoreViewer } from '@bpmn-nova/studio/viewer';
|
|
4
5
|
import { importBpmn, exportBpmn } from '@bpmn-nova/studio';
|
|
@@ -6,6 +7,26 @@ import { createEmptyProcess } from '@bpmn-nova/studio';
|
|
|
6
7
|
import { BpmnCanvas as CoreCanvas, BpmnStudioShell as CoreStudioShell, createInteractionController, createStudioController, createTemplateRegistry } from '@bpmn-nova/studio';
|
|
7
8
|
import { createDefaultPaletteRegistry, PalettePanel as CorePalettePanel } from '@bpmn-nova/studio';
|
|
8
9
|
|
|
10
|
+
export {
|
|
11
|
+
createContextMenuRegistry,
|
|
12
|
+
createDefaultContextMenuRegistry,
|
|
13
|
+
createDefaultIconRegistry,
|
|
14
|
+
createDefaultPaletteRegistry,
|
|
15
|
+
createDefaultPropertiesRegistry,
|
|
16
|
+
createEmptyProcess,
|
|
17
|
+
createPaletteRegistry,
|
|
18
|
+
createPropertiesRegistry,
|
|
19
|
+
createStudioController,
|
|
20
|
+
createTemplateRegistry,
|
|
21
|
+
exportBpmn,
|
|
22
|
+
importBpmn,
|
|
23
|
+
propertyEntry,
|
|
24
|
+
propertyGroup,
|
|
25
|
+
registerActivitiProperties,
|
|
26
|
+
registerBpmnProperties,
|
|
27
|
+
registerFlowableProperties,
|
|
28
|
+
} from '@bpmn-nova/studio';
|
|
29
|
+
|
|
9
30
|
export function createReactRuntimeDetailsComponent(Component) {
|
|
10
31
|
return ({ container, ...context }) => {
|
|
11
32
|
const root = createRoot(container);
|
|
@@ -30,7 +51,7 @@ function resolveModel({ model, xml, engine = 'flowable' }) {
|
|
|
30
51
|
|
|
31
52
|
export function useBpmnStudio(options = {}) {
|
|
32
53
|
const ownedRef = useRef(null);
|
|
33
|
-
if (!options.studio && !ownedRef.current) ownedRef.current = createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile });
|
|
54
|
+
if (!options.studio && !ownedRef.current) ownedRef.current = createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
|
|
34
55
|
const studio = options.studio || ownedRef.current;
|
|
35
56
|
const [state, setState] = useState(() => studio.getState());
|
|
36
57
|
useEffect(() => studio.subscribe((event) => setState(event.state)), [studio]);
|
|
@@ -41,9 +62,13 @@ export function useBpmnStudio(options = {}) {
|
|
|
41
62
|
export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
|
|
42
63
|
const hostRef = useRef(null);
|
|
43
64
|
const instanceRef = useRef(null);
|
|
65
|
+
const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
|
|
66
|
+
subtitleResolverRef.current = props.nodeSubtitleResolver;
|
|
67
|
+
const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
|
|
44
68
|
useImperativeHandle(ref, () => ({
|
|
45
69
|
get instance() { return instanceRef.current; },
|
|
46
70
|
fitView(padding, options) { instanceRef.current?.fitView(padding, options); },
|
|
71
|
+
refreshPresentation() { instanceRef.current?.refreshPresentation(); },
|
|
47
72
|
clientToWorld(x, y) { return instanceRef.current?.clientToWorld(x, y); },
|
|
48
73
|
setTheme(theme) { return instanceRef.current?.setTheme(theme); },
|
|
49
74
|
exportSvg(options) { return instanceRef.current?.exportSvg(options); },
|
|
@@ -52,12 +77,13 @@ export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
|
|
|
52
77
|
useEffect(() => {
|
|
53
78
|
if (!hostRef.current || !props.studio) return undefined;
|
|
54
79
|
const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
|
|
55
|
-
const instance = new CoreCanvas({ container: hostRef.current, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
|
|
80
|
+
const instance = new CoreCanvas({ container: hostRef.current, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, nodeSubtitleResolver: stableSubtitleResolverRef.current, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
|
|
56
81
|
instanceRef.current = instance;
|
|
57
82
|
requestAnimationFrame(() => instance.fitView());
|
|
58
83
|
return () => { instance.destroy(); instanceRef.current = null; };
|
|
59
84
|
}, [props.studio]);
|
|
60
85
|
useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
86
|
+
useEffect(() => { instanceRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
|
|
61
87
|
return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 320, ...props.style } });
|
|
62
88
|
});
|
|
63
89
|
|
|
@@ -72,7 +98,7 @@ export const BpmnPalettePanel = forwardRef(function BpmnPalettePanel(props, ref)
|
|
|
72
98
|
const panel = new CorePalettePanel({ container: hostRef.current, registry, studio: props.studio, interactions, canvas: props.canvas, iconRegistry: props.iconRegistry, renderItem: props.renderItem, renderSection: props.renderSection, theme: props.theme, onThemeChange: props.onThemeChange });
|
|
73
99
|
panelRef.current = panel;
|
|
74
100
|
return () => { panel.destroy(); panelRef.current = null; };
|
|
75
|
-
}, [props.studio, props.registry, props.interactions, props.canvas]);
|
|
101
|
+
}, [props.studio, props.registry, props.interactions, props.templates, props.canvas, props.providers, props.iconRegistry, props.renderItem, props.renderSection, props.onThemeChange]);
|
|
76
102
|
useEffect(() => { if (props.theme !== undefined) panelRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
77
103
|
return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', overflow: 'auto', ...props.style } });
|
|
78
104
|
});
|
|
@@ -81,21 +107,53 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
81
107
|
const hostRef = useRef(null);
|
|
82
108
|
const ownedStudioRef = useRef(null);
|
|
83
109
|
const shellRef = useRef(null);
|
|
110
|
+
const mountedModelSyncRef = useRef(false);
|
|
111
|
+
const [headerPortal, setHeaderPortal] = useState(null);
|
|
112
|
+
const [headerStartPortal, setHeaderStartPortal] = useState(null);
|
|
113
|
+
const [headerActionsPortal, setHeaderActionsPortal] = useState(null);
|
|
84
114
|
const callbacksRef = useRef({});
|
|
85
115
|
callbacksRef.current = props;
|
|
86
|
-
if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile });
|
|
116
|
+
if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
|
|
87
117
|
const studio = props.studio || ownedStudioRef.current;
|
|
118
|
+
const [studioState, setStudioState] = useState(() => studio.getState());
|
|
119
|
+
const [actualMode, setActualMode] = useState(() => props.mode || props.allowedModes?.[0] || 'design');
|
|
120
|
+
const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
|
|
121
|
+
subtitleResolverRef.current = props.nodeSubtitleResolver;
|
|
122
|
+
const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
|
|
88
123
|
useImperativeHandle(ref, () => ({
|
|
89
124
|
studio,
|
|
90
125
|
get shell() { return shellRef.current; },
|
|
126
|
+
get actions() { return shellRef.current?.actions || null; },
|
|
127
|
+
get mode() { return shellRef.current?.getMode(); },
|
|
128
|
+
getMode: () => shellRef.current?.getMode(),
|
|
129
|
+
getAllowedModes: () => shellRef.current?.getAllowedModes() || [],
|
|
130
|
+
setMode: (mode) => shellRef.current?.setMode(mode) || false,
|
|
131
|
+
setAllowedModes: (modes) => shellRef.current?.setAllowedModes(modes) || [],
|
|
132
|
+
refreshPresentation: () => shellRef.current?.refreshPresentation(),
|
|
133
|
+
validate: () => shellRef.current?.validate() || [],
|
|
91
134
|
exportXml: (engine) => studio.exportXml(engine),
|
|
92
135
|
exportSvg: (options) => shellRef.current?.exportSvg(options),
|
|
93
136
|
openSvgExportPreview: (options) => shellRef.current?.openSvgExportPreview(options),
|
|
94
|
-
fitView: () => shellRef.current?.
|
|
137
|
+
fitView: () => shellRef.current?.fitView(),
|
|
95
138
|
setTheme: (theme) => shellRef.current?.setTheme(theme),
|
|
96
139
|
}), [studio]);
|
|
140
|
+
const hasNativeHeader = props.header !== undefined && props.header !== null;
|
|
141
|
+
const hasNativeHeaderStart = props.headerStart !== undefined && props.headerStart !== null;
|
|
142
|
+
const hasNativeHeaderActions = props.headerActions !== undefined && props.headerActions !== null;
|
|
97
143
|
useEffect(() => {
|
|
98
144
|
if (!hostRef.current) return undefined;
|
|
145
|
+
setStudioState(studio.getState());
|
|
146
|
+
const createPortalSlot = (setPortal) => (context) => {
|
|
147
|
+
const portal = { target: context.container, context };
|
|
148
|
+
setPortal(portal);
|
|
149
|
+
return () => setPortal((current) => current?.target === context.container ? null : current);
|
|
150
|
+
};
|
|
151
|
+
const shellSlots = { ...(props.slots || {}) };
|
|
152
|
+
if (hasNativeHeader) shellSlots.header = createPortalSlot(setHeaderPortal);
|
|
153
|
+
else if (!shellSlots.header) {
|
|
154
|
+
if (hasNativeHeaderStart) shellSlots.headerStart = createPortalSlot(setHeaderStartPortal);
|
|
155
|
+
if (hasNativeHeaderActions) shellSlots.headerActions = createPortalSlot(setHeaderActionsPortal);
|
|
156
|
+
}
|
|
99
157
|
const shell = new CoreStudioShell({
|
|
100
158
|
container: hostRef.current,
|
|
101
159
|
studio,
|
|
@@ -103,9 +161,11 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
103
161
|
paletteRegistry: props.paletteRegistry,
|
|
104
162
|
propertiesRegistry: props.propertiesRegistry,
|
|
105
163
|
templateRegistry: props.templateRegistry,
|
|
164
|
+
contextMenuRegistry: props.contextMenuRegistry,
|
|
106
165
|
rendererOptions: {
|
|
107
166
|
nodeRenderers: props.nodeRenderers,
|
|
108
167
|
nodeRenderer: props.nodeRenderer,
|
|
168
|
+
nodeSubtitleResolver: stableSubtitleResolverRef.current,
|
|
109
169
|
runtimePresenter: props.runtimePresenter,
|
|
110
170
|
runtimeTraceProjector: props.runtimeTraceProjector,
|
|
111
171
|
runtimeAssetResolver: props.runtimeAssetResolver,
|
|
@@ -121,10 +181,12 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
121
181
|
onElementClick: (payload) => callbacksRef.current.onElementClick?.(payload),
|
|
122
182
|
onTraceClick: (payload) => callbacksRef.current.onTraceClick?.(payload),
|
|
123
183
|
},
|
|
124
|
-
slots:
|
|
184
|
+
slots: shellSlots,
|
|
125
185
|
layout: props.layout,
|
|
186
|
+
regions: props.regions,
|
|
126
187
|
runtime: props.runtime,
|
|
127
188
|
mode: props.mode,
|
|
189
|
+
allowedModes: props.allowedModes,
|
|
128
190
|
projection: props.projection,
|
|
129
191
|
responsive: props.responsive,
|
|
130
192
|
projectionOptions: props.projectionOptions,
|
|
@@ -136,19 +198,46 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
136
198
|
onThemeChange: (state) => callbacksRef.current.onThemeChange?.(state),
|
|
137
199
|
});
|
|
138
200
|
shellRef.current = shell;
|
|
201
|
+
setActualMode(shell.getMode());
|
|
202
|
+
const offMode = shell.subscribeMode((event) => {
|
|
203
|
+
setActualMode(event.mode);
|
|
204
|
+
callbacksRef.current.onModeChange?.(event);
|
|
205
|
+
});
|
|
206
|
+
const offValidation = shell.subscribeValidation((event) => callbacksRef.current.onValidation?.(event));
|
|
139
207
|
const off = studio.subscribe((event) => {
|
|
208
|
+
setStudioState(event.state);
|
|
140
209
|
if (event.type === 'modelChanged') callbacksRef.current.onChange?.(studio.model, event.reason, studio.exportXml());
|
|
141
210
|
if (event.type === 'selectionChanged') callbacksRef.current.onSelectionChange?.(studio.selection, studio.getSelectedElement());
|
|
142
211
|
if (event.type === 'scopeChanged') callbacksRef.current.onScopeChange?.(event.activeScopeId, event.scopePath, studio.getState());
|
|
143
212
|
});
|
|
144
|
-
return () => {
|
|
145
|
-
}, [studio]);
|
|
146
|
-
useEffect(() =>
|
|
213
|
+
return () => { offMode(); offValidation(); off(); shell.destroy(); shellRef.current = null; };
|
|
214
|
+
}, [studio, hasNativeHeader, hasNativeHeaderStart, hasNativeHeaderActions]);
|
|
215
|
+
useEffect(() => () => {
|
|
216
|
+
if (!props.studio && ownedStudioRef.current === studio) {
|
|
217
|
+
studio.destroy();
|
|
218
|
+
ownedStudioRef.current = null;
|
|
219
|
+
}
|
|
220
|
+
}, [studio, Boolean(props.studio)]);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!mountedModelSyncRef.current) {
|
|
223
|
+
mountedModelSyncRef.current = true;
|
|
224
|
+
if (!props.studio) return;
|
|
225
|
+
}
|
|
226
|
+
if (props.model && props.model !== studio.model) studio.setModel(props.model);
|
|
227
|
+
else if (props.xml && props.xml !== studio.exportXml(props.engine)) studio.importXml(props.xml, props.engine);
|
|
228
|
+
}, [studio, props.model, props.xml, props.engine]);
|
|
147
229
|
useEffect(() => { if (props.runtime !== undefined) shellRef.current?.setRuntime(props.runtime); }, [props.runtime]);
|
|
148
|
-
useEffect(() => {
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const shell = shellRef.current;
|
|
232
|
+
if (!shell) return;
|
|
233
|
+
if (props.allowedModes !== undefined) shell.setAllowedModes(props.allowedModes);
|
|
234
|
+
if (props.mode) shell.setMode(props.mode);
|
|
235
|
+
}, [props.allowedModes, props.mode]);
|
|
149
236
|
useEffect(() => { if (props.projection) shellRef.current?.setProjection(props.projection); }, [props.projection]);
|
|
237
|
+
useEffect(() => { if (props.regions !== undefined) shellRef.current?.setRegions(props.regions); }, [props.regions]);
|
|
150
238
|
useEffect(() => { if (props.theme !== undefined) shellRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
151
239
|
useEffect(() => { if (props.runtimeAppearance !== undefined) shellRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
|
|
240
|
+
useEffect(() => { shellRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
|
|
152
241
|
useEffect(() => {
|
|
153
242
|
const shell = shellRef.current;
|
|
154
243
|
if (!shell) return;
|
|
@@ -157,7 +246,23 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
|
|
|
157
246
|
shell.rendererOptions.runtimeTraceOptions = props.runtimeTraceOptions;
|
|
158
247
|
shell.viewer?.setDisplayOptions({ timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions });
|
|
159
248
|
}, [props.timeline, props.runtimeDetails, props.runtimeTraceOptions]);
|
|
160
|
-
|
|
249
|
+
const renderSlot = (slot, portal, key) => {
|
|
250
|
+
if (slot === undefined || slot === null || !portal) return null;
|
|
251
|
+
const context = {
|
|
252
|
+
studio: portal.context.studio,
|
|
253
|
+
shell: portal.context.shell,
|
|
254
|
+
actions: portal.context.actions,
|
|
255
|
+
state: studioState,
|
|
256
|
+
mode: actualMode,
|
|
257
|
+
};
|
|
258
|
+
return createPortal(typeof slot === 'function' ? slot(context) : slot, portal.target, key);
|
|
259
|
+
};
|
|
260
|
+
return React.createElement(React.Fragment, null,
|
|
261
|
+
React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 520, ...props.style } }),
|
|
262
|
+
renderSlot(props.header, headerPortal, 'bpmn-nova-header'),
|
|
263
|
+
renderSlot(props.headerStart, headerStartPortal, 'bpmn-nova-header-start'),
|
|
264
|
+
renderSlot(props.headerActions, headerActionsPortal, 'bpmn-nova-header-actions'),
|
|
265
|
+
);
|
|
161
266
|
});
|
|
162
267
|
|
|
163
268
|
export const BpmnDesigner = forwardRef(function BpmnDesigner(props, ref) {
|
|
@@ -221,6 +326,9 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
221
326
|
const hostRef = useRef(null);
|
|
222
327
|
const instanceRef = useRef(null);
|
|
223
328
|
const callbacksRef = useRef({});
|
|
329
|
+
const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
|
|
330
|
+
subtitleResolverRef.current = props.nodeSubtitleResolver;
|
|
331
|
+
const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
|
|
224
332
|
callbacksRef.current = {
|
|
225
333
|
onTraceClick: props.onTraceClick,
|
|
226
334
|
onElementClick: props.onElementClick,
|
|
@@ -234,6 +342,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
234
342
|
useImperativeHandle(ref, () => ({
|
|
235
343
|
get instance() { return instanceRef.current; },
|
|
236
344
|
fitView() { instanceRef.current?.fitView(); },
|
|
345
|
+
refreshPresentation() { instanceRef.current?.refreshPresentation(); },
|
|
237
346
|
setProjection(value) { instanceRef.current?.setProjection(value); },
|
|
238
347
|
setRuntime(value) { instanceRef.current?.setRuntime(value); },
|
|
239
348
|
setDisplayOptions(value) { instanceRef.current?.setDisplayOptions(value); },
|
|
@@ -256,6 +365,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
256
365
|
iconRegistry: props.iconRegistry,
|
|
257
366
|
nodeRenderers: props.nodeRenderers,
|
|
258
367
|
nodeRenderer: props.nodeRenderer,
|
|
368
|
+
nodeSubtitleResolver: stableSubtitleResolverRef.current,
|
|
259
369
|
runtimePresenter: props.runtimePresenter,
|
|
260
370
|
runtimeTraceProjector: props.runtimeTraceProjector,
|
|
261
371
|
runtimeAssetResolver: props.runtimeAssetResolver,
|
|
@@ -285,6 +395,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
|
|
|
285
395
|
useEffect(() => { if (instanceRef.current && props.projection) instanceRef.current.setProjection(props.projection); }, [props.projection]);
|
|
286
396
|
useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
|
|
287
397
|
useEffect(() => { if (props.runtimeAppearance !== undefined) instanceRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
|
|
398
|
+
useEffect(() => { instanceRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
|
|
288
399
|
useEffect(() => { instanceRef.current?.setDisplayOptions({ timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions, runtimeAssetResolver: props.runtimeAssetResolver }); }, [props.timeline, props.runtimeDetails, props.runtimeTraceOptions, props.runtimeAssetResolver]);
|
|
289
400
|
useEffect(() => {
|
|
290
401
|
if (!instanceRef.current) return;
|
package/dist/styles.css
CHANGED
|
@@ -511,8 +511,8 @@
|
|
|
511
511
|
.mb-node-event,
|
|
512
512
|
.mb-node-boundary { display: grid; place-items: center; z-index: 7; }
|
|
513
513
|
.mb-event-shape {
|
|
514
|
-
width: 46px;
|
|
515
|
-
height: 46px;
|
|
514
|
+
width: var(--mb-event-shape-size, 46px);
|
|
515
|
+
height: var(--mb-event-shape-size, 46px);
|
|
516
516
|
box-sizing: border-box;
|
|
517
517
|
display: grid;
|
|
518
518
|
place-items: center;
|
|
@@ -523,7 +523,7 @@
|
|
|
523
523
|
box-shadow: var(--nova-shadow-sm);
|
|
524
524
|
position: relative;
|
|
525
525
|
}
|
|
526
|
-
.mb-node-boundary .mb-event-shape { width: 40px; height: 40px; border-width: 2px; }
|
|
526
|
+
.mb-node-boundary .mb-event-shape { width: var(--mb-event-shape-size, 40px); height: var(--mb-event-shape-size, 40px); border-width: 2px; }
|
|
527
527
|
.mb-event-shape[data-stage="intermediate"],
|
|
528
528
|
.mb-node-boundary .mb-event-shape { box-shadow: inset 0 0 0 3px var(--nova-color-surface), inset 0 0 0 4.5px var(--nova-color-text-muted, #8796a9), var(--nova-shadow-sm); }
|
|
529
529
|
.mb-event-shape[data-stage="end"] { border-width: 4px; border-color: var(--nova-color-text-secondary); }
|
|
@@ -1141,6 +1141,8 @@ textarea.property-control { resize: vertical; }
|
|
|
1141
1141
|
|
|
1142
1142
|
/* @bpmn-nova/internal/studio */
|
|
1143
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); }
|
|
1144
|
+
.nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
|
|
1145
|
+
.nova-studio-header[hidden], .nova-studio-left[hidden], .nova-studio-right[hidden], .nova-studio-statusbar[hidden] { display: none !important; }
|
|
1144
1146
|
.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; }
|
|
1145
1147
|
.nova-studio-brand { min-width: 0; display: flex; align-items: center; gap: 10px; }
|
|
1146
1148
|
.nova-studio-brand-mark { flex: none; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--nova-color-text-inverse, #fff); background: linear-gradient(145deg, var(--nova-color-primary, #5362da), var(--nova-color-primary-hover)); font-size: var(--nova-font-size-md); font-weight: var(--nova-font-weight-bold); box-shadow: var(--nova-shadow-sm); }
|
|
@@ -1157,6 +1159,7 @@ textarea.property-control { resize: vertical; }
|
|
|
1157
1159
|
.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); }
|
|
1158
1160
|
.nova-studio-tools { min-width: 0; overflow: visible; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
|
|
1159
1161
|
.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; }
|
|
1160
1163
|
.nova-studio-tool-divider { flex: none; width: 1px; height: 20px; margin: 0 2px; background: var(--nova-color-divider, #e4e8f0); }
|
|
1161
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; }
|
|
1162
1165
|
.nova-studio-tool:focus { outline: none; }.nova-studio-tool:focus-visible { outline: 2px solid var(--nova-color-focus-ring, #aeb7ff); outline-offset: 1px; }
|
|
@@ -1194,7 +1197,11 @@ textarea.property-control { resize: vertical; }
|
|
|
1194
1197
|
.nova-studio-beautify-action-icon > .nova-icon-svg { width: 14px; height: 14px; }
|
|
1195
1198
|
.nova-studio-beautify-action strong { font-size: var(--nova-font-size-sm); font-weight: var(--nova-font-weight-semibold); line-height: 18px; }.nova-studio-beautify-action small { color: #778195; color: var(--nova-color-text-muted, #778195); font-size: var(--nova-font-size-xs); line-height: 14px; }
|
|
1196
1199
|
.nova-studio-body { min-width: 0; min-height: 0; display: grid; grid-template-columns: var(--nova-left-width) minmax(320px, 1fr) var(--nova-right-width); }
|
|
1200
|
+
.nova-studio-body.is-left-hidden { grid-template-columns: minmax(320px, 1fr) var(--nova-right-width); }
|
|
1201
|
+
.nova-studio-body.is-right-hidden { grid-template-columns: var(--nova-left-width) minmax(320px, 1fr); }
|
|
1202
|
+
.nova-studio-body.is-left-hidden.is-right-hidden { grid-template-columns: minmax(320px, 1fr); }
|
|
1197
1203
|
.nova-studio-body.is-readonly { grid-template-columns: minmax(320px, 1fr) var(--nova-right-width); }
|
|
1204
|
+
.nova-studio-body.is-readonly.is-right-hidden { grid-template-columns: minmax(320px, 1fr); }
|
|
1198
1205
|
.nova-studio-body.is-readonly .nova-studio-left { display: none; }
|
|
1199
1206
|
.nova-studio-left, .nova-studio-right, .nova-studio-canvas-column, .nova-studio-canvas { min-width: 0; min-height: 0; overflow: hidden; }
|
|
1200
1207
|
.nova-studio-left { border-right: 1px solid var(--nova-color-divider, #e4e8f0); background: var(--nova-color-surface, #fff); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }
|
|
@@ -1206,6 +1213,7 @@ textarea.property-control { resize: vertical; }
|
|
|
1206
1213
|
.nova-studio-left::-webkit-scrollbar-thumb:hover, .nova-studio-right::-webkit-scrollbar-thumb:hover { background: #c1c9d5; background: var(--nova-color-text-muted); }
|
|
1207
1214
|
.nova-studio-left::-webkit-scrollbar-button, .nova-studio-right::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
|
|
1208
1215
|
.nova-studio-canvas-column { display: grid; grid-template-rows: minmax(0, 1fr) 35px; background: var(--nova-color-canvas, #f7f8fc); }
|
|
1216
|
+
.nova-studio-canvas-column.is-footer-hidden { grid-template-rows: minmax(0, 1fr); }
|
|
1209
1217
|
.nova-studio-canvas-stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
|
|
1210
1218
|
.nova-studio-canvas { position: relative; }
|
|
1211
1219
|
.nova-studio-canvas-stage > .nova-studio-canvas { width: 100%; height: 100%; }
|
|
@@ -1422,6 +1430,7 @@ textarea.property-control { resize: vertical; }
|
|
|
1422
1430
|
}
|
|
1423
1431
|
@media (max-width: 720px) {
|
|
1424
1432
|
.nova-studio-shell { grid-template-rows: 54px minmax(0,1fr); }
|
|
1433
|
+
.nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
|
|
1425
1434
|
.nova-studio-header { grid-template-columns: auto minmax(0,1fr); padding-left: 9px; }
|
|
1426
1435
|
.nova-studio-brand-copy { display: none; }
|
|
1427
1436
|
.nova-studio-body { grid-template-columns: 1fr; }
|