@stocksharp/diagram 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ssdiagram.js CHANGED
@@ -52,6 +52,7 @@ var SSDiagram = (() => {
52
52
  parseDiagramDocument: () => parseDiagramDocument,
53
53
  parseDiagramViewState: () => parseDiagramViewState,
54
54
  renderAll: () => renderAll,
55
+ renderDiagramSvg: () => renderDiagramSvg,
55
56
  renderFromInline: () => renderFromInline,
56
57
  renderFromSource: () => renderFromSource,
57
58
  renderScheme: () => renderScheme,
@@ -471,334 +472,6 @@ var SSDiagram = (() => {
471
472
  return value;
472
473
  }
473
474
 
474
- // src/core/action-registry.ts
475
- var DiagramActionRegistry = class {
476
- constructor() {
477
- this.actions = /* @__PURE__ */ new Map();
478
- }
479
- register(action) {
480
- if (this.actions.has(action.id)) {
481
- throw new Error(`Diagram action "${action.id}" is already registered.`);
482
- }
483
- this.actions.set(action.id, action);
484
- return () => {
485
- if (this.actions.get(action.id) === action) this.actions.delete(action.id);
486
- };
487
- }
488
- /**
489
- * Registers one action per id from a table keyed by TId, in key order.
490
- * Because the table is a total Record, adding a member to TId without adding
491
- * its action stops compiling instead of producing a command that silently
492
- * does nothing.
493
- */
494
- registerAll(actions) {
495
- const ids = Object.keys(actions);
496
- const disposers = ids.map((id) => {
497
- const action = actions[id];
498
- return this.register({
499
- id,
500
- canExecute: (context) => action.canExecute(context),
501
- execute: (context) => action.execute(context)
502
- });
503
- });
504
- return () => disposers.forEach((dispose) => dispose());
505
- }
506
- get(id) {
507
- return this.actions.get(id) ?? null;
508
- }
509
- states(context) {
510
- return [...this.actions.values()].map((action) => ({
511
- id: action.id,
512
- enabled: action.canExecute(context)
513
- }));
514
- }
515
- canExecute(id, context) {
516
- return this.actions.get(id)?.canExecute(context) ?? false;
517
- }
518
- execute(id, context) {
519
- const action = this.actions.get(id);
520
- if (action === void 0 || !action.canExecute(context)) return false;
521
- action.execute(context);
522
- return true;
523
- }
524
- };
525
-
526
- // src/core/history.ts
527
- var DiagramCommandHistory = class {
528
- constructor(listener) {
529
- this.listener = listener;
530
- this.undoCommands = [];
531
- this.redoCommands = [];
532
- this.transactions = [];
533
- this.replaying = false;
534
- }
535
- get state() {
536
- return {
537
- canUndo: this.undoCommands.length > 0,
538
- canRedo: this.redoCommands.length > 0,
539
- undoDepth: this.undoCommands.length,
540
- redoDepth: this.redoCommands.length,
541
- undoLabel: this.undoCommands[this.undoCommands.length - 1]?.label ?? null,
542
- redoLabel: this.redoCommands[this.redoCommands.length - 1]?.label ?? null
543
- };
544
- }
545
- execute(command) {
546
- if (this.replaying) {
547
- command.execute();
548
- return;
549
- }
550
- command.execute();
551
- this.recordApplied(command);
552
- }
553
- /** Records a gesture that already changed the document, such as pointer drag. */
554
- recordApplied(command) {
555
- if (this.replaying) return;
556
- const transaction = this.transactions[this.transactions.length - 1];
557
- if (transaction !== void 0) {
558
- transaction.commands.push(command);
559
- return;
560
- }
561
- this.undoCommands.push(command);
562
- this.redoCommands.length = 0;
563
- this.notify();
564
- }
565
- transaction(label2, action) {
566
- const frame = { label: label2, commands: [] };
567
- this.transactions.push(frame);
568
- try {
569
- const result = action();
570
- this.transactions.pop();
571
- const command = compose(frame);
572
- if (command !== null) this.recordApplied(command);
573
- return result;
574
- } catch (error) {
575
- this.transactions.pop();
576
- this.replay(() => {
577
- for (const command of frame.commands.slice().reverse()) command.undo();
578
- });
579
- throw error;
580
- }
581
- }
582
- assertNoOpenTransaction(operation) {
583
- if (this.transactions.length > 0) {
584
- throw new Error(`Cannot ${operation} command history inside a transaction.`);
585
- }
586
- }
587
- undo() {
588
- this.assertNoOpenTransaction("undo");
589
- const command = this.undoCommands.pop();
590
- if (command === void 0) return false;
591
- try {
592
- this.replay(() => command.undo());
593
- } catch (error) {
594
- this.undoCommands.push(command);
595
- throw error;
596
- }
597
- this.redoCommands.push(command);
598
- this.notify();
599
- return true;
600
- }
601
- redo() {
602
- this.assertNoOpenTransaction("redo");
603
- const command = this.redoCommands.pop();
604
- if (command === void 0) return false;
605
- try {
606
- this.replay(() => command.execute());
607
- } catch (error) {
608
- this.redoCommands.push(command);
609
- throw error;
610
- }
611
- this.undoCommands.push(command);
612
- this.notify();
613
- return true;
614
- }
615
- clear() {
616
- this.assertNoOpenTransaction("clear");
617
- this.undoCommands.length = 0;
618
- this.redoCommands.length = 0;
619
- this.notify();
620
- }
621
- replay(action) {
622
- const previous = this.replaying;
623
- this.replaying = true;
624
- try {
625
- action();
626
- } finally {
627
- this.replaying = previous;
628
- }
629
- }
630
- notify() {
631
- this.listener?.(this.state);
632
- }
633
- };
634
- function compose(frame) {
635
- if (frame.commands.length === 0) return null;
636
- if (frame.commands.length === 1) {
637
- const command = frame.commands[0];
638
- return frame.label === command.label ? command : {
639
- label: frame.label,
640
- execute: () => command.execute(),
641
- undo: () => command.undo()
642
- };
643
- }
644
- const commands = [...frame.commands];
645
- return {
646
- label: frame.label,
647
- execute: () => {
648
- for (const command of commands) command.execute();
649
- },
650
- undo: () => {
651
- for (const command of commands.slice().reverse()) command.undo();
652
- }
653
- };
654
- }
655
-
656
- // src/core/state.ts
657
- function createDiagramRuntimeState() {
658
- return { activeNodeId: null, nodes: {}, globalError: null };
659
- }
660
- function cloneDiagramRuntimeState(state) {
661
- return {
662
- activeNodeId: state.activeNodeId,
663
- globalError: state.globalError === null ? null : { ...state.globalError },
664
- nodes: Object.fromEntries(Object.entries(state.nodes).map(([nodeId, node]) => [nodeId, {
665
- active: node.active,
666
- errors: cloneDiagramNodeErrors(node.errors),
667
- ports: {
668
- in: Object.fromEntries(Object.entries(node.ports.in).map(([portId, port]) => [portId, { ...port }])),
669
- out: Object.fromEntries(Object.entries(node.ports.out).map(([portId, port]) => [portId, { ...port }]))
670
- }
671
- }]))
672
- };
673
- }
674
- function createDiagramViewState() {
675
- return { zoom: 1, panX: 0, panY: 0, overviewVisible: true };
676
- }
677
- function createDiagramSelection() {
678
- return {
679
- nodeIds: [],
680
- linkIds: [],
681
- port: null,
682
- primaryNodeId: null,
683
- primaryLinkId: null
684
- };
685
- }
686
- function createDiagramPortRuntimeState() {
687
- return {
688
- active: false,
689
- selected: false,
690
- breakpoint: false,
691
- breakpointActive: false,
692
- value: null,
693
- error: null
694
- };
695
- }
696
- function createEditableDiagramPermissions() {
697
- return {
698
- select: true,
699
- inspect: true,
700
- copy: true,
701
- moveNodes: true,
702
- createLinks: true,
703
- deleteSelection: true,
704
- paste: true,
705
- history: true
706
- };
707
- }
708
- function createReadOnlyDiagramPermissions() {
709
- return {
710
- select: true,
711
- inspect: true,
712
- copy: true,
713
- moveNodes: false,
714
- createLinks: false,
715
- deleteSelection: false,
716
- paste: false,
717
- history: false
718
- };
719
- }
720
- function createDiagramNodeRuntimeState() {
721
- return {
722
- active: false,
723
- errors: {},
724
- ports: { in: {}, out: {} }
725
- };
726
- }
727
- function cloneDiagramNodeErrors(errors) {
728
- const result = {};
729
- if (errors === void 0 || errors === null) return result;
730
- if (errors.runtime !== void 0) result.runtime = { ...errors.runtime };
731
- if (errors.load !== void 0) result.load = { ...errors.load };
732
- return result;
733
- }
734
-
735
- // src/core/view-state.ts
736
- var DIAGRAM_VIEW_STATE_VERSION = 1;
737
- var DiagramViewStateError = class extends Error {
738
- constructor(message, path = "$") {
739
- super(`${path}: ${message}`);
740
- this.path = path;
741
- this.name = "DiagramViewStateError";
742
- }
743
- };
744
- function createDiagramViewStateDocument(view = createDiagramViewState()) {
745
- return {
746
- version: DIAGRAM_VIEW_STATE_VERSION,
747
- view: normalizeView(view)
748
- };
749
- }
750
- function parseDiagramViewState(source) {
751
- let value = source;
752
- if (typeof source === "string") {
753
- try {
754
- value = JSON.parse(source);
755
- } catch (error) {
756
- const reason = error instanceof Error ? error.message : "invalid JSON";
757
- throw new DiagramViewStateError(reason);
758
- }
759
- }
760
- const root = requireObject2(value, "$");
761
- const version = requireFiniteNumber2(root.version, "$.version");
762
- if (version !== DIAGRAM_VIEW_STATE_VERSION) {
763
- throw new DiagramViewStateError(`unsupported view-state version ${version}`, "$.version");
764
- }
765
- return normalizeView(requireObject2(root.view, "$.view"));
766
- }
767
- function serializeDiagramViewState(view, space) {
768
- return JSON.stringify(createDiagramViewStateDocument(view), null, space);
769
- }
770
- function normalizeView(value) {
771
- const view = requireObject2(value, "$.view");
772
- const zoom = requireFiniteNumber2(view.zoom, "$.view.zoom");
773
- if (zoom <= 0) throw new DiagramViewStateError("expected a positive number", "$.view.zoom");
774
- return {
775
- zoom,
776
- panX: requireFiniteNumber2(view.panX, "$.view.panX"),
777
- panY: requireFiniteNumber2(view.panY, "$.view.panY"),
778
- overviewVisible: requireBoolean2(view.overviewVisible, "$.view.overviewVisible")
779
- };
780
- }
781
- function requireObject2(value, path) {
782
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
783
- throw new DiagramViewStateError("expected an object", path);
784
- }
785
- const prototype = Object.getPrototypeOf(value);
786
- if (prototype !== Object.prototype && prototype !== null) {
787
- throw new DiagramViewStateError("expected a plain object", path);
788
- }
789
- return value;
790
- }
791
- function requireFiniteNumber2(value, path) {
792
- if (typeof value !== "number" || !Number.isFinite(value)) {
793
- throw new DiagramViewStateError("expected a finite number", path);
794
- }
795
- return value;
796
- }
797
- function requireBoolean2(value, path) {
798
- if (typeof value !== "boolean") throw new DiagramViewStateError("expected a boolean", path);
799
- return value;
800
- }
801
-
802
475
  // src/svg-surface.ts
803
476
  var IDENTITY = [1, 0, 0, 1, 0, 0];
804
477
  function multiply(m, n2) {
@@ -1123,6 +796,215 @@ var SSDiagram = (() => {
1123
796
  }
1124
797
  };
1125
798
 
799
+ // src/core/history.ts
800
+ var DiagramCommandHistory = class {
801
+ constructor(listener) {
802
+ this.listener = listener;
803
+ this.undoCommands = [];
804
+ this.redoCommands = [];
805
+ this.transactions = [];
806
+ this.replaying = false;
807
+ }
808
+ get state() {
809
+ return {
810
+ canUndo: this.undoCommands.length > 0,
811
+ canRedo: this.redoCommands.length > 0,
812
+ undoDepth: this.undoCommands.length,
813
+ redoDepth: this.redoCommands.length,
814
+ undoLabel: this.undoCommands[this.undoCommands.length - 1]?.label ?? null,
815
+ redoLabel: this.redoCommands[this.redoCommands.length - 1]?.label ?? null
816
+ };
817
+ }
818
+ execute(command) {
819
+ if (this.replaying) {
820
+ command.execute();
821
+ return;
822
+ }
823
+ command.execute();
824
+ this.recordApplied(command);
825
+ }
826
+ /** Records a gesture that already changed the document, such as pointer drag. */
827
+ recordApplied(command) {
828
+ if (this.replaying) return;
829
+ const transaction = this.transactions[this.transactions.length - 1];
830
+ if (transaction !== void 0) {
831
+ transaction.commands.push(command);
832
+ return;
833
+ }
834
+ this.undoCommands.push(command);
835
+ this.redoCommands.length = 0;
836
+ this.notify();
837
+ }
838
+ transaction(label2, action) {
839
+ const frame = { label: label2, commands: [] };
840
+ this.transactions.push(frame);
841
+ try {
842
+ const result = action();
843
+ this.transactions.pop();
844
+ const command = compose(frame);
845
+ if (command !== null) this.recordApplied(command);
846
+ return result;
847
+ } catch (error) {
848
+ this.transactions.pop();
849
+ this.replay(() => {
850
+ for (const command of frame.commands.slice().reverse()) command.undo();
851
+ });
852
+ throw error;
853
+ }
854
+ }
855
+ assertNoOpenTransaction(operation) {
856
+ if (this.transactions.length > 0) {
857
+ throw new Error(`Cannot ${operation} command history inside a transaction.`);
858
+ }
859
+ }
860
+ undo() {
861
+ this.assertNoOpenTransaction("undo");
862
+ const command = this.undoCommands.pop();
863
+ if (command === void 0) return false;
864
+ try {
865
+ this.replay(() => command.undo());
866
+ } catch (error) {
867
+ this.undoCommands.push(command);
868
+ throw error;
869
+ }
870
+ this.redoCommands.push(command);
871
+ this.notify();
872
+ return true;
873
+ }
874
+ redo() {
875
+ this.assertNoOpenTransaction("redo");
876
+ const command = this.redoCommands.pop();
877
+ if (command === void 0) return false;
878
+ try {
879
+ this.replay(() => command.execute());
880
+ } catch (error) {
881
+ this.redoCommands.push(command);
882
+ throw error;
883
+ }
884
+ this.undoCommands.push(command);
885
+ this.notify();
886
+ return true;
887
+ }
888
+ clear() {
889
+ this.assertNoOpenTransaction("clear");
890
+ this.undoCommands.length = 0;
891
+ this.redoCommands.length = 0;
892
+ this.notify();
893
+ }
894
+ replay(action) {
895
+ const previous = this.replaying;
896
+ this.replaying = true;
897
+ try {
898
+ action();
899
+ } finally {
900
+ this.replaying = previous;
901
+ }
902
+ }
903
+ notify() {
904
+ this.listener?.(this.state);
905
+ }
906
+ };
907
+ function compose(frame) {
908
+ if (frame.commands.length === 0) return null;
909
+ if (frame.commands.length === 1) {
910
+ const command = frame.commands[0];
911
+ return frame.label === command.label ? command : {
912
+ label: frame.label,
913
+ execute: () => command.execute(),
914
+ undo: () => command.undo()
915
+ };
916
+ }
917
+ const commands = [...frame.commands];
918
+ return {
919
+ label: frame.label,
920
+ execute: () => {
921
+ for (const command of commands) command.execute();
922
+ },
923
+ undo: () => {
924
+ for (const command of commands.slice().reverse()) command.undo();
925
+ }
926
+ };
927
+ }
928
+
929
+ // src/core/state.ts
930
+ function createDiagramRuntimeState() {
931
+ return { activeNodeId: null, nodes: {}, globalError: null };
932
+ }
933
+ function cloneDiagramRuntimeState(state) {
934
+ return {
935
+ activeNodeId: state.activeNodeId,
936
+ globalError: state.globalError === null ? null : { ...state.globalError },
937
+ nodes: Object.fromEntries(Object.entries(state.nodes).map(([nodeId, node]) => [nodeId, {
938
+ active: node.active,
939
+ errors: cloneDiagramNodeErrors(node.errors),
940
+ ports: {
941
+ in: Object.fromEntries(Object.entries(node.ports.in).map(([portId, port]) => [portId, { ...port }])),
942
+ out: Object.fromEntries(Object.entries(node.ports.out).map(([portId, port]) => [portId, { ...port }]))
943
+ }
944
+ }]))
945
+ };
946
+ }
947
+ function createDiagramViewState() {
948
+ return { zoom: 1, panX: 0, panY: 0, overviewVisible: true };
949
+ }
950
+ function createDiagramSelection() {
951
+ return {
952
+ nodeIds: [],
953
+ linkIds: [],
954
+ port: null,
955
+ primaryNodeId: null,
956
+ primaryLinkId: null
957
+ };
958
+ }
959
+ function createDiagramPortRuntimeState() {
960
+ return {
961
+ active: false,
962
+ selected: false,
963
+ breakpoint: false,
964
+ breakpointActive: false,
965
+ value: null,
966
+ error: null
967
+ };
968
+ }
969
+ function createEditableDiagramPermissions() {
970
+ return {
971
+ select: true,
972
+ inspect: true,
973
+ copy: true,
974
+ moveNodes: true,
975
+ createLinks: true,
976
+ deleteSelection: true,
977
+ paste: true,
978
+ history: true
979
+ };
980
+ }
981
+ function createReadOnlyDiagramPermissions() {
982
+ return {
983
+ select: true,
984
+ inspect: true,
985
+ copy: true,
986
+ moveNodes: false,
987
+ createLinks: false,
988
+ deleteSelection: false,
989
+ paste: false,
990
+ history: false
991
+ };
992
+ }
993
+ function createDiagramNodeRuntimeState() {
994
+ return {
995
+ active: false,
996
+ errors: {},
997
+ ports: { in: {}, out: {} }
998
+ };
999
+ }
1000
+ function cloneDiagramNodeErrors(errors) {
1001
+ const result = {};
1002
+ if (errors === void 0 || errors === null) return result;
1003
+ if (errors.runtime !== void 0) result.runtime = { ...errors.runtime };
1004
+ if (errors.load !== void 0) result.load = { ...errors.load };
1005
+ return result;
1006
+ }
1007
+
1126
1008
  // src/canvas-renderer.ts
1127
1009
  var SCREEN_RENDER_OPTIONS = {
1128
1010
  grid: true,
@@ -4237,6 +4119,232 @@ ${runtime.error}`;
4237
4119
  return Math.hypot(px - cx, py - cy);
4238
4120
  }
4239
4121
 
4122
+ // src/headless.ts
4123
+ function createContext() {
4124
+ const own = {
4125
+ globalAlpha: 1,
4126
+ measureText: (text) => ({ width: text.length * 7 })
4127
+ };
4128
+ return new Proxy(own, {
4129
+ get: (target, property) => property in target ? target[property] : () => void 0,
4130
+ set: (target, property, value) => {
4131
+ target[property] = value;
4132
+ return true;
4133
+ }
4134
+ });
4135
+ }
4136
+ var StubElement = class {
4137
+ constructor() {
4138
+ this.style = {};
4139
+ this.className = "";
4140
+ this.tabIndex = 0;
4141
+ this.width = 0;
4142
+ this.height = 0;
4143
+ this.textContent = "";
4144
+ this.children = [];
4145
+ this.classList = { toggle: () => false, add: () => void 0, remove: () => void 0 };
4146
+ this.context = createContext();
4147
+ }
4148
+ appendChild(child) {
4149
+ this.children.push(child);
4150
+ return child;
4151
+ }
4152
+ removeChild(child) {
4153
+ const at = this.children.indexOf(child);
4154
+ if (at >= 0) this.children.splice(at, 1);
4155
+ return child;
4156
+ }
4157
+ remove() {
4158
+ }
4159
+ setAttribute() {
4160
+ }
4161
+ focus() {
4162
+ }
4163
+ addEventListener() {
4164
+ }
4165
+ removeEventListener() {
4166
+ }
4167
+ getContext() {
4168
+ return this.context;
4169
+ }
4170
+ getBoundingClientRect() {
4171
+ return { left: 0, top: 0, right: this.width, bottom: this.height, width: this.width, height: this.height, x: 0, y: 0, toJSON: () => ({}) };
4172
+ }
4173
+ get clientWidth() {
4174
+ return this.width;
4175
+ }
4176
+ get clientHeight() {
4177
+ return this.height;
4178
+ }
4179
+ };
4180
+ var GLOBAL_KEYS = ["window", "document", "requestAnimationFrame", "cancelAnimationFrame", "Image", "getComputedStyle", "performance"];
4181
+ function installGlobals() {
4182
+ const target = globalThis;
4183
+ if (target.document !== void 0)
4184
+ return () => void 0;
4185
+ const before = /* @__PURE__ */ new Map();
4186
+ for (const key of GLOBAL_KEYS) before.set(key, target[key]);
4187
+ const stubDocument = Object.assign(new StubElement(), {
4188
+ documentElement: new StubElement(),
4189
+ createElement: () => new StubElement()
4190
+ });
4191
+ Object.assign(target, {
4192
+ window: Object.assign(new StubElement(), { devicePixelRatio: 1 }),
4193
+ document: stubDocument,
4194
+ requestAnimationFrame: () => 1,
4195
+ cancelAnimationFrame: () => void 0,
4196
+ Image: class {
4197
+ },
4198
+ getComputedStyle: () => ({ getPropertyValue: () => "" }),
4199
+ // A host without a browser may have no clock either -- an embedded JS engine has none. The drawing
4200
+ // reads it for animation, and a still picture has no animation to advance.
4201
+ performance: globalThis.performance ?? { now: () => 0 }
4202
+ });
4203
+ return () => {
4204
+ for (const [key, value] of before) {
4205
+ if (value === void 0) delete target[key];
4206
+ else target[key] = value;
4207
+ }
4208
+ };
4209
+ }
4210
+ function renderDiagramSvg(source, options = {}) {
4211
+ const { hostWidth = 1200, hostHeight = 800, ...screenshot } = options;
4212
+ const restore = installGlobals();
4213
+ try {
4214
+ const host = new StubElement();
4215
+ host.width = hostWidth;
4216
+ host.height = hostHeight;
4217
+ const diagram = new Diagram({ host });
4218
+ try {
4219
+ diagram.loadDocument(parseDiagramDocument(source));
4220
+ return diagram.takeSvg({ scope: "content", ...screenshot });
4221
+ } finally {
4222
+ diagram.destroy();
4223
+ }
4224
+ } finally {
4225
+ restore();
4226
+ }
4227
+ }
4228
+
4229
+ // src/core/action-registry.ts
4230
+ var DiagramActionRegistry = class {
4231
+ constructor() {
4232
+ this.actions = /* @__PURE__ */ new Map();
4233
+ }
4234
+ register(action) {
4235
+ if (this.actions.has(action.id)) {
4236
+ throw new Error(`Diagram action "${action.id}" is already registered.`);
4237
+ }
4238
+ this.actions.set(action.id, action);
4239
+ return () => {
4240
+ if (this.actions.get(action.id) === action) this.actions.delete(action.id);
4241
+ };
4242
+ }
4243
+ /**
4244
+ * Registers one action per id from a table keyed by TId, in key order.
4245
+ * Because the table is a total Record, adding a member to TId without adding
4246
+ * its action stops compiling instead of producing a command that silently
4247
+ * does nothing.
4248
+ */
4249
+ registerAll(actions) {
4250
+ const ids = Object.keys(actions);
4251
+ const disposers = ids.map((id) => {
4252
+ const action = actions[id];
4253
+ return this.register({
4254
+ id,
4255
+ canExecute: (context) => action.canExecute(context),
4256
+ execute: (context) => action.execute(context)
4257
+ });
4258
+ });
4259
+ return () => disposers.forEach((dispose) => dispose());
4260
+ }
4261
+ get(id) {
4262
+ return this.actions.get(id) ?? null;
4263
+ }
4264
+ states(context) {
4265
+ return [...this.actions.values()].map((action) => ({
4266
+ id: action.id,
4267
+ enabled: action.canExecute(context)
4268
+ }));
4269
+ }
4270
+ canExecute(id, context) {
4271
+ return this.actions.get(id)?.canExecute(context) ?? false;
4272
+ }
4273
+ execute(id, context) {
4274
+ const action = this.actions.get(id);
4275
+ if (action === void 0 || !action.canExecute(context)) return false;
4276
+ action.execute(context);
4277
+ return true;
4278
+ }
4279
+ };
4280
+
4281
+ // src/core/view-state.ts
4282
+ var DIAGRAM_VIEW_STATE_VERSION = 1;
4283
+ var DiagramViewStateError = class extends Error {
4284
+ constructor(message, path = "$") {
4285
+ super(`${path}: ${message}`);
4286
+ this.path = path;
4287
+ this.name = "DiagramViewStateError";
4288
+ }
4289
+ };
4290
+ function createDiagramViewStateDocument(view = createDiagramViewState()) {
4291
+ return {
4292
+ version: DIAGRAM_VIEW_STATE_VERSION,
4293
+ view: normalizeView(view)
4294
+ };
4295
+ }
4296
+ function parseDiagramViewState(source) {
4297
+ let value = source;
4298
+ if (typeof source === "string") {
4299
+ try {
4300
+ value = JSON.parse(source);
4301
+ } catch (error) {
4302
+ const reason = error instanceof Error ? error.message : "invalid JSON";
4303
+ throw new DiagramViewStateError(reason);
4304
+ }
4305
+ }
4306
+ const root = requireObject2(value, "$");
4307
+ const version = requireFiniteNumber2(root.version, "$.version");
4308
+ if (version !== DIAGRAM_VIEW_STATE_VERSION) {
4309
+ throw new DiagramViewStateError(`unsupported view-state version ${version}`, "$.version");
4310
+ }
4311
+ return normalizeView(requireObject2(root.view, "$.view"));
4312
+ }
4313
+ function serializeDiagramViewState(view, space) {
4314
+ return JSON.stringify(createDiagramViewStateDocument(view), null, space);
4315
+ }
4316
+ function normalizeView(value) {
4317
+ const view = requireObject2(value, "$.view");
4318
+ const zoom = requireFiniteNumber2(view.zoom, "$.view.zoom");
4319
+ if (zoom <= 0) throw new DiagramViewStateError("expected a positive number", "$.view.zoom");
4320
+ return {
4321
+ zoom,
4322
+ panX: requireFiniteNumber2(view.panX, "$.view.panX"),
4323
+ panY: requireFiniteNumber2(view.panY, "$.view.panY"),
4324
+ overviewVisible: requireBoolean2(view.overviewVisible, "$.view.overviewVisible")
4325
+ };
4326
+ }
4327
+ function requireObject2(value, path) {
4328
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4329
+ throw new DiagramViewStateError("expected an object", path);
4330
+ }
4331
+ const prototype = Object.getPrototypeOf(value);
4332
+ if (prototype !== Object.prototype && prototype !== null) {
4333
+ throw new DiagramViewStateError("expected a plain object", path);
4334
+ }
4335
+ return value;
4336
+ }
4337
+ function requireFiniteNumber2(value, path) {
4338
+ if (typeof value !== "number" || !Number.isFinite(value)) {
4339
+ throw new DiagramViewStateError("expected a finite number", path);
4340
+ }
4341
+ return value;
4342
+ }
4343
+ function requireBoolean2(value, path) {
4344
+ if (typeof value !== "boolean") throw new DiagramViewStateError("expected a boolean", path);
4345
+ return value;
4346
+ }
4347
+
4240
4348
  // src/i18n.ts
4241
4349
  function t(key, fallback) {
4242
4350
  if (typeof window === "undefined") return fallback;
@@ -4735,6 +4843,8 @@ ${runtime.error}`;
4735
4843
  var StockSharpDiagram = class extends EventEmitter {
4736
4844
  constructor(options) {
4737
4845
  super();
4846
+ // Built only where a host asked for it: a control nobody asked to save gets no button at all.
4847
+ this.downloadButton = null;
4738
4848
  this.disposables = [];
4739
4849
  this.idCounter = 1;
4740
4850
  this.undoEnabled = true;
@@ -4744,6 +4854,8 @@ ${runtime.error}`;
4744
4854
  this.destroyed = false;
4745
4855
  this.fullscreen = false;
4746
4856
  this.fullscreenButtonVisible = true;
4857
+ this.downloadButtonVisible = false;
4858
+ this.downloadLabel = null;
4747
4859
  // null means the host never named the button, so its wording comes from the shared bundle.
4748
4860
  // Keeping the two apart is what lets an explicit label win while an untouched control still
4749
4861
  // follows the page's language instead of being stuck in English.
@@ -4766,6 +4878,8 @@ ${runtime.error}`;
4766
4878
  this.prepareFullscreenButtonHost();
4767
4879
  this.fullscreenButton = this.createFullscreenButton();
4768
4880
  this.updateFullscreenButton();
4881
+ this.downloadLabel = options.downloadLabel ?? null;
4882
+ this.setDownloadButtonVisible(options.showDownloadButton ?? false);
4769
4883
  this.setContextMenuEnabled(options.showContextMenu ?? true);
4770
4884
  this.canvas.setHistoryShortcutHandler((direction) => {
4771
4885
  if (direction === "undo") this.undo();
@@ -5002,6 +5116,28 @@ ${runtime.error}`;
5002
5116
  isFullscreen() {
5003
5117
  return this.fullscreen;
5004
5118
  }
5119
+ /** Shows or hides the button that asks the host to save the diagram. */
5120
+ setDownloadButtonVisible(visible) {
5121
+ this.downloadButtonVisible = visible;
5122
+ if (!visible) {
5123
+ if (this.downloadButton === null) return;
5124
+ this.downloadButton.hidden = true;
5125
+ this.downloadButton.style.display = "none";
5126
+ return;
5127
+ }
5128
+ this.downloadButton ?? (this.downloadButton = this.createDownloadButton());
5129
+ this.updateDownloadButton();
5130
+ this.downloadButton.hidden = false;
5131
+ this.downloadButton.style.display = "inline-flex";
5132
+ }
5133
+ isDownloadButtonVisible() {
5134
+ return this.downloadButtonVisible;
5135
+ }
5136
+ /** Text of the download button, for a host that renders in another language. */
5137
+ setDownloadLabel(label2) {
5138
+ this.downloadLabel = label2;
5139
+ this.updateDownloadButton();
5140
+ }
5005
5141
  setFullscreenButtonVisible(visible) {
5006
5142
  this.fullscreenButtonVisible = visible;
5007
5143
  this.fullscreenButton.hidden = !visible;
@@ -5287,6 +5423,49 @@ ${runtime.error}`;
5287
5423
  button.style.display = this.fullscreenButtonVisible ? "inline-flex" : "none";
5288
5424
  return button;
5289
5425
  }
5426
+ createDownloadButton() {
5427
+ const owner = this.div.ownerDocument ?? document;
5428
+ const button = owner.createElement("button");
5429
+ button.type = "button";
5430
+ button.className = "ssdiagram-download-button";
5431
+ button.setAttribute("data-ssdiagram-download-button", "");
5432
+ Object.assign(button.style, {
5433
+ position: "absolute",
5434
+ top: "8px",
5435
+ right: "46px",
5436
+ zIndex: "4",
5437
+ display: "inline-flex",
5438
+ alignItems: "center",
5439
+ justifyContent: "center",
5440
+ width: "30px",
5441
+ height: "30px",
5442
+ padding: "0",
5443
+ border: "1px solid var(--ssdiagram-control-border, var(--t-border, #3a4250))",
5444
+ borderRadius: "5px",
5445
+ background: "var(--ssdiagram-control-background, var(--t-surface, rgba(18, 21, 28, 0.9)))",
5446
+ color: "var(--ssdiagram-control-color, var(--t-text, #eaecef))",
5447
+ boxSizing: "border-box",
5448
+ cursor: "pointer",
5449
+ opacity: "0.9"
5450
+ });
5451
+ const click = () => {
5452
+ this.emit("exportRequested", { format: "document" });
5453
+ };
5454
+ button.addEventListener("click", click);
5455
+ this.div.appendChild(button);
5456
+ this.disposables.push(() => {
5457
+ button.removeEventListener("click", click);
5458
+ button.remove();
5459
+ });
5460
+ return button;
5461
+ }
5462
+ updateDownloadButton() {
5463
+ if (this.downloadButton === null) return;
5464
+ const label2 = this.downloadLabel ?? t("download", "Download");
5465
+ this.downloadButton.title = label2;
5466
+ this.downloadButton.setAttribute("aria-label", label2);
5467
+ this.downloadButton.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M8 11l4 4 4-4M5 19h14"/></svg>';
5468
+ }
5290
5469
  updateFullscreenButton() {
5291
5470
  const fullscreen = this.fullscreen;
5292
5471
  const labels = this.resolveFullscreenLabels();
@@ -6127,12 +6306,16 @@ ${runtime.error}`;
6127
6306
  div.classList.remove("ss-diagram-error");
6128
6307
  div.replaceChildren();
6129
6308
  const [enterLabel, exitLabel] = (div.dataset.diagramFullscreen ?? "").split("|");
6309
+ const downloadLabel = div.dataset.diagramDownload;
6310
+ const showDownloadButton = options.onExportRequested !== void 0;
6130
6311
  let diagram;
6131
6312
  try {
6132
6313
  diagram = new StockSharpDiagram({
6133
6314
  div,
6134
6315
  catalog,
6135
- ...enterLabel && exitLabel ? { fullscreenLabels: { enter: enterLabel, exit: exitLabel } } : {}
6316
+ ...enterLabel && exitLabel ? { fullscreenLabels: { enter: enterLabel, exit: exitLabel } } : {},
6317
+ ...showDownloadButton ? { showDownloadButton: true } : {},
6318
+ ...downloadLabel ? { downloadLabel } : {}
6136
6319
  });
6137
6320
  } catch (error) {
6138
6321
  div.replaceChildren();
@@ -6179,6 +6362,10 @@ ${runtime.error}`;
6179
6362
  activeRenders.set(div, handle);
6180
6363
  connectedRenders.add(handle);
6181
6364
  ensureDisconnectedHostObserver();
6365
+ if (options.onExportRequested !== void 0) {
6366
+ const notifyHost = options.onExportRequested;
6367
+ cleanups.push(diagram.on("exportRequested", (request) => notifyHost(request, handle)));
6368
+ }
6182
6369
  if (options.onFullscreenRequested !== void 0) {
6183
6370
  const notifyHost = options.onFullscreenRequested;
6184
6371
  cleanups.push(diagram.on("fullscreenRequested", (request) => notifyHost(request, handle)));