@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.
@@ -2,15 +2,16 @@
2
2
  * Shared browser-embed helpers for the `@diagrammo/dgmo` client-side
3
3
  * drop-ins (`./auto` script-tag renderer and `./element` custom element).
4
4
  *
5
- * These are the SVG sanitizer, theme resolver, source panel (with Copy +
6
- * "Open in editor"), error banner, aria-label derivation, style injection,
7
- * and share-URL builder — factored out of `auto/index.ts` so the two entries
8
- * share one implementation rather than diverging copies. The `auto` entry's
9
- * public `window.dgmo` surface is unaffected; it imports from here.
5
+ * These are the SVG sanitizer, theme resolver, the standard-block DOM
6
+ * assembler (canonical chrome from `src/embed` — BL-114), the error card,
7
+ * aria-label derivation, style injection, and share-URL builder — factored
8
+ * out of `auto/index.ts` so the two entries share one implementation rather
9
+ * than diverging copies. The `auto` entry's public `window.dgmo` surface is
10
+ * unaffected; it imports from here.
10
11
  */
11
12
 
12
13
  import { encodeDiagramUrl } from '../sharing';
13
- import { highlightDgmo, type HighlightToken } from '../editor/highlight-api';
14
+ import { buildDgmoBlockHtml, errorBlockHtml } from '../embed';
14
15
  import { CSS } from './styles';
15
16
  import { safeHref } from '../utils/safe-href';
16
17
 
@@ -22,10 +23,10 @@ export const VERSION: string =
22
23
  (typeof __DGMO_VERSION__ === 'string' && __DGMO_VERSION__) || 'dev';
23
24
 
24
25
  /** Default hosted editor used by "Open in editor" share links. */
25
- export const EDITOR_BASE_URL = 'https://online.diagrammo.app';
26
+ export { EDITOR_BASE_URL } from '../embed';
26
27
 
27
28
  const ARIA_LABEL_MAX = 200;
28
- const COPIED_INTERACTION_MS = 1200;
29
+ const COPIED_INTERACTION_MS = 1500;
29
30
  const LOG_PREFIX = '[dgmo]';
30
31
 
31
32
  // Bidi override + control char strip for aria-label sanitization. The regex
@@ -148,156 +149,149 @@ export function deriveAriaLabel(source: string): string {
148
149
  }
149
150
 
150
151
  // ============================================================
151
- // Source panel construction
152
+ // Standard block assembly (canonical chrome from src/embed — BL-114)
152
153
  // ============================================================
153
154
 
