@stocksharp/diagram 0.5.0 → 0.6.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.
@@ -28,6 +28,7 @@ import {
28
28
  } from '../canvas-renderer.js';
29
29
  import { StockSharpCatalog } from './catalog.js';
30
30
  import { ContextMenuView } from './context-menu.js';
31
+ import { t } from '../i18n.js';
31
32
  import { EventEmitter } from './event-emitter.js';
32
33
  import type {
33
34
  DiagramEvents,
@@ -80,10 +81,16 @@ const CONTEXT_MENU: readonly ContextMenuEntry[] = [
80
81
  'open',
81
82
  'delete',
82
83
  { group: 'export', commands: ['exportDocument', 'exportPng', 'exportSvg'] },
84
+ 'overview',
83
85
  'properties',
84
86
  'help',
85
87
  ];
86
88
 
89
+ /** Commands that toggle rather than do, so the menu reports their state instead of just enabling them. */
90
+ const CONTEXT_CHECKED: Partial<Record<ContextCommand, (diagram: StockSharpDiagram) => boolean>> = {
91
+ overview: (diagram) => diagram.getViewState().overviewVisible,
92
+ };
93
+
87
94
  export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
88
95
  private readonly div: HTMLElement;
89
96
  private readonly catalog: StockSharpCatalog;
@@ -101,7 +108,10 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
101
108
  private destroyed = false;
102
109
  private fullscreen = false;
103
110
  private fullscreenButtonVisible = true;
104
- private fullscreenLabels: DiagramFullscreenLabels = { enter: 'Enter fullscreen', exit: 'Exit fullscreen' };
111
+ // null means the host never named the button, so its wording comes from the shared bundle.
112
+ // Keeping the two apart is what lets an explicit label win while an untouched control still
113
+ // follows the page's language instead of being stuck in English.
114
+ private fullscreenLabels: DiagramFullscreenLabels | null = null;
105
115
  private readonly contextActions = new DiagramActionRegistry<ContextCommand, ContextActionContext>();
106
116
  private contextMenu: ContextMenuView | null = null;
107
117
 
@@ -119,7 +129,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
119
129
  gridSize: options.gridSize,
120
130
  });
121
131
  this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
122
- this.fullscreenLabels = options.fullscreenLabels ?? this.fullscreenLabels;
132
+ this.fullscreenLabels = options.fullscreenLabels ?? null;
123
133
  this.prepareFullscreenButtonHost();
124
134
  this.fullscreenButton = this.createFullscreenButton();
125
135
  this.updateFullscreenButton();
@@ -443,8 +453,24 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
443
453
  this.updateFullscreenButton();
444
454
  }
445
455
 
456
+ /**
457
+ * Re-reads the shared bundle for text the control renders itself. Call it after changing
458
+ * the page language: the context menu is rebuilt on every open and needs nothing, but the
459
+ * fullscreen button is drawn once and would otherwise keep the old wording.
460
+ */
461
+ refreshLabels(): void {
462
+ this.updateFullscreenButton();
463
+ }
464
+
446
465
  getFullscreenLabels(): DiagramSnapshot<DiagramFullscreenLabels> {
447
- return { ...this.fullscreenLabels };
466
+ return { ...this.resolveFullscreenLabels() };
467
+ }
468
+
469
+ private resolveFullscreenLabels(): DiagramFullscreenLabels {
470
+ return this.fullscreenLabels ?? {
471
+ enter: t('fullscreenEnter', 'Enter fullscreen'),
472
+ exit: t('fullscreenExit', 'Exit fullscreen'),
473
+ };
448
474
  }
449
475
 
450
476
  /** Updates only the control's state after the host changed its own layout. */
@@ -565,10 +591,14 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
565
591
 
566
592
  getContextCommands(): ContextMenuItemState[] {
567
593
  const context = this.contextActionContext();
568
- const state = (command: ContextCommand): ContextCommandState => ({
569
- command,
570
- enabled: this.contextActions.canExecute(command, context),
571
- });
594
+ const state = (command: ContextCommand): ContextCommandState => {
595
+ const checked = CONTEXT_CHECKED[command];
596
+ return {
597
+ command,
598
+ enabled: this.contextActions.canExecute(command, context),
599
+ ...(checked === undefined ? {} : { checked: checked(this) }),
600
+ };
601
+ };
572
602
  return CONTEXT_MENU.map((entry) => {
573
603
  if (typeof entry === 'string') return state(entry);
574
604
  const commands = entry.commands.map(state);
@@ -709,7 +739,8 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
709
739
 
710
740
  private updateFullscreenButton(): void {
711
741
  const fullscreen = this.fullscreen;
712
- const label = fullscreen ? this.fullscreenLabels.exit : this.fullscreenLabels.enter;
742
+ const labels = this.resolveFullscreenLabels();
743
+ const label = fullscreen ? labels.exit : labels.enter;
713
744
  const path = fullscreen
714
745
  ? 'M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5'
715
746
  : 'M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5';
@@ -886,6 +917,12 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
886
917
  canExecute: ({ document }) => document.nodes.length > 0,
887
918
  execute: () => this.emit('exportRequested', { format: 'svg' }),
888
919
  },
920
+ // Always available: it shows the overview as readily as it hides it, and an empty
921
+ // diagram is exactly when someone may want the panel out of the way.
922
+ overview: {
923
+ canExecute: () => true,
924
+ execute: () => this.setOverviewVisible(!this.getViewState().overviewVisible),
925
+ },
889
926
  properties: {
890
927
  canExecute: ({ nodes }) => nodes.length > 0,
891
928
  execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
package/src/embed.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { t } from './i18n.js';
1
2
  // Shared read-only diagram embed layer. Rendering, palette loading and theming
2
3
  // live next to the diagram engine so web applications do not copy that logic.
3
4
  //
@@ -407,7 +408,7 @@ async function renderSchemeAtRevision(
407
408
  diagram.setLinkValidator(() => true);
408
409
  diagram.setOverviewVisible(false);
409
410
  const missingTypeTemplate = div.dataset.diagramMissingElement
410
- ?? 'Element type "{typeId}" is missing from the palette.';
411
+ ?? t('embedMissingElement', 'Element type "{typeId}" is missing from the palette.');
411
412
  const converted = toDiagramNodes(
412
413
  scheme,
413
414
  catalog,
@@ -508,10 +509,12 @@ interface EmbedErrorTexts {
508
509
  // default would never fire and the note would render blank.
509
510
  function errorTexts(div: HTMLElement): EmbedErrorTexts {
510
511
  const parts = (div.dataset.diagramErrors ?? '').split('|');
512
+ // The attribute stays the override; without it the wording comes from the shared bundle, so
513
+ // a page that already translated the menu does not have to repeat itself per embed.
511
514
  return {
512
- load: parts[0] || 'Diagram source could not be loaded.',
513
- empty: parts[1] || 'Diagram is empty or malformed.',
514
- draw: parts[2] || 'Diagram could not be rendered.',
515
+ load: parts[0] || t('embedErrorLoad', 'Diagram source could not be loaded.'),
516
+ empty: parts[1] || t('embedErrorEmpty', 'Diagram is empty or malformed.'),
517
+ draw: parts[2] || t('embedErrorDraw', 'Diagram could not be rendered.'),
515
518
  };
516
519
  }
517
520
 
package/src/i18n.ts CHANGED
@@ -95,6 +95,11 @@ export interface DesignerI18n {
95
95
  copy?: string;
96
96
  paste?: string;
97
97
  ctxOpen?: string;
98
+ // Deletes the selected nodes and links. Deliberately not the solution explorer's
99
+ // `delete`, which removes a whole strategy: same English word, but the object differs,
100
+ // and a language that declines it -- or a host that writes "Delete strategy..." -- cannot
101
+ // serve both from one key.
102
+ ctxDelete?: string;
98
103
  // Submenu title, and deliberately not `ctxExport` -- that one is the solution
99
104
  // explorer's "export this strategy", a different action that a host may well word
100
105
  // differently. Reads as its own item too: "Export as > SVG image".
@@ -102,8 +107,23 @@ export interface DesignerI18n {
102
107
  ctxExportDocument?: string;
103
108
  ctxExportPng?: string;
104
109
  ctxExportSvg?: string;
110
+ ctxOverview?: string;
105
111
  properties?: string;
106
112
  ctxHelp?: string;
113
+
114
+ // Everything else the control renders by itself. Each of these also has a per-instance
115
+ // override -- the fullscreenLabels option, or a data-diagram-* attribute on an embedded
116
+ // host -- and that override wins; these are the defaults when none was given.
117
+ fullscreenEnter?: string;
118
+ fullscreenExit?: string;
119
+ embedErrorLoad?: string;
120
+ embedErrorEmpty?: string;
121
+ embedErrorDraw?: string;
122
+ embedMissingElement?: string;
123
+
124
+ // Not the diagram menu: it has no collapse/expand command. Kept for hosts already
125
+ // shipping them, but moved out of the block above so nobody translates them expecting
126
+ // to see them in the context menu.
107
127
  collapse?: string;
108
128
  expand?: string;
109
129
 
@@ -205,13 +225,21 @@ export interface DesignerI18n {
205
225
  opterr_plotly_missing?: string;
206
226
  }
207
227
 
208
- const i18n: DesignerI18n =
209
- typeof window === 'undefined'
210
- ? {}
211
- : (window as unknown as { __designerI18n?: DesignerI18n }).__designerI18n ?? {};
212
-
213
228
  /// Localized string for `key`, or `fallback` (English) when the host
214
229
  /// didn't provide it.
230
+ ///
231
+ /// The bundle is read on every call rather than captured when this module loads. Capturing
232
+ /// it meant the host had to assign `window.__designerI18n` before the bundle evaluated --
233
+ /// the opposite of the usual order, and impossible for a lazily loaded chunk -- and a host
234
+ /// that assigned it afterwards silently got English with nothing to explain why. Reading it
235
+ /// live also makes assigning and mutating the object behave the same, and lets the language
236
+ /// change mid-session: the only thing reading these keys is the context menu, and it is
237
+ /// built fresh on every right-click.
238
+ ///
239
+ /// An empty string counts as missing. Half-finished translation exports routinely emit one,
240
+ /// and a blank menu entry is worse than an English one.
215
241
  export function t(key: keyof DesignerI18n, fallback: string): string {
216
- return i18n[key] ?? fallback;
242
+ if (typeof window === 'undefined') return fallback;
243
+ const value = (window as unknown as { __designerI18n?: DesignerI18n }).__designerI18n?.[key];
244
+ return value === undefined || value === '' ? fallback : value;
217
245
  }
package/src/index.ts CHANGED
@@ -85,6 +85,11 @@ export {
85
85
  StockSharpDiagram,
86
86
  } from './diagram/stocksharp-diagram.js';
87
87
 
88
+ // The menu resolves its labels through this, so a host needs the type to write a bundle the
89
+ // compiler can check. Without it the key names get hand-copied, and a typo shows up only as an
90
+ // English label on screen.
91
+ export type { DesignerI18n } from './i18n.js';
92
+
88
93
  export type {
89
94
  ContextCommand,
90
95
  ContextCommandGroup,