@diagrammo/dgmo 0.45.0 → 0.46.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.
@@ -45,8 +45,8 @@ import {
45
45
  resolveTheme,
46
46
  sanitizeSvgInPlace,
47
47
  deriveAriaLabel,
48
- buildSourcePanel,
49
- buildErrorBanner,
48
+ buildRenderedBlock,
49
+ buildErrorBlock,
50
50
  buildShareUrl,
51
51
  sharedWarn,
52
52
  type ThemePreference,
@@ -275,7 +275,9 @@ export class DgmoDiagram extends HTMLElement {
275
275
  line?: number;
276
276
  column?: number;
277
277
  }): void {
278
- this.mount(buildErrorBanner(opts), 'dgmo dgmo--error');
278
+ // The standard error card carries `.dgmo.dgmo--error` on its own root,
279
+ // so the container itself stays class-free.
280
+ this.mount(buildErrorBlock({ ...opts, source: this.source ?? '' }), '');
279
281
  }
280
282
 
281
283
  private async rerender(): Promise<void> {
@@ -289,9 +291,8 @@ export class DgmoDiagram extends HTMLElement {
289
291
 
290
292
  // Loading state (visible only while an async render/fetch is in flight).
291
293
  const loading = document.createElement('div');
292
- loading.className = 'dgmo-error-banner-loc';
293
294
  loading.textContent = 'Rendering diagram…';
294
- this.mount(loading, 'dgmo dgmo--loading');
295
+ this.mount(loading, 'dgmo--loading');
295
296
 
296
297
  const themePref = this.themePreference();
297
298
  const resolvedTheme =
@@ -382,22 +383,29 @@ export class DgmoDiagram extends HTMLElement {
382
383
  ? 'dgmo-theme-transparent'
383
384
  : 'dgmo-theme-light';
384
385
 
385
- const frag = document.createDocumentFragment();
386
- frag.appendChild(svgEl);
387
-
388
- // Showcase chrome: collapsible source panel + Copy + Open-in-editor.
389
- if (this.isShowcase()) {
390
- const shareUrl = buildShareUrl(src, {
391
- palette,
392
- theme: resolvedTheme === 'dark' ? 'dark' : 'light',
393
- editorBase: this.editorBase(),
394
- campaign: VERSION,
395
- utmSource: 'element-embed',
396
- });
397
- frag.appendChild(buildSourcePanel(src, shareUrl, true));
398
- }
399
-
400
- this.mount(frag, `dgmo-rendered ${themeClass} dgmo--rendered`);
386
+ // Showcase chrome: standard embed block (BL-114) with the hover-reveal
387
+ // toolbar (view-source toggle + Copy + Open-in-editor).
388
+ const showcase = this.isShowcase();
389
+ const shareUrl = showcase
390
+ ? buildShareUrl(src, {
391
+ palette,
392
+ theme: resolvedTheme === 'dark' ? 'dark' : 'light',
393
+ editorBase: this.editorBase(),
394
+ campaign: VERSION,
395
+ utmSource: 'element-embed',
396
+ })
397
+ : null;
398
+
399
+ const wrapper = buildRenderedBlock({
400
+ source: src,
401
+ svgEl,
402
+ themeClass,
403
+ showSource: showcase,
404
+ showEditorLink: showcase,
405
+ shareUrl,
406
+ });
407
+
408
+ this.mount(wrapper, 'dgmo--rendered');
401
409
  }
402
410
  }
403
411
 
@@ -2109,7 +2109,7 @@ function renderLegend(
2109
2109
  .attr('transform', `translate(0, ${legendY})`);
2110
2110
 
2111
2111
  if (activeGroup) {
2112
- legendG.attr('data-legend-active', activeGroup.toLowerCase());
2112
+ legendG.attr('data-legend-active', tagAttrKey(activeGroup));
2113
2113
  }
2114
2114
 
2115
2115
  // Build all groups including Playback as a regular group for consistent rendering
package/src/render.ts CHANGED
@@ -24,11 +24,29 @@ const DOM_GLOBALS = [
24
24
  'HTMLElement',
25
25
  'SVGElement',
26
26
  ] as const;
27
- let domRefCount = 0;
28
- let domInstallPromise: Promise<void> | null = null;
29
- let domInstalledByUs = false;
27
+ // The ref-count state lives on `globalThis` under a well-known symbol rather
28
+ // than in module state: dist/index and dist/block are each self-contained
29
+ // bundles, so a host importing both gets two copies of this module. With
30
+ // per-copy state, each copy keeps its own ref-count over the SAME
31
+ // `globalThis.document` — one copy's release tears down globals mid-render of
32
+ // the other. `Symbol.for` puts every copy on one shared count.
33
+ interface DomGlobalsState {
34
+ refCount: number;
35
+ installPromise: Promise<void> | null;
36
+ installedByUs: boolean;
37
+ }
38
+ const DOM_STATE_KEY = Symbol.for('diagrammo.dgmo.dom-globals');
39
+
40
+ function domState(): DomGlobalsState {
41
+ const g = globalThis as { [DOM_STATE_KEY]?: DomGlobalsState };
42
+ return (g[DOM_STATE_KEY] ??= {
43
+ refCount: 0,
44
+ installPromise: null,
45
+ installedByUs: false,
46
+ });
47
+ }
30
48
 
31
- async function installDom(): Promise<void> {
49
+ async function installDom(state: DomGlobalsState): Promise<void> {
32
50
  const { JSDOM } = await loadJsdom();
33
51
  // Concrete URL → non-opaque origin, so host code that touches
34
52
  // window.localStorage during a same-process render doesn't throw.
@@ -49,7 +67,7 @@ async function installDom(): Promise<void> {
49
67
  configurable: true,
50
68
  });
51
69
  }
52
- domInstalledByUs = true;
70
+ state.installedByUs = true;
53
71
  }
54
72
 
55
73
  /**
@@ -58,22 +76,24 @@ async function installDom(): Promise<void> {
58
76
  * we did not install). Pair every successful call with `releaseDom()`.
59
77
  */
60
78
  async function acquireDom(): Promise<void> {
61
- if (typeof document !== 'undefined' && !domInstalledByUs) return;
62
- domRefCount++;
63
- if (!domInstallPromise) domInstallPromise = installDom();
64
- await domInstallPromise;
79
+ const state = domState();
80
+ if (typeof document !== 'undefined' && !state.installedByUs) return;
81
+ state.refCount++;
82
+ if (!state.installPromise) state.installPromise = installDom(state);
83
+ await state.installPromise;
65
84
  }
66
85
 
67
86
  /** Tear down the jsdom globals once no render is in flight. */
68
87
  function releaseDom(): void {
69
- if (!domInstalledByUs) return;
70
- if (--domRefCount > 0) return;
88
+ const state = domState();
89
+ if (!state.installedByUs) return;
90
+ if (--state.refCount > 0) return;
71
91
  for (const key of DOM_GLOBALS) {
72
92
  delete (globalThis as Record<string, unknown>)[key];
73
93
  }
74
- domInstalledByUs = false;
75
- domInstallPromise = null;
76
- domRefCount = 0;
94
+ state.installedByUs = false;
95
+ state.installPromise = null;
96
+ state.refCount = 0;
77
97
  }
78
98
 
79
99
  /**
@@ -663,7 +663,7 @@ export function renderSitemap(
663
663
  .attr('class', 'sitemap-legend-fixed')
664
664
  .attr('transform', `translate(0, ${sDiagramPadding + fixedTitleH})`);
665
665
  if (activeTagGroup) {
666
- legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
666
+ legendParent.attr('data-legend-active', tagAttrKey(activeTagGroup));
667
667
  }
668
668
  renderLegend(
669
669
  legendParent,
@@ -1299,7 +1299,7 @@ function renderTimelineTagLegendOverlay(
1299
1299
  if (currentActiveGroup) {
1300
1300
  legendContainer.attr(
1301
1301
  'data-legend-active',
1302
- currentActiveGroup.toLowerCase()
1302
+ tagAttrKey(currentActiveGroup)
1303
1303
  );
1304
1304
  }
1305
1305
 
@@ -25,6 +25,8 @@
25
25
  // there is no opacity double-up. `render({ bakeHover: false })` opts out.
26
26
  // ============================================================
27
27
 
28
+ import { tagAttrKey } from './tag-groups';
29
+
28
30
  /** How a chart's cross-highlight rules are shaped. `self` = self-emphasis only
29
31
  * (no cross-highlight — single-series / non-relational charts). */
30
32
  export type HoverStrategy = 'enumerated' | 'structural' | 'connection' | 'self';
@@ -599,7 +601,10 @@ function deriveFromSvg(svg: string, spec: HoverSpec): HoverDerived {
599
601
  .querySelector('[data-legend-active]')
600
602
  ?.getAttribute('data-legend-active');
601
603
  if (slug) {
602
- groupAttr = `data-tag-${slug}`;
604
+ // Defensive re-slug: legacy SVGs (pre-0.46) carried the raw lowercased
605
+ // group name here, which is not a valid attribute name when the group
606
+ // name contains spaces/parens — querySelectorAll would throw.
607
+ groupAttr = `data-tag-${tagAttrKey(slug)}`;
603
608
  } else {
604
609
  // Fallback for charts with no legend-active marker (e.g. mindmap): if
605
610
  // the marks carry exactly ONE distinct `data-tag-*` group, use it.
@@ -74,10 +74,7 @@ export function renderLegendD3(
74
74
 
75
75
  // Set active attribute on container
76
76
  if (currentState.activeGroup) {
77
- legendG.attr(
78
- 'data-legend-active',
79
- currentState.activeGroup.toLowerCase()
80
- );
77
+ legendG.attr('data-legend-active', tagAttrKey(currentState.activeGroup));
81
78
  } else {
82
79
  legendG.attr('data-legend-active', null);
83
80
  }
@@ -180,7 +180,7 @@ export function renderLegendSvg(
180
180
 
181
181
  const classAttr = className ? ` class="${esc(className)}"` : '';
182
182
  const activeAttr = activeGroup
183
- ? ` data-legend-active="${esc(activeGroup.toLowerCase())}"`
183
+ ? ` data-legend-active="${esc(tagAttrKey(activeGroup))}"`
184
184
  : '';
185
185
  const svg = `<g${classAttr}${activeAttr}>${parts.join('')}</g>`;
186
186