@bpmn-nova/studio 0.3.0-preview → 0.3.2-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.
Files changed (63) hide show
  1. package/README.md +245 -20
  2. package/dist/canvas.js +7 -4
  3. package/dist/context-menu.js +2 -2
  4. package/dist/controller.js +84 -6
  5. package/dist/index.d.ts +138 -38
  6. package/dist/index.js +12 -11
  7. package/dist/interactions.js +1 -1
  8. package/dist/modules/bpmn-model/index.d.ts +11 -0
  9. package/dist/modules/bpmn-model/index.js +703 -0
  10. package/dist/modules/core/containment.js +590 -0
  11. package/dist/modules/core/gateway.js +72 -0
  12. package/dist/modules/core/history.js +32 -0
  13. package/dist/modules/core/index.d.ts +268 -0
  14. package/dist/modules/core/index.js +7 -0
  15. package/dist/modules/core/layout.js +644 -0
  16. package/dist/modules/core/model.js +287 -0
  17. package/dist/modules/core/runtime-transition-route.js +360 -0
  18. package/dist/modules/core/scope.js +99 -0
  19. package/dist/modules/designer/index.d.ts +79 -0
  20. package/dist/modules/designer/index.js +607 -0
  21. package/dist/modules/engine-activiti/index.d.ts +19 -0
  22. package/dist/modules/engine-activiti/index.js +160 -0
  23. package/dist/modules/engine-flowable/index.d.ts +19 -0
  24. package/dist/modules/engine-flowable/index.js +160 -0
  25. package/dist/modules/export-svg/index.d.ts +112 -0
  26. package/dist/modules/export-svg/index.js +2 -0
  27. package/dist/modules/export-svg/preview.js +327 -0
  28. package/dist/modules/export-svg/render.js +718 -0
  29. package/dist/modules/icons/index.d.ts +24 -0
  30. package/dist/modules/icons/index.js +264 -0
  31. package/dist/modules/palette/index.d.ts +74 -0
  32. package/dist/modules/palette/index.js +99 -0
  33. package/dist/modules/palette/panel.js +99 -0
  34. package/dist/modules/properties/index.d.ts +20 -0
  35. package/dist/modules/properties/index.js +19 -0
  36. package/dist/modules/properties-activiti/index.d.ts +3 -0
  37. package/dist/modules/properties-activiti/index.js +97 -0
  38. package/dist/modules/properties-bpmn/index.d.ts +3 -0
  39. package/dist/modules/properties-bpmn/index.js +518 -0
  40. package/dist/modules/properties-core/index.d.ts +124 -0
  41. package/dist/modules/properties-core/index.js +312 -0
  42. package/dist/modules/properties-flowable/index.d.ts +3 -0
  43. package/dist/modules/properties-flowable/index.js +114 -0
  44. package/dist/modules/properties-renderer/index.d.ts +25 -0
  45. package/dist/modules/properties-renderer/index.js +491 -0
  46. package/dist/modules/renderer-svg/index.d.ts +118 -0
  47. package/dist/modules/renderer-svg/index.js +1460 -0
  48. package/dist/modules/runtime/index.d.ts +169 -0
  49. package/dist/modules/runtime/index.js +535 -0
  50. package/dist/modules/theme/index.d.ts +95 -0
  51. package/dist/modules/theme/index.js +368 -0
  52. package/dist/modules/viewer/index.d.ts +265 -0
  53. package/dist/modules/viewer/index.js +1011 -0
  54. package/dist/modules/viewer/runtime-content.js +123 -0
  55. package/dist/modules/viewer/runtime-details-motion.js +228 -0
  56. package/dist/modules/viewer/runtime-trace.js +574 -0
  57. package/dist/modules/viewer/timeline.js +276 -0
  58. package/dist/selection-layout.js +1 -1
  59. package/dist/shell.js +210 -26
  60. package/dist/styles.css +116 -7
  61. package/llms-full.txt +3082 -0
  62. package/llms.txt +225 -0
  63. package/package.json +39 -16
@@ -0,0 +1,327 @@
1
+ import { downloadSvg } from './render.js';
2
+
3
+ function element(document, tag, className = '', text = '') {
4
+ const node = document.createElement(tag);
5
+ if (className) node.className = className;
6
+ if (text) node.textContent = text;
7
+ return node;
8
+ }
9
+
10
+ function button(document, label, className = '') {
11
+ const node = element(document, 'button', className, label);
12
+ node.type = 'button';
13
+ return node;
14
+ }
15
+
16
+ function clamp(value, min, max) {
17
+ return Math.max(min, Math.min(max, value));
18
+ }
19
+
20
+ export class SvgExportPreviewController {
21
+ constructor({ container, createArtifact, title = '导出 SVG', initialTheme = 'current', onClose = null, onDownload = null } = {}) {
22
+ if (!container) throw new Error('SVG export preview requires a container.');
23
+ if (typeof createArtifact !== 'function') throw new Error('SVG export preview requires createArtifact().');
24
+ this.container = container;
25
+ this.document = container.ownerDocument;
26
+ this.ownerWindow = this.document.defaultView || globalThis;
27
+ this.createArtifact = createArtifact;
28
+ this.onClose = onClose;
29
+ this.onDownload = onDownload;
30
+ this.theme = ['light', 'dark'].includes(initialTheme) ? initialTheme : 'current';
31
+ this.transparentBackground = false;
32
+ this.artifact = null;
33
+ this.abortController = null;
34
+ this.objectUrl = null;
35
+ this.zoom = 1;
36
+ this.pan = { x: 0, y: 0 };
37
+ this.drag = null;
38
+ this.closed = false;
39
+ this._restoreFocus = this.document.activeElement instanceof this.ownerWindow.HTMLElement ? this.document.activeElement : null;
40
+ this._build(title);
41
+ this._bind();
42
+ this._generate();
43
+ }
44
+
45
+ _build(title) {
46
+ const document = this.document;
47
+ const root = element(document, 'div', 'nova-svg-export-preview is-entering');
48
+ root.setAttribute('role', 'dialog');
49
+ root.setAttribute('aria-modal', 'true');
50
+ root.setAttribute('aria-label', title);
51
+ const dialog = element(document, 'section', 'nova-svg-export-dialog');
52
+ const header = element(document, 'header', 'nova-svg-export-header');
53
+ const heading = element(document, 'div', 'nova-svg-export-heading');
54
+ heading.append(element(document, 'strong', '', title), element(document, 'span', '', '预览确认后下载'));
55
+ const close = button(document, '×', 'nova-svg-export-close');
56
+ close.setAttribute('aria-label', '关闭 SVG 预览');
57
+ header.append(heading, close);
58
+
59
+ const toolbar = element(document, 'div', 'nova-svg-export-toolbar');
60
+ const themeGroup = element(document, 'div', 'nova-svg-export-theme');
61
+ themeGroup.setAttribute('role', 'group');
62
+ themeGroup.setAttribute('aria-label', 'SVG 主题');
63
+ this.themeButtons = new Map();
64
+ for (const option of [{ value: 'current', label: '当前' }, { value: 'light', label: '浅色' }, { value: 'dark', label: '深色' }]) {
65
+ const item = button(document, option.label);
66
+ item.dataset.exportTheme = option.value;
67
+ item.addEventListener('click', () => this.setTheme(option.value));
68
+ themeGroup.appendChild(item);
69
+ this.themeButtons.set(option.value, item);
70
+ }
71
+ const transparentLabel = element(document, 'label', 'nova-svg-export-transparent');
72
+ const transparent = element(document, 'input');
73
+ transparent.type = 'checkbox';
74
+ transparent.addEventListener('change', () => this.setTransparentBackground(transparent.checked));
75
+ transparentLabel.append(transparent, document.createTextNode('透明背景'));
76
+ const viewportTools = element(document, 'div', 'nova-svg-export-viewport-tools');
77
+ const zoomOut = button(document, '−');
78
+ zoomOut.title = '缩小';
79
+ const fit = button(document, '最佳视图');
80
+ const actual = button(document, '1:1');
81
+ const zoomIn = button(document, '+');
82
+ zoomIn.title = '放大';
83
+ this.zoomText = element(document, 'span', '', '100%');
84
+ viewportTools.append(zoomOut, this.zoomText, zoomIn, fit, actual);
85
+ toolbar.append(themeGroup, transparentLabel, viewportTools);
86
+
87
+ const body = element(document, 'div', 'nova-svg-export-body');
88
+ const viewport = element(document, 'div', 'nova-svg-export-viewport');
89
+ viewport.tabIndex = 0;
90
+ viewport.setAttribute('aria-label', 'SVG 预览画布,可拖动和缩放');
91
+ const stage = element(document, 'div', 'nova-svg-export-stage');
92
+ const image = element(document, 'img', 'nova-svg-export-image');
93
+ image.alt = 'SVG 导出预览';
94
+ stage.appendChild(image);
95
+ viewport.appendChild(stage);
96
+ const state = element(document, 'div', 'nova-svg-export-state');
97
+ state.append(element(document, 'span', 'nova-svg-export-spinner'), element(document, 'strong', '', '正在生成 SVG…'));
98
+ body.append(viewport, state);
99
+
100
+ const warning = element(document, 'div', 'nova-svg-export-warnings');
101
+ warning.hidden = true;
102
+ const footer = element(document, 'footer', 'nova-svg-export-footer');
103
+ const summary = element(document, 'span', 'nova-svg-export-summary', '准备导出');
104
+ const actions = element(document, 'div', 'nova-svg-export-actions');
105
+ const cancel = button(document, '取消');
106
+ const retry = button(document, '重试', 'nova-svg-export-retry');
107
+ retry.hidden = true;
108
+ const confirm = button(document, '确认下载', 'is-primary');
109
+ confirm.disabled = true;
110
+ actions.append(cancel, retry, confirm);
111
+ footer.append(summary, actions);
112
+ dialog.append(header, toolbar, body, warning, footer);
113
+ root.appendChild(dialog);
114
+ this.container.appendChild(root);
115
+
116
+ Object.assign(this, { root, dialog, closeButton: close, toolbar, transparentInput: transparent, viewport, stage, image, state, warning, summary, cancelButton: cancel, retryButton: retry, confirmButton: confirm, zoomOutButton: zoomOut, zoomInButton: zoomIn, fitButton: fit, actualButton: actual });
117
+ this._syncThemeButtons();
118
+ this.ownerWindow.requestAnimationFrame(() => {
119
+ if (!root.isConnected) return;
120
+ root.classList.remove('is-entering');
121
+ root.classList.add('is-open');
122
+ close.focus({ preventScroll: true });
123
+ });
124
+ }
125
+
126
+ _bind() {
127
+ this.root.addEventListener('pointerdown', (event) => {
128
+ if (event.target === this.root) this.close();
129
+ });
130
+ this.closeButton.addEventListener('click', () => this.close());
131
+ this.cancelButton.addEventListener('click', () => this.close());
132
+ this.retryButton.addEventListener('click', () => this._generate());
133
+ this.confirmButton.addEventListener('click', () => {
134
+ if (!this.artifact) return;
135
+ downloadSvg(this.artifact, { document: this.document });
136
+ this.onDownload?.(this.artifact);
137
+ this.close();
138
+ });
139
+ this.zoomOutButton.addEventListener('click', () => this._zoomBy(0.85));
140
+ this.zoomInButton.addEventListener('click', () => this._zoomBy(1.18));
141
+ this.fitButton.addEventListener('click', () => this.fit());
142
+ this.actualButton.addEventListener('click', () => this.actualSize());
143
+ this.viewport.addEventListener('wheel', (event) => {
144
+ event.preventDefault();
145
+ const rect = this.viewport.getBoundingClientRect();
146
+ const point = { x: event.clientX - rect.left, y: event.clientY - rect.top };
147
+ this._setZoom(this.zoom * (event.deltaY < 0 ? 1.1 : 0.9), point);
148
+ }, { passive: false });
149
+ this.viewport.addEventListener('pointerdown', (event) => {
150
+ if (event.button !== 0) return;
151
+ this.drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, pan: { ...this.pan } };
152
+ this.viewport.setPointerCapture?.(event.pointerId);
153
+ this.viewport.classList.add('is-dragging');
154
+ });
155
+ this.viewport.addEventListener('pointermove', (event) => {
156
+ if (!this.drag || event.pointerId !== this.drag.pointerId) return;
157
+ this.pan.x = this.drag.pan.x + event.clientX - this.drag.x;
158
+ this.pan.y = this.drag.pan.y + event.clientY - this.drag.y;
159
+ this._applyTransform();
160
+ });
161
+ const finishDrag = (event) => {
162
+ if (!this.drag || event.pointerId !== this.drag.pointerId) return;
163
+ if (this.viewport.hasPointerCapture?.(event.pointerId)) this.viewport.releasePointerCapture?.(event.pointerId);
164
+ this.drag = null;
165
+ this.viewport.classList.remove('is-dragging');
166
+ };
167
+ this.viewport.addEventListener('pointerup', finishDrag);
168
+ this.viewport.addEventListener('pointercancel', finishDrag);
169
+ this._keyHandler = (event) => {
170
+ if (!this.root?.isConnected) return;
171
+ if (event.key === 'Escape') { event.preventDefault(); this.close(); return; }
172
+ if (event.key !== 'Tab') return;
173
+ const focusable = [...this.dialog.querySelectorAll('button:not([disabled]):not([hidden]), input:not([disabled]), [tabindex]:not([tabindex="-1"])')].filter((node) => !node.hidden);
174
+ if (!focusable.length) return;
175
+ const first = focusable[0];
176
+ const last = focusable.at(-1);
177
+ if (event.shiftKey && this.document.activeElement === first) { event.preventDefault(); last.focus(); }
178
+ else if (!event.shiftKey && this.document.activeElement === last) { event.preventDefault(); first.focus(); }
179
+ };
180
+ this.document.addEventListener('keydown', this._keyHandler);
181
+ }
182
+
183
+ _syncThemeButtons() {
184
+ this.themeButtons.forEach((button, value) => {
185
+ const active = value === this.theme;
186
+ button.classList.toggle('is-active', active);
187
+ button.setAttribute('aria-pressed', String(active));
188
+ });
189
+ }
190
+
191
+ async _generate() {
192
+ if (this.closed) return;
193
+ this.abortController?.abort();
194
+ this.abortController = new AbortController();
195
+ const signal = this.abortController.signal;
196
+ this.artifact = null;
197
+ this.confirmButton.disabled = true;
198
+ this.retryButton.hidden = true;
199
+ this.warning.hidden = true;
200
+ this.root.classList.add('is-loading');
201
+ this.state.hidden = false;
202
+ this.state.classList.remove('is-error');
203
+ this.state.querySelector('strong').textContent = '正在生成 SVG…';
204
+ this.summary.textContent = '正在准备完整内容';
205
+ this._revokePreviewUrl();
206
+ this.image.removeAttribute('src');
207
+ try {
208
+ const artifact = await this.createArtifact({ theme: this.theme, transparentBackground: this.transparentBackground, signal });
209
+ if (signal.aborted || this.closed) return;
210
+ this.artifact = artifact;
211
+ this.objectUrl = this.ownerWindow.URL.createObjectURL(artifact.blob);
212
+ this.image.addEventListener('load', () => {
213
+ if (!this.root?.isConnected) return;
214
+ this.state.hidden = true;
215
+ this.root.classList.remove('is-loading');
216
+ this.fit();
217
+ }, { once: true });
218
+ this.image.src = this.objectUrl;
219
+ this.confirmButton.disabled = false;
220
+ this.summary.textContent = `${artifact.filename} · ${Math.round(artifact.width)} × ${Math.round(artifact.height)}`;
221
+ this._renderWarnings(artifact.warnings || []);
222
+ } catch (error) {
223
+ if (signal.aborted || this.closed) return;
224
+ this.root.classList.remove('is-loading');
225
+ this.state.hidden = false;
226
+ this.state.classList.add('is-error');
227
+ this.state.querySelector('strong').textContent = `生成失败:${error?.message || '未知错误'}`;
228
+ this.summary.textContent = 'SVG 尚未生成';
229
+ this.retryButton.hidden = false;
230
+ }
231
+ }
232
+
233
+ _renderWarnings(warnings) {
234
+ this.warning.replaceChildren();
235
+ if (!warnings.length) { this.warning.hidden = true; return; }
236
+ this.warning.hidden = false;
237
+ this.warning.appendChild(element(this.document, 'strong', '', `${warnings.length} 项内容已降级处理`));
238
+ const list = element(this.document, 'ul');
239
+ warnings.slice(0, 5).forEach((item) => list.appendChild(element(this.document, 'li', '', item.message)));
240
+ if (warnings.length > 5) list.appendChild(element(this.document, 'li', '', `另有 ${warnings.length - 5} 项`));
241
+ this.warning.appendChild(list);
242
+ }
243
+
244
+ _revokePreviewUrl() {
245
+ if (this.objectUrl) this.ownerWindow.URL.revokeObjectURL(this.objectUrl);
246
+ this.objectUrl = null;
247
+ }
248
+
249
+ _applyTransform() {
250
+ this.stage.style.transform = `translate(${this.pan.x}px, ${this.pan.y}px) scale(${this.zoom})`;
251
+ this.zoomText.textContent = `${Math.round(this.zoom * 100)}%`;
252
+ }
253
+
254
+ _setZoom(value, anchor = null) {
255
+ const next = clamp(value, 0.08, 6);
256
+ if (anchor) {
257
+ this.pan.x = anchor.x - (anchor.x - this.pan.x) * (next / this.zoom);
258
+ this.pan.y = anchor.y - (anchor.y - this.pan.y) * (next / this.zoom);
259
+ }
260
+ this.zoom = next;
261
+ this._applyTransform();
262
+ }
263
+
264
+ _zoomBy(factor) {
265
+ const rect = this.viewport.getBoundingClientRect();
266
+ this._setZoom(this.zoom * factor, { x: rect.width / 2, y: rect.height / 2 });
267
+ }
268
+
269
+ fit() {
270
+ if (!this.artifact) return;
271
+ const rect = this.viewport.getBoundingClientRect();
272
+ const zoom = clamp(Math.min((rect.width - 48) / this.artifact.width, (rect.height - 48) / this.artifact.height), 0.08, 2);
273
+ this.zoom = zoom;
274
+ this.pan = { x: (rect.width - this.artifact.width * zoom) / 2, y: (rect.height - this.artifact.height * zoom) / 2 };
275
+ this._applyTransform();
276
+ }
277
+
278
+ actualSize() {
279
+ if (!this.artifact) return;
280
+ const rect = this.viewport.getBoundingClientRect();
281
+ this.zoom = 1;
282
+ this.pan = { x: (rect.width - this.artifact.width) / 2, y: (rect.height - this.artifact.height) / 2 };
283
+ this._applyTransform();
284
+ }
285
+
286
+ setTheme(theme) {
287
+ if (!['current', 'light', 'dark'].includes(theme) || theme === this.theme) return;
288
+ this.theme = theme;
289
+ this._syncThemeButtons();
290
+ this._generate();
291
+ }
292
+
293
+ setTransparentBackground(value) {
294
+ const next = Boolean(value);
295
+ if (next === this.transparentBackground) return;
296
+ this.transparentBackground = next;
297
+ this.transparentInput.checked = next;
298
+ this._generate();
299
+ }
300
+
301
+ focus() {
302
+ this.closeButton?.focus?.({ preventScroll: true });
303
+ }
304
+
305
+ close({ immediate = false } = {}) {
306
+ if (this.closed) return;
307
+ this.closed = true;
308
+ this.abortController?.abort();
309
+ this.document.removeEventListener('keydown', this._keyHandler);
310
+ this._revokePreviewUrl();
311
+ const finalize = () => {
312
+ this.root?.remove();
313
+ this._restoreFocus?.focus?.({ preventScroll: true });
314
+ this.onClose?.();
315
+ };
316
+ if (immediate || this.ownerWindow.matchMedia?.('(prefers-reduced-motion: reduce)').matches) finalize();
317
+ else {
318
+ this.root.classList.remove('is-open');
319
+ this.root.classList.add('is-closing');
320
+ this.ownerWindow.setTimeout(finalize, 190);
321
+ }
322
+ }
323
+ }
324
+
325
+ export function openSvgExportPreview(options) {
326
+ return new SvgExportPreviewController(options);
327
+ }