154
- function buildHighlightedSource(source: string): DocumentFragment {
155
- const frag = document.createDocumentFragment();
156
- let tokens: HighlightToken[];
157
- try {
158
- tokens = highlightDgmo(source);
159
- } catch {
160
- frag.appendChild(document.createTextNode(source));
161
- return frag;
162
- }
163
- for (const tok of tokens) {
164
- if (!tok.text) continue;
165
- const span = document.createElement('span');
166
- span.className = `dgmo-tok-${tok.role}`;
167
- span.textContent = tok.text;
168
- frag.appendChild(span);
169
- }
170
- return frag;
155
+ export interface RenderedBlockOptions {
156
+ /** Trimmed DGMO source (copy payload + highlighted panel content). */
157
+ source: string;
158
+ /** Already-rendered AND sanitized SVG element to mount as the diagram. */
159
+ svgEl: Element;
160
+ /** `dgmo-theme-light` / `dgmo-theme-dark` / `dgmo-theme-transparent`. */
161
+ themeClass: string;
162
+ /** Emit the source disclosure + toolbar (showcase) or diagram-only. */
163
+ showSource: boolean;
164
+ /** Emit the open-in-editor toolbar link at all. */
165
+ showEditorLink: boolean;
166
+ /**
167
+ * Pre-built share URL (with the surface's UTM params) for the
168
+ * open-in-editor link. `null` (encode failed / too large) drops the link.
169
+ */
170
+ shareUrl: string | null;
171
171
  }
172
172
 
173
- // ----- icon SVGs (static author-controlled markup) -----
174
- const COPY_ICON_SVG =
175
- '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5V3a1.5 1.5 0 0 0-1.5-1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h2.5"/></svg>';
176
- const CHECK_ICON_SVG =
177
- '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 8.5 6.5 12 13 4.5"/></svg>';
178
- const EXTERNAL_ICON_SVG =
179
- '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2h5v5"/><path d="M14 2L7 9"/><path d="M13 9v4a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h4"/></svg>';
180
-
181
- function setIcon(el: Element, svg: string): void {
182
- // Static icon strings authored above — no user content.
183
- el.innerHTML = svg;
173
+ /**
174
+ * Assemble the standard DGMO embed block (`figure.dgmo` + hover-reveal icon
175
+ * toolbar + native `<details>` source panel) around an already-sanitized SVG
176
+ * node, and bind the toolbar's copy/open click handling.
177
+ *
178
+ * The markup comes from the canonical `buildDgmoBlockHtml` in `src/embed`;
179
+ * this wrapper only (1) mounts the live SVG node into the `.dgmo-svg` slot,
180
+ * (2) swaps the open-in-editor href for the surface's UTM-tagged share URL,
181
+ * and (3) attaches the click handler that `<details>` can't provide (copy).
182
+ */
183
+ export function buildRenderedBlock(opts: RenderedBlockOptions): HTMLElement {
184
+ const html = buildDgmoBlockHtml(opts.source, '<div class="dgmo-svg"></div>', {
185
+ mode: opts.showSource ? 'showcase' : 'diagram',
186
+ showOpenInEditor: opts.showSource && opts.showEditorLink,
187
+ // Keep the legacy `.dgmo-rendered` + theme hooks so the anti-flash CSS,
188
+ // idempotency filters, and theme-scoped token colors keep working.
189
+ legacyClassNames: ['dgmo-rendered', opts.themeClass],
190
+ });
191
+ const holder = document.createElement('div');
192
+ // buildDgmoBlockHtml output is author-controlled markup with all
193
+ // user-supplied strings escaped; the SVG slot is empty at this point.
194
+ holder.innerHTML = html;
195
+ const wrapper = holder.firstElementChild as HTMLElement;
196
+
197
+ // Mount the sanitized SVG node directly (no serialize/re-parse round trip).
198
+ wrapper.querySelector('.dgmo-svg')?.appendChild(opts.svgEl);
199
+
200
+ // The canonical builder encodes a bare share URL; the browser surfaces
201
+ // carry UTM attribution, so swap in the pre-built URL (or drop the link
202
+ // when encoding failed — copy remains as the fallback affordance).
203
+ const open = wrapper.querySelector<HTMLAnchorElement>('a.dgmo-open');
204
+ if (open) {
205
+ if (opts.shareUrl) open.href = opts.shareUrl;
206
+ else open.remove();
207
+ }
208
+
209
+ bindBlockToolbar(wrapper);
210
+ return wrapper;
184
211
  }
185
212
 
186
- export function buildSourcePanel(
187
- source: string,
188
- shareUrl: string | null,
189
- showEditorLink: boolean
190
- ): HTMLDivElement {
191
- const panel = document.createElement('div');
192
- panel.className = 'dgmo-source-panel';
193
-
194
- const toggle = document.createElement('button');
195
- toggle.type = 'button';
196
- toggle.className = 'dgmo-source-toggle';
197
- toggle.setAttribute('aria-expanded', 'false');
198
- const chevron = document.createElement('span');
199
- chevron.className = 'dgmo-chevron';
200
- chevron.setAttribute('aria-hidden', 'true');
201
- chevron.textContent = '▸';
202
- toggle.appendChild(chevron);
203
- const label = document.createElement('span');
204
- label.textContent = 'DGMO source';
205
- toggle.appendChild(label);
206
-
207
- const body = document.createElement('div');
208
- body.className = 'dgmo-source-body';
209
- const pre = document.createElement('pre');
210
- pre.className = 'dgmo-source-pre';
211
- pre.appendChild(buildHighlightedSource(source));
212
- body.appendChild(pre);
213
-
214
- const actions = document.createElement('div');
215
- actions.className = 'dgmo-source-actions';
216
-
217
- const copyBtn = document.createElement('button');
218
- copyBtn.type = 'button';
219
- copyBtn.className = 'dgmo-btn dgmo-btn-copy';
220
- copyBtn.setAttribute('aria-label', 'Copy DGMO source');
221
- copyBtn.title = 'Copy source';
222
- setIcon(copyBtn, COPY_ICON_SVG);
223
- copyBtn.addEventListener('click', () => {
224
- void copySource(source, copyBtn);
213
+ /**
214
+ * Click handling for the standard block toolbar (mirrors remark-dgmo's
215
+ * `bindDgmo` reference implementation, but bound per-wrapper because these
216
+ * surfaces build their DOM imperatively).
217
+ *
218
+ * The toolbar is the `<summary>` of the source `<details>`: clicks on the
219
+ * `</>` toggle keep the native default (toggling), while clicks on
220
+ * `.dgmo-toolbar-btn` (copy / open) must preventDefault so they don't ALSO
221
+ * toggle — which cancels an anchor's navigation, hence the manual
222
+ * `window.open`.
223
+ */
224
+ function bindBlockToolbar(wrapper: HTMLElement): void {
225
+ wrapper.addEventListener('click', (e) => {
226
+ void handleToolbarClick(e);
225
227
  });
226
- actions.appendChild(copyBtn);
227
-
228
- if (showEditorLink) {
229
- const editorBtn = document.createElement('a');
230
- editorBtn.className = 'dgmo-btn dgmo-btn-editor';
231
- editorBtn.target = '_blank';
232
- editorBtn.rel = 'noopener noreferrer';
233
- editorBtn.setAttribute('aria-label', 'Open in editor');
234
- setIcon(editorBtn, EXTERNAL_ICON_SVG);
235
- if (shareUrl) {
236
- editorBtn.href = shareUrl;
237
- editorBtn.title = 'Open in editor';
228
+ }
229
+
230
+ async function handleToolbarClick(e: Event): Promise<void> {
231
+ const target = e.target as Element | null;
232
+ if (!target || typeof target.closest !== 'function') return;
233
+ const btn = target.closest('.dgmo-toolbar-btn') as HTMLElement | null;
234
+ if (!btn) return;
235
+
236
+ const insideSummary = !!btn.closest('summary');
237
+ if (insideSummary) e.preventDefault();
238
+
239
+ if (btn.matches('button.dgmo-copy')) {
240
+ const copied = await copyText(btn.dataset['dgmoSource'] ?? '');
241
+ if (copied) {
242
+ btn.classList.add('dgmo-copy--success');
243
+ setTimeout(
244
+ () => btn.classList.remove('dgmo-copy--success'),
245
+ COPIED_INTERACTION_MS
246
+ );
238
247
  } else {
239
- editorBtn.setAttribute('aria-disabled', 'true');
240
- editorBtn.title =
241
- 'Diagram too large for share link; copy source and paste into editor';
242
- editorBtn.addEventListener('click', (e) => e.preventDefault());
248
+ sharedWarn('clipboard write failed');
243
249
  }
244
- actions.appendChild(editorBtn);
250
+ return;
245
251
  }
246
252
 
247
- body.appendChild(actions);
248
- panel.appendChild(toggle);
249
- panel.appendChild(body);
250
-
251
- toggle.addEventListener('click', () => {
252
- const open = body.classList.toggle('dgmo-open');
253
- toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
254
- });
255
-
256
- return panel;
253
+ if (insideSummary && btn.matches('a.dgmo-open')) {
254
+ const anchor = btn as HTMLAnchorElement;
255
+ if (anchor.href) {
256
+ window.open(
257
+ anchor.href,
258
+ anchor.target || '_blank',
259
+ 'noopener,noreferrer'
260
+ );
261
+ }
262
+ }
257
263
  }
258
264
 
259
- async function copySource(
260
- source: string,
261
- btn: HTMLButtonElement
262
- ): Promise<void> {
263
- let copied = false;
265
+ async function copyText(text: string): Promise<boolean> {
264
266
  try {
265
267
  if (navigator?.clipboard?.writeText) {
266
- await navigator.clipboard.writeText(source);
267
- copied = true;
268
+ await navigator.clipboard.writeText(text);
269
+ return true;
268
270
  }
269
271
  } catch {
270
272
  // fallthrough to execCommand
271
273
  }
272
- if (!copied && typeof document !== 'undefined') {
274
+ if (typeof document !== 'undefined') {
273
275
  try {
274
276
  const ta = document.createElement('textarea');
275
- ta.value = source;
277
+ ta.value = text;
276
278
  ta.setAttribute('readonly', '');
277
279
  ta.style.position = 'absolute';
278
280
  ta.style.left = '-9999px';
279
281
  document.body.appendChild(ta);
280
282
  ta.select();
281
- copied = document.execCommand('copy');
283
+ const copied = document.execCommand('copy');
282
284
  document.body.removeChild(ta);
285
+ return copied;
283
286
  } catch {
284
- copied = false;
287
+ return false;
285
288
  }
286
289
  }
287
- if (copied) {
288
- setIcon(btn, CHECK_ICON_SVG);
289
- btn.classList.add('dgmo-btn-copied');
290
- setTimeout(() => {
291
- setIcon(btn, COPY_ICON_SVG);
292
- btn.classList.remove('dgmo-btn-copied');
293
- }, COPIED_INTERACTION_MS);
294
- } else {
295
- sharedWarn('clipboard write failed');
296
- }
290
+ return false;
297
291
  }
298
292
 
299
293
  // ============================================================
300
- // Error banner
294
+ // Error card (canonical errorBlockHtml from src/embed)
301
295
  // ============================================================
302
296
 
303
297
  export interface BannerOptions {
@@ -307,26 +301,32 @@ export interface BannerOptions {
307
301
  column?: number;
308
302
  }
309
303
 
310
- export function buildErrorBanner(opts: BannerOptions): HTMLDivElement {
311
- const banner = document.createElement('div');
312
- banner.className = 'dgmo-error-banner';
313
- banner.setAttribute('role', 'alert');
314
-
315
- const title = document.createElement('div');
316
- title.className = 'dgmo-error-banner-title';
317
- title.textContent = `${opts.severity ?? 'error'}: ${opts.message}`;
318
- banner.appendChild(title);
319
-
304
+ /**
305
+ * Build the standard `.dgmo--error` card as a live DOM element. Message +
306
+ * location are folded into one line; the offending source is shown inside
307
+ * the card (canonical shape — same as remark-dgmo and the other adopters).
308
+ */
309
+ export function buildErrorBlock(
310
+ opts: BannerOptions & { source: string }
311
+ ): HTMLElement {
312
+ let message =
313
+ opts.severity && opts.severity !== 'error'
314
+ ? `${opts.severity}: ${opts.message}`
315
+ : opts.message;
320
316
  if (opts.line !== undefined && opts.line > 0) {
321
- const loc = document.createElement('div');
322
- loc.className = 'dgmo-error-banner-loc';
323
- loc.textContent =
317
+ message +=
324
318
  opts.column !== undefined
325
- ? `at ${opts.line}:${opts.column}`
326
- : `at line ${opts.line}`;
327
- banner.appendChild(loc);
319
+ ? ` (at ${opts.line}:${opts.column})`
320
+ : ` (at line ${opts.line})`;
328
321
  }
329
- return banner;
322
+ const holder = document.createElement('div');
323
+ // errorBlockHtml escapes both message and source.
324
+ holder.innerHTML = errorBlockHtml(new Error(message), opts.source);
325
+ const card = holder.firstElementChild as HTMLElement;
326
+ // Mark processed so the auto surface's selectors never re-scan the card
327
+ // (its root carries the `.dgmo` class the scanner matches on).
328
+ card.setAttribute('data-dgmo-processed', 'true');
329
+ return card;
330
330
  }
331
331
 
332
332
  // ============================================================
@@ -1,24 +1,40 @@
1
1
  /**
2
- * Static CSS for `@diagrammo/dgmo/auto`.
2
+ * Static CSS for `@diagrammo/dgmo/auto` (and `./element`, which shares the
3
+ * same injector).
3
4
  *
4
- * The string is a literal — no template interpolation of user-supplied
5
- * values. Theme/palette differences are applied via class hooks
6
- * (`.dgmo-theme-light`, `.dgmo-theme-dark`) on the rendered wrapper, so
7
- * a malicious theme name can never be smuggled into the stylesheet text.
5
+ * As of BL-114 the diagram + source chrome is the canonical standard block
6
+ * from `src/embed` — its styles ship in `BLOCK_CSS` and are appended below.
7
+ * This file only keeps the auto-surface-specific rules: anti-flash hiding,
8
+ * the `.dgmo-rendered` container reset, and the loading hint.
9
+ *
10
+ * `BLOCK_CSS` keys its dark-mode overrides off `[data-theme="dark"]`; the
11
+ * browser surfaces resolve the theme themselves and stamp a
12
+ * `.dgmo-theme-dark` class on the wrapper, so a re-scoped copy of the dark
13
+ * rules is appended too (same adapter pattern as fumadocs/nextra's
14
+ * build-css.mjs, done here at module init).
8
15
  *
9
- * All selectors are prefixed `.dgmo-` to avoid collisions with embedder
10
- * styles. Class names are part of the SemVer-stable surface — see
16
+ * The string is a literal — no template interpolation of user-supplied
17
+ * values. All selectors are prefixed `.dgmo-` to avoid collisions with
18
+ * embedder styles. Class names are part of the SemVer-stable surface — see
11
19
  * "SemVer policy" in the README HTML section.
20
+ *
21
+ * IMPORTANT: keep AUTO_BASE_CSS a single pure template literal — tsup's
22
+ * emitAutoCss extracts it (plus BLOCK_CSS) with a regex to build
23
+ * `dist/auto.css`, mirroring the concatenation done at the bottom of this
24
+ * file. Update tsup.config.ts if the assembly here changes.
12
25
  */
13
- export const CSS: string = `
26
+
27
+ import { BLOCK_CSS } from '../embed/css';
28
+
29
+ const AUTO_BASE_CSS: string = `
14
30
  /* anti-flash: hide source elements until either render swaps them out
15
- or the error path explicitly un-hides them */
31
+ or the error path replaces them with the standard error card */
16
32
  pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
17
- .dgmo:not(svg *):not(.dgmo-rendered):not(.dgmo-rendered *) {
33
+ .dgmo:not(svg *):not(.dgmo-rendered):not(.dgmo-rendered *):not(.dgmo--error):not(.dgmo--error *) {
18
34
  visibility: hidden;
19
35
  }
20
36
  .dgmo-rendered, .dgmo-rendered *,
21
- .dgmo-error-banner, .dgmo-error-banner * {
37
+ .dgmo--error, .dgmo--error * {
22
38
  visibility: visible !important;
23
39
  }
24
40
 
@@ -32,197 +48,26 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
32
48
  line-height: 1.4;
33
49
  color-scheme: light dark;
34
50
  }
35
- .dgmo-rendered svg {
36
- display: block;
37
- max-width: 100%;
38
- height: auto;
39
- }
40
-
41
- .dgmo-source-panel {
42
- margin-top: 4px;
43
- position: relative;
44
- }
45
-
46
- .dgmo-source-toggle {
47
- display: inline-flex;
48
- align-items: center;
49
- gap: 4px;
50
- /* Visual size is small; the 28px hit area + padding still meets
51
- the 44 × 44 minimum touch target across most stylesheets, and
52
- we widen padding on coarse pointers below. */
53
- min-height: 28px;
54
- padding: 4px 6px;
55
- background: transparent;
56
- border: 0;
57
- font: inherit;
58
- font-size: 11px;
59
- font-weight: 500;
60
- letter-spacing: 0.02em;
61
- text-align: left;
62
- cursor: pointer;
63
- color: inherit;
64
- opacity: 0.45;
65
- border-radius: 3px;
66
- transition: opacity 120ms ease, background 120ms ease;
67
- }
68
- .dgmo-source-toggle:hover {
69
- opacity: 0.85;
70
- background: rgba(127, 127, 127, 0.08);
71
- }
72
- .dgmo-source-toggle:focus-visible {
73
- opacity: 1;
74
- outline: 2px solid currentColor;
75
- outline-offset: 1px;
76
- }
77
- .dgmo-source-toggle[aria-expanded="true"] {
78
- opacity: 0.7;
79
- }
80
- .dgmo-source-toggle .dgmo-chevron {
81
- display: inline-block;
82
- width: 8px;
83
- height: 8px;
84
- font-size: 8px;
85
- line-height: 1;
86
- transition: transform 150ms ease-out;
87
- }
88
- .dgmo-source-toggle[aria-expanded="true"] .dgmo-chevron {
89
- transform: rotate(90deg);
90
- }
91
- @media (pointer: coarse) {
92
- /* Larger hit area on touch devices without making the visible
93
- toggle bigger on the desktop. */
94
- .dgmo-source-toggle {
95
- min-height: 44px;
96
- padding: 12px 10px;
97
- }
98
- }
99
-
100
- .dgmo-source-body {
101
- overflow: hidden;
102
- max-height: 0;
103
- transition: max-height 150ms ease-out;
104
- position: relative;
105
- }
106
- .dgmo-source-body.dgmo-open {
107
- max-height: 60vh;
108
- overflow: auto;
109
- margin-top: 2px;
110
- border-radius: 4px;
111
- background: rgba(127, 127, 127, 0.04);
112
- }
113
- .dgmo-rendered.dgmo-theme-dark .dgmo-source-body.dgmo-open {
114
- background: rgba(255, 255, 255, 0.03);
115
- }
116
- .dgmo-source-pre {
117
- margin: 0;
118
- /* Right padding leaves room for the floating action icons. */
119
- padding: 10px 56px 10px 14px;
120
- font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
121
- font-size: 12px;
122
- line-height: 1.45;
123
- white-space: pre;
124
- overflow: auto;
125
- background: transparent;
126
- color: inherit;
127
- }
128
-
129
- .dgmo-source-actions {
130
- position: absolute;
131
- top: 6px;
132
- right: 6px;
133
- display: flex;
134
- gap: 2px;
135
- /* Inherit visibility:hidden when collapsed; pointer-events:none keeps
136
- them off the tab order until the panel opens. */
137
- pointer-events: none;
138
- }
139
- .dgmo-source-body.dgmo-open .dgmo-source-actions {
140
- pointer-events: auto;
141
- }
142
- .dgmo-btn {
143
- display: inline-flex;
144
- align-items: center;
145
- justify-content: center;
146
- width: 26px;
147
- height: 26px;
148
- padding: 0;
149
- background: transparent;
150
- color: inherit;
151
- border: 0;
152
- border-radius: 3px;
153
- cursor: pointer;
154
- text-decoration: none;
155
- opacity: 0.4;
156
- transition: opacity 120ms ease, background 120ms ease, color 120ms ease;
157
- }
158
- .dgmo-btn:hover {
159
- opacity: 1;
160
- background: rgba(127, 127, 127, 0.15);
161
- }
162
- .dgmo-btn:focus-visible {
163
- opacity: 1;
164
- outline: 2px solid currentColor;
165
- outline-offset: 1px;
166
- }
167
- .dgmo-btn[aria-disabled="true"] {
168
- opacity: 0.25;
169
- cursor: not-allowed;
170
- }
171
- .dgmo-btn[aria-disabled="true"]:hover {
172
- background: transparent;
173
- }
174
- .dgmo-btn-copied {
175
- opacity: 1 !important;
176
- color: rgb(120, 200, 120);
177
- }
178
- .dgmo-btn svg {
179
- width: 14px;
180
- height: 14px;
181
- display: block;
182
- }
183
51
 
184
- .dgmo-error-banner {
185
- display: block;
186
- margin: 0.5em 0;
187
- padding: 10px 14px;
188
- border-left: 4px solid #c44;
189
- background: rgba(204, 68, 68, 0.08);
190
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
191
- font-size: 13px;
192
- line-height: 1.4;
193
- color: inherit;
194
- border-radius: 0 4px 4px 0;
195
- }
196
- .dgmo-error-banner-title {
197
- font-weight: 600;
198
- margin-bottom: 2px;
199
- }
200
- .dgmo-error-banner-loc {
201
- opacity: 0.7;
52
+ .dgmo--loading {
53
+ opacity: 0.6;
202
54
  font-size: 12px;
203
55
  font-family: ui-monospace, monospace;
204
56
  }
57
+ `;
205
58
 
206
- /* Token roles — fallback colors used when theme classes don't override */
207
- .dgmo-source-pre .dgmo-tok-keyword { color: #5e81ac; font-weight: 600; }
208
- .dgmo-source-pre .dgmo-tok-controlKeyword { color: #b48ead; font-weight: 600; }
209
- .dgmo-source-pre .dgmo-tok-definitionKeyword { color: #5e81ac; font-weight: 600; }
210
- .dgmo-source-pre .dgmo-tok-modifier { color: #b48ead; }
211
- .dgmo-source-pre .dgmo-tok-chartType { color: #d08770; font-weight: 600; }
212
- .dgmo-source-pre .dgmo-tok-operator { color: #bf616a; font-weight: 600; }
213
- .dgmo-source-pre .dgmo-tok-number { color: #b48ead; }
214
- .dgmo-source-pre .dgmo-tok-comment { color: #6c7a96; font-style: italic; }
215
- .dgmo-source-pre .dgmo-tok-heading { color: #d08770; font-weight: 600; }
216
- .dgmo-source-pre .dgmo-tok-bracket { color: #5e81ac; }
217
- .dgmo-source-pre .dgmo-tok-separator { color: #88c0d0; }
218
- .dgmo-source-pre .dgmo-tok-url { color: #88c0d0; text-decoration: underline; }
219
- .dgmo-source-pre .dgmo-tok-colorAnnotation { color: #d08770; font-style: italic; }
220
- .dgmo-source-pre .dgmo-tok-punctuation { color: #6c7a96; }
221
- .dgmo-source-pre .dgmo-tok-noteContent { color: #6c7a96; font-style: italic; }
59
+ /** Re-scope BLOCK_CSS's \`[data-theme="dark"]\` rules to `.dgmo-theme-dark`. */
60
+ function darkScopedBlockCss(css: string): string {
61
+ const rules = css.match(/\[data-theme="dark"\][^{]*\{[^}]*\}/g) ?? [];
62
+ return rules
63
+ .map((rule) => rule.replace(/\[data-theme="dark"\]/g, '.dgmo-theme-dark'))
64
+ .join('\n');
65
+ }
222
66
 
223
- .dgmo-rendered.dgmo-theme-dark .dgmo-source-pre .dgmo-tok-keyword,
224
- .dgmo-rendered.dgmo-theme-dark .dgmo-source-pre .dgmo-tok-definitionKeyword { color: #81a1c1; }
225
- .dgmo-rendered.dgmo-theme-dark .dgmo-source-pre .dgmo-tok-comment,
226
- .dgmo-rendered.dgmo-theme-dark .dgmo-source-pre .dgmo-tok-noteContent,
227
- .dgmo-rendered.dgmo-theme-dark .dgmo-source-pre .dgmo-tok-punctuation { color: #616e88; }
228
- `;
67
+ export const CSS: string =
68
+ AUTO_BASE_CSS +
69
+ '\n' +
70
+ BLOCK_CSS +
71
+ '\n' +
72
+ darkScopedBlockCss(BLOCK_CSS) +
73
+ '\n';
@@ -622,7 +622,7 @@ export function renderC4Context(
622
622
  .attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
623
623
  : contentG.append('g').attr('class', 'c4-legend');
624
624
  if (activeTagGroup) {
625
- legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
625
+ legendParent.attr('data-legend-active', tagAttrKey(activeTagGroup));
626
626
  }
627
627
  renderLegend(
628
628
  legendParent as GSelection,
@@ -1804,7 +1804,7 @@ export function renderC4Containers(
1804
1804
  .attr('transform', `translate(0, ${DIAGRAM_PADDING + titleHeight})`)
1805
1805
  : contentG.append('g').attr('class', 'c4-legend');
1806
1806
  if (activeTagGroup) {
1807
- legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
1807
+ legendParent.attr('data-legend-active', tagAttrKey(activeTagGroup));
1808
1808
  }
1809
1809
  renderLegend(
1810
1810
  legendParent as GSelection,