react-super-mermaid 0.3.1 → 0.4.2

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/README.md CHANGED
@@ -166,7 +166,7 @@ gitGraph
166
166
  - 🔍 **In-diagram search** — highlight + pan to matches (`/` or `Ctrl/Cmd+F`).
167
167
  - đŸ–ī¸ **Pan & zoom** — fit, actual size, keyboard `+ - 0 1 w` (via `svg-pan-zoom`).
168
168
  - â›ļ **Fullscreen modal** — open the diagram in a viewport-filling, RWD-friendly popup (`f` / `Esc`); body scroll locked, auto re-fit.
169
- - â–Ļ **Background toggle** — cycle the canvas between transparent / solid / dot-grid (`b`), handy for reading and exports.
169
+ - â–Ļ **Background picker** — a swatch popover to set the canvas **surface** (preset colors + a custom color well) and an independent **pattern** (none / dots / grid lines, cycle with `b`). Surface + pattern combine freely and carry through to exports.
170
170
  - 📤 **Export** — SVG and high-res PNG/JPEG/WebP (1×/2×/4×, optional transparent background).
171
171
  - đŸĒļ **Lightweight & decoupled** — `mermaid`, `svg-pan-zoom`, `react` are **optional peer deps**, never bundled. No Tailwind, no app coupling.
172
172
  - 🌐 **Load external mermaid** — inject an instance, dynamic-import the peer, or pull from a CDN. Your bundle doesn't carry mermaid.
@@ -248,8 +248,9 @@ Resolution order is **injected → peer import → CDN**, memoized so mermaid lo
248
248
  | `panZoom` | `boolean` | `true` | |
249
249
  | `search` | `boolean` | `true` | toolbar search |
250
250
  | `exportable` | `boolean` | `true` | toolbar SVG/PNG export |
251
- | `background` | `boolean` | `true` | toolbar background toggle (transparent / solid / grid) |
252
- | `backgroundMode` | `'transparent' \| 'solid' \| 'grid'` | `'transparent'` | initial / controlled canvas background |
251
+ | `background` | `boolean` | `true` | show the background picker (surface swatches + custom color + pattern toggle) |
252
+ | `pattern` | `'none' \| 'dots' \| 'grid'` | `'dots'` | initial / controlled overlay pattern |
253
+ | `solidColor` | `string \| null` | `null` (transparent) | initial / controlled canvas surface color (hex); `null` follows the page |
253
254
  | `fullscreen` | `boolean` | `true` | toolbar fullscreen button — opens a viewport-filling modal (RWD) |
254
255
  | `onFullscreenChange` | `(fullscreen: boolean) => void` | — | fired on enter / exit fullscreen |
255
256
  | `keyboard` | `boolean` | `true` | shortcuts when the viewer is focused |
@@ -281,7 +282,7 @@ await ref.current?.downloadPng('diagram.png', { scale: 4 });
281
282
  const svgString = ref.current?.exportSvg();
282
283
  ```
283
284
 
284
- Handle: `zoomIn / zoomOut / fit / reset / actualSize / getZoomPercent / search / next / prev / clearSearch / exportSvg / exportPng / downloadSvg / downloadPng / getSvg / enterFullscreen / exitFullscreen / toggleFullscreen / isFullscreen / setBackground / cycleBackground / getBackground`.
285
+ Handle: `zoomIn / zoomOut / fit / reset / actualSize / getZoomPercent / search / next / prev / clearSearch / exportSvg / exportPng / downloadSvg / downloadPng / getSvg / enterFullscreen / exitFullscreen / toggleFullscreen / isFullscreen / setPattern / cyclePattern / getPattern / setSolidColor / getSolidColor`.
285
286
 
286
287
  ## Themes & the sketch font
287
288
 
@@ -307,7 +308,7 @@ Styles are injected automatically (`injectStyles` default `true`) — no CSS imp
307
308
 
308
309
  ## Keyboard shortcuts
309
310
 
310
- Focus the viewer, then: `/` or `Ctrl/Cmd+F` search ¡ `+`/`-` zoom ¡ `0` fit ¡ `1` actual size ¡ `w` fit width ¡ `f` toggle fullscreen ¡ `b` cycle background ¡ `Esc` close search / exit fullscreen.
311
+ Focus the viewer, then: `/` or `Ctrl/Cmd+F` search ¡ `+`/`-` zoom ¡ `0` fit ¡ `1` actual size ¡ `w` fit width ¡ `f` toggle fullscreen ¡ `b` cycle background pattern (none / dots / grid) ¡ `Esc` close search / exit fullscreen.
311
312
 
312
313
  ## Next.js / SSR
313
314
 
package/dist/index.cjs CHANGED
@@ -107,8 +107,13 @@ var RSM_CSS = `
107
107
  --rsm-accent: #2563eb;
108
108
  --rsm-hover: #f3f4f6;
109
109
  --rsm-surface: #ffffff;
110
+ /* --rsm-paper = \u756B\u5E03\u5E95\u8272,\u5C0D\u9F4A VS Code \u64F4\u5145\u5957\u4EF6\u7684 editor-background(\u4EAE)\u3002 */
111
+ --rsm-paper: #ffffff;
110
112
  --rsm-canvas-bg: transparent;
111
- --rsm-grid-dot: rgba(0, 0, 0, 0.08);
113
+ /* \u9EDE\u9663\u683C\u7DDA:\u5C0D\u9F4A VS Code \u7684 color-mix(foreground 9%) \u516C\u5F0F\u3002 */
114
+ --rsm-grid-dot: color-mix(in srgb, var(--rsm-fg) 9%, transparent);
115
+ /* \u7DB2\u683C\u7DDA:\u6BD4\u7DB2\u9EDE\u518D\u6DE1\u4E00\u9EDE,\u907F\u514D\u7DDA\u689D\u6436\u904E\u5716\u8868\u3002 */
116
+ --rsm-grid-line: color-mix(in srgb, var(--rsm-fg) 7%, transparent);
112
117
  --rsm-radius: 8px;
113
118
  display: flex;
114
119
  flex-direction: column;
@@ -170,6 +175,14 @@ var RSM_CSS = `
170
175
  }
171
176
  .rsm-btn:disabled { opacity: 0.6; cursor: default; }
172
177
 
178
+ /* \u5DE5\u5177\u5217\u63A7\u5236\u9805\u9AD8\u5EA6\u4E00\u81F4(\u6A23\u5F0F\u4E0B\u62C9 / \u4E00\u822C\u9215 / \u7E2E\u653E\u7FA4 / \u80CC\u666F\u9215),\u907F\u514D\u9AD8\u4F4E\u4E0D\u9F4A\u3002 */
179
+ .rsm-toolbar .rsm-btn,
180
+ .rsm-toolbar .rsm-select,
181
+ .rsm-toolbar .rsm-zoom { min-height: 30px; }
182
+ .rsm-toolbar .rsm-btn,
183
+ .rsm-toolbar .rsm-select { align-items: center; }
184
+ .rsm-toolbar .rsm-zoom > button { display: inline-flex; align-items: center; justify-content: center; }
185
+
173
186
  .rsm-zoom {
174
187
  display: inline-flex;
175
188
  align-items: stretch;
@@ -215,7 +228,8 @@ var RSM_CSS = `
215
228
  flex: 1 1 auto;
216
229
  min-height: 0;
217
230
  overflow: hidden;
218
- background: var(--rsm-canvas-bg);
231
+ /* \u5E95\u8272\u7531 --rsm-canvas-bg \u63A7\u5236(\u9810\u8A2D\u900F\u660E,\u8DDF\u96A8\u9801\u9762);\u5716\u6A23\u758A\u5728\u5176\u4E0A(background-image)\u3002 */
232
+ background-color: var(--rsm-canvas-bg, transparent);
219
233
  }
220
234
  .rsm-stage { width: 100%; height: 100%; }
221
235
  .rsm-root svg { cursor: grab; user-select: none; }
@@ -240,23 +254,172 @@ var RSM_CSS = `
240
254
  .rsm-root .rsm-hit { filter: drop-shadow(0 0 5px #f59e0b) drop-shadow(0 0 1.5px #f59e0b); }
241
255
 
242
256
  .rsm-root.rsm-dark {
243
- --rsm-border: #374151;
244
- --rsm-fg: #e5e7eb;
245
- --rsm-muted: #9ca3af;
246
- --rsm-accent: #60a5fa;
247
- --rsm-hover: #1f2937;
248
- --rsm-surface: #111827;
249
- --rsm-grid-dot: rgba(255, 255, 255, 0.10);
257
+ /* \u6697\u8272\u9762\u677F\u5C0D\u9F4A VS Code Dark+ / Dark Modern \u7684\u4E2D\u6027\u7070(\u975E\u85CD\u8ABF)\u3002 */
258
+ --rsm-border: #3c3c3c;
259
+ --rsm-fg: #cccccc;
260
+ --rsm-muted: #9d9d9d;
261
+ --rsm-accent: #3794ff;
262
+ --rsm-hover: #2a2d2e;
263
+ --rsm-surface: #252526;
264
+ /* \u756B\u5E03\u5E95\u8272 = VS Code editor-background(\u6697);grid-dot \u7531 --rsm-fg 9% \u81EA\u52D5\u63A8\u5C0E\u3002 */
265
+ --rsm-paper: #1e1e1e;
250
266
  }
251
267
 
252
- /* \u2500\u2500 \u80CC\u666F\u6A21\u5F0F \u2500\u2500 \u900F\u660E(\u9810\u8A2D,\u8DDF\u96A8\u9801\u9762) / \u7D14\u8272(surface) / \u9EDE\u9663\u683C\u7DDA\u3002 */
253
- .rsm-root.rsm-bg-solid .rsm-canvas { background: var(--rsm-surface); }
254
- .rsm-root.rsm-bg-grid .rsm-canvas {
255
- background-color: var(--rsm-surface);
268
+ /* \u2500\u2500 \u80CC\u666F \u2500\u2500 \u5E95\u8272 + \u758A\u52A0\u5716\u6A23,\u5169\u8005\u7368\u7ACB\u3002
269
+ * \u5E95\u8272:--rsm-canvas-bg(\u7531\u8272\u7968 / \u81EA\u8A02\u8272 inline \u8986\u5BEB;\u672A\u8A2D = \u900F\u660E\u8DDF\u96A8\u9801\u9762)\u3002
270
+ * \u5716\u6A23:.rsm-pattern-dots(\u7DB2\u9EDE) / .rsm-pattern-grid(\u7DB2\u683C\u7DDA),\u758A\u5728\u5E95\u8272\u4E4B\u4E0A\u3002 */
271
+ .rsm-root.rsm-pattern-dots .rsm-canvas {
256
272
  background-image: radial-gradient(var(--rsm-grid-dot) 1px, transparent 1px);
257
273
  background-size: 18px 18px;
258
274
  background-position: -9px -9px;
259
275
  }
276
+ .rsm-root.rsm-pattern-grid .rsm-canvas {
277
+ background-image:
278
+ linear-gradient(to right, var(--rsm-grid-line) 1px, transparent 1px),
279
+ linear-gradient(to bottom, var(--rsm-grid-line) 1px, transparent 1px);
280
+ background-size: 22px 22px;
281
+ background-position: -1px -1px;
282
+ }
283
+
284
+ /* \u2500\u2500 \u80CC\u666F\u9078\u64C7\u5668(toolbar \u5167\u7684\u8272\u4E95\u6309\u9215 + \u5F48\u51FA\u9762\u677F)\u2500\u2500 */
285
+ .rsm-bg { position: relative; display: inline-flex; }
286
+
287
+ /* \u89F8\u767C\u9215\u5DE6\u5074\u7684\u300C\u8272\u4E95\u300D:\u53CD\u6620\u76EE\u524D\u5E95\u8272;\u900F\u660E / \u9810\u8A2D\u6642\u756B\u4E00\u9053\u659C\u7DDA\u8868\u793A\u300C\u4E0D\u8986\u5BEB\u300D\u3002 */
288
+ .rsm-bg-well {
289
+ width: 16px;
290
+ height: 16px;
291
+ border-radius: 4px;
292
+ border: 1px solid color-mix(in srgb, var(--rsm-fg) 28%, transparent);
293
+ background-color: var(--rsm-well-color, transparent);
294
+ }
295
+ .rsm-bg-well[data-empty="true"] {
296
+ background-color: var(--rsm-surface);
297
+ background-image: linear-gradient(
298
+ to top right,
299
+ transparent calc(50% - 1px),
300
+ #ef4444 calc(50% - 1px),
301
+ #ef4444 calc(50% + 1px),
302
+ transparent calc(50% + 1px)
303
+ );
304
+ }
305
+
306
+ /* \u5F48\u51FA\u9762\u677F:\u5361\u7247\u5F0F\u3001\u8F15\u9670\u5F71\u3001\u6DE1\u5165\u3002 */
307
+ .rsm-bg-pop {
308
+ position: absolute;
309
+ top: calc(100% + 8px);
310
+ left: 0;
311
+ z-index: 50;
312
+ display: flex;
313
+ flex-direction: column;
314
+ gap: 12px;
315
+ padding: 12px;
316
+ min-width: 260px;
317
+ border: 1px solid var(--rsm-border);
318
+ border-radius: 12px;
319
+ background: var(--rsm-surface);
320
+ color: var(--rsm-fg);
321
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16), 0 2px 6px rgba(0, 0, 0, 0.08);
322
+ animation: rsm-pop-in 0.13s ease-out;
323
+ }
324
+ @keyframes rsm-pop-in {
325
+ from { opacity: 0; transform: translateY(-5px); }
326
+ to { opacity: 1; transform: none; }
327
+ }
328
+ .rsm-bg-section { display: flex; flex-direction: column; gap: 8px; }
329
+ .rsm-bg-section-label {
330
+ font-size: 11px;
331
+ font-weight: 600;
332
+ letter-spacing: 0.03em;
333
+ color: var(--rsm-muted);
334
+ }
335
+ .rsm-bg-swatches { display: flex; flex-wrap: wrap; gap: 8px; }
336
+
337
+ /* \u8272\u7968:\u5713\u89D2\u5C0F\u65B9\u584A;\u9078\u4E2D\u52A0\u540C\u8272\u5916\u74B0\u3002 */
338
+ .rsm-swatch {
339
+ position: relative;
340
+ width: 26px;
341
+ height: 26px;
342
+ padding: 0;
343
+ border: 1px solid color-mix(in srgb, var(--rsm-fg) 16%, transparent);
344
+ border-radius: 7px;
345
+ cursor: pointer;
346
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
347
+ }
348
+ .rsm-swatch:hover { transform: scale(1.12); }
349
+ .rsm-swatch:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--rsm-accent); }
350
+ .rsm-swatch.rsm-selected { outline: 2px solid var(--rsm-accent); outline-offset: 2px; }
351
+ .rsm-swatch[data-empty="true"] {
352
+ background-color: var(--rsm-surface);
353
+ background-image: linear-gradient(
354
+ to top right,
355
+ transparent calc(50% - 1px),
356
+ #ef4444 calc(50% - 1px),
357
+ #ef4444 calc(50% + 1px),
358
+ transparent calc(50% + 1px)
359
+ );
360
+ }
361
+
362
+ /* \u81EA\u8A02\u8272\u7968:\u8986\u4E00\u500B\u96B1\u5F62\u7684\u539F\u751F color input,\u672A\u9078\u6642\u986F\u793A \u{1F3A8}\u3002 */
363
+ .rsm-swatch-custom {
364
+ display: inline-flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ overflow: hidden;
368
+ background:
369
+ conic-gradient(from 180deg, #f87171, #fbbf24, #34d399, #60a5fa, #a78bfa, #f87171);
370
+ }
371
+ .rsm-swatch-custom.rsm-has-color { background: none; }
372
+ .rsm-swatch-custom input[type="color"] {
373
+ position: absolute;
374
+ inset: 0;
375
+ width: 100%;
376
+ height: 100%;
377
+ margin: 0;
378
+ padding: 0;
379
+ border: 0;
380
+ opacity: 0;
381
+ cursor: pointer;
382
+ }
383
+ .rsm-swatch-custom-icon {
384
+ font-size: 12px;
385
+ line-height: 1;
386
+ pointer-events: none;
387
+ filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35));
388
+ }
389
+
390
+ /* \u5716\u6A23\u5207\u63DB:\u5206\u6BB5\u5F0F\u6309\u9215(\u7121 / \u7DB2\u9EDE / \u7DB2\u683C)\u3002 */
391
+ .rsm-seg {
392
+ display: inline-flex;
393
+ align-self: flex-start;
394
+ border: 1px solid var(--rsm-border);
395
+ border-radius: 8px;
396
+ overflow: hidden;
397
+ }
398
+ .rsm-seg > button {
399
+ flex: 1 1 0;
400
+ min-width: 58px;
401
+ display: inline-flex;
402
+ align-items: center;
403
+ justify-content: center;
404
+ gap: 5px;
405
+ border: 0;
406
+ background: var(--rsm-surface);
407
+ color: var(--rsm-fg);
408
+ padding: 6px 11px;
409
+ font-size: 12px;
410
+ line-height: 1.3;
411
+ white-space: nowrap;
412
+ cursor: pointer;
413
+ transition: background 0.1s ease, color 0.1s ease;
414
+ }
415
+ .rsm-seg > button + button { border-left: 1px solid var(--rsm-border); }
416
+ .rsm-seg > button:hover { background: var(--rsm-hover); }
417
+ .rsm-seg > button[aria-pressed="true"] {
418
+ background: color-mix(in srgb, var(--rsm-accent) 14%, transparent);
419
+ color: var(--rsm-accent);
420
+ font-weight: 600;
421
+ }
422
+ .rsm-seg-glyph { font-size: 13px; line-height: 1; }
260
423
 
261
424
  /* \u2500\u2500 \u5168\u87A2\u5E55\u8DF3\u7A97 \u2500\u2500 position:fixed \u8986\u84CB\u6574\u500B\u8996\u7A97,RWD \u53CB\u5584\u3002 */
262
425
  .rsm-root.rsm-fullscreen {
@@ -338,7 +501,11 @@ function ensureStyles() {
338
501
  if (typeof document === "undefined") {
339
502
  return;
340
503
  }
341
- if (document.getElementById(RSM_STYLE_ID)) {
504
+ const existing = document.getElementById(RSM_STYLE_ID);
505
+ if (existing) {
506
+ if (existing.textContent !== RSM_CSS) {
507
+ existing.textContent = RSM_CSS;
508
+ }
342
509
  return;
343
510
  }
344
511
  const style = document.createElement("style");
@@ -1449,7 +1616,7 @@ async function rasterizeToBlob(prepared, opts = {}) {
1449
1616
  throw new Error("\u53D6\u4E0D\u5230 Canvas 2D context\u3002");
1450
1617
  }
1451
1618
  if (!transparent || mime === "image/jpeg") {
1452
- ctx.fillStyle = opts.background ?? (opts.dark ? "#111827" : "#ffffff");
1619
+ ctx.fillStyle = opts.background ?? (opts.dark ? "#1e1e1e" : "#ffffff");
1453
1620
  ctx.fillRect(0, 0, canvas.width, canvas.height);
1454
1621
  }
1455
1622
  ctx.scale(scale, scale);
@@ -1690,11 +1857,19 @@ function useMermaidViewer(opts) {
1690
1857
  getSvg
1691
1858
  };
1692
1859
  }
1693
- var BACKGROUND_LABELS = {
1694
- transparent: { icon: "\u25A6", label: "\u900F\u660E" },
1695
- solid: { icon: "\u25FB", label: "\u7D14\u8272" },
1696
- grid: { icon: "\u229E", label: "\u683C\u7DDA" }
1697
- };
1860
+ var BACKGROUND_PRESETS = [
1861
+ { value: null, label: "\u9810\u8A2D / \u900F\u660E" },
1862
+ { value: "#FFFFFF", label: "\u767D" },
1863
+ { value: "#F3F4F6", label: "\u6DFA\u7070" },
1864
+ { value: "#EFF6FF", label: "\u6DFA\u85CD" },
1865
+ { value: "#FEFCE8", label: "\u6DFA\u9EC3" },
1866
+ { value: "#FDF2F8", label: "\u6DFA\u73AB\u7470" }
1867
+ ];
1868
+ var PATTERN_OPTIONS = [
1869
+ { value: "none", glyph: "\u25A2", label: "\u7121" },
1870
+ { value: "dots", glyph: "\u283F", label: "\u7DB2\u9EDE" },
1871
+ { value: "grid", glyph: "\u229E", label: "\u7DB2\u683C" }
1872
+ ];
1698
1873
  var DEFAULT_THEME_OPTIONS = [
1699
1874
  { value: "colorful", label: "Colorful" },
1700
1875
  { value: "sketch", label: "Excalidraw" },
@@ -1704,6 +1879,126 @@ var DEFAULT_THEME_OPTIONS = [
1704
1879
  { value: "neutral", label: "Neutral" },
1705
1880
  { value: "forest", label: "Forest" }
1706
1881
  ];
1882
+ var HEX6 = /^#[0-9a-fA-F]{6}$/;
1883
+ function isSameColor(a, b) {
1884
+ if (a === null || b === null) {
1885
+ return a === b;
1886
+ }
1887
+ return a.toLowerCase() === b.toLowerCase();
1888
+ }
1889
+ function BackgroundPicker(props) {
1890
+ const [open, setOpen] = react.useState(false);
1891
+ const rootRef = react.useRef(null);
1892
+ react.useEffect(() => {
1893
+ if (!open) {
1894
+ return void 0;
1895
+ }
1896
+ const onDocPointer = (e) => {
1897
+ if (rootRef.current && !rootRef.current.contains(e.target)) {
1898
+ setOpen(false);
1899
+ }
1900
+ };
1901
+ const onKey = (e) => {
1902
+ if (e.key === "Escape") {
1903
+ setOpen(false);
1904
+ }
1905
+ };
1906
+ document.addEventListener("mousedown", onDocPointer);
1907
+ document.addEventListener("keydown", onKey);
1908
+ return () => {
1909
+ document.removeEventListener("mousedown", onDocPointer);
1910
+ document.removeEventListener("keydown", onKey);
1911
+ };
1912
+ }, [open]);
1913
+ const isPreset = BACKGROUND_PRESETS.some((p) => isSameColor(p.value, props.surface));
1914
+ const customActive = props.surface !== null && !isPreset;
1915
+ const customInputValue = props.surface && HEX6.test(props.surface) ? props.surface : "#1e293b";
1916
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rsm-bg", ref: rootRef, children: [
1917
+ /* @__PURE__ */ jsxRuntime.jsxs(
1918
+ "button",
1919
+ {
1920
+ type: "button",
1921
+ className: "rsm-btn rsm-bg-trigger",
1922
+ "aria-expanded": open,
1923
+ "aria-haspopup": "dialog",
1924
+ onClick: () => setOpen((o) => !o),
1925
+ title: "\u756B\u5E03\u80CC\u666F\uFF08\u5E95\u8272 + \u7DB2\u9EDE / \u7DB2\u683C\uFF0CB\uFF09",
1926
+ children: [
1927
+ /* @__PURE__ */ jsxRuntime.jsx(
1928
+ "span",
1929
+ {
1930
+ className: "rsm-bg-well",
1931
+ "data-empty": props.surface === null ? "true" : void 0,
1932
+ style: props.surface ? { ["--rsm-well-color"]: props.surface } : void 0
1933
+ }
1934
+ ),
1935
+ "\u80CC\u666F"
1936
+ ]
1937
+ }
1938
+ ),
1939
+ open ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rsm-bg-pop", role: "dialog", "aria-label": "\u756B\u5E03\u80CC\u666F\u8A2D\u5B9A", children: [
1940
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rsm-bg-section", children: [
1941
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rsm-bg-section-label", children: "\u5E95\u8272" }),
1942
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rsm-bg-swatches", children: [
1943
+ BACKGROUND_PRESETS.map((preset) => {
1944
+ const selected = isSameColor(preset.value, props.surface);
1945
+ return /* @__PURE__ */ jsxRuntime.jsx(
1946
+ "button",
1947
+ {
1948
+ type: "button",
1949
+ className: `rsm-swatch${selected ? " rsm-selected" : ""}`,
1950
+ "data-empty": preset.value === null ? "true" : void 0,
1951
+ style: preset.value ? { backgroundColor: preset.value } : void 0,
1952
+ title: preset.label,
1953
+ "aria-label": `\u5E95\u8272\uFF1A${preset.label}`,
1954
+ "aria-pressed": selected,
1955
+ onClick: () => props.onSurfaceChange(preset.value)
1956
+ },
1957
+ preset.label
1958
+ );
1959
+ }),
1960
+ /* @__PURE__ */ jsxRuntime.jsxs(
1961
+ "span",
1962
+ {
1963
+ className: `rsm-swatch rsm-swatch-custom${customActive ? " rsm-has-color rsm-selected" : ""}`,
1964
+ style: customActive ? { backgroundColor: props.surface } : void 0,
1965
+ title: "\u81EA\u8A02\u984F\u8272",
1966
+ children: [
1967
+ !customActive ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rsm-swatch-custom-icon", children: "\u{1F3A8}" }) : null,
1968
+ /* @__PURE__ */ jsxRuntime.jsx(
1969
+ "input",
1970
+ {
1971
+ type: "color",
1972
+ value: customInputValue,
1973
+ "aria-label": "\u81EA\u8A02\u5E95\u8272",
1974
+ onChange: (e) => props.onSurfaceChange(e.target.value)
1975
+ }
1976
+ )
1977
+ ]
1978
+ }
1979
+ )
1980
+ ] })
1981
+ ] }),
1982
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rsm-bg-section", children: [
1983
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rsm-bg-section-label", children: "\u5716\u6A23" }),
1984
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rsm-seg", role: "group", "aria-label": "\u80CC\u666F\u5716\u6A23", children: PATTERN_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntime.jsxs(
1985
+ "button",
1986
+ {
1987
+ type: "button",
1988
+ "aria-pressed": props.pattern === opt.value,
1989
+ title: opt.label,
1990
+ onClick: () => props.onPatternChange(opt.value),
1991
+ children: [
1992
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rsm-seg-glyph", "aria-hidden": "true", children: opt.glyph }),
1993
+ opt.label
1994
+ ]
1995
+ },
1996
+ opt.value
1997
+ )) })
1998
+ ] })
1999
+ ] }) : null
2000
+ ] });
2001
+ }
1707
2002
  function Toolbar(props) {
1708
2003
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rsm-toolbar", children: [
1709
2004
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "rsm-label", children: [
@@ -1718,18 +2013,13 @@ function Toolbar(props) {
1718
2013
  }
1719
2014
  )
1720
2015
  ] }),
1721
- props.backgroundEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(
1722
- "button",
2016
+ props.backgroundEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
2017
+ BackgroundPicker,
1723
2018
  {
1724
- type: "button",
1725
- className: "rsm-btn",
1726
- onClick: props.onCycleBackground,
1727
- title: "\u5207\u63DB\u756B\u5E03\u80CC\u666F\uFF08\u900F\u660E / \u7D14\u8272 / \u683C\u7DDA\uFF0CB\uFF09",
1728
- children: [
1729
- BACKGROUND_LABELS[props.background].icon,
1730
- " \u80CC\u666F\uFF1A",
1731
- BACKGROUND_LABELS[props.background].label
1732
- ]
2019
+ surface: props.surface,
2020
+ onSurfaceChange: props.onSurfaceChange,
2021
+ pattern: props.pattern,
2022
+ onPatternChange: props.onPatternChange
1733
2023
  }
1734
2024
  ) : null,
1735
2025
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rsm-toolbar-spacer" }),
@@ -1799,7 +2089,24 @@ function Toolbar(props) {
1799
2089
  ) : null
1800
2090
  ] });
1801
2091
  }
1802
- var BACKGROUND_CYCLE = ["transparent", "solid", "grid"];
2092
+ var PATTERN_CYCLE = ["none", "dots", "grid"];
2093
+ function defaultSolidColor(dark) {
2094
+ return dark ? "#1e1e1e" : "#ffffff";
2095
+ }
2096
+ function patternInk(surface, dark) {
2097
+ const m = surface ? /^#([0-9a-fA-F]{6})$/.exec(surface) : null;
2098
+ let lightCanvas;
2099
+ if (m) {
2100
+ const r = parseInt(m[1].slice(0, 2), 16);
2101
+ const g = parseInt(m[1].slice(2, 4), 16);
2102
+ const b = parseInt(m[1].slice(4, 6), 16);
2103
+ lightCanvas = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 > 0.5;
2104
+ } else {
2105
+ lightCanvas = !dark;
2106
+ }
2107
+ const ink = lightCanvas ? "15, 23, 42" : "226, 232, 240";
2108
+ return { dot: `rgba(${ink}, 0.34)`, line: `rgba(${ink}, 0.2)` };
2109
+ }
1803
2110
  function usePrefersDark(explicit) {
1804
2111
  const [autoDark, setAutoDark] = react.useState(false);
1805
2112
  react.useEffect(() => {
@@ -1849,14 +2156,18 @@ var MermaidViewer = react.forwardRef(
1849
2156
  const [query, setQuery] = react.useState("");
1850
2157
  const [matchInfo, setMatchInfo] = react.useState({ current: 0, total: 0 });
1851
2158
  const [exporting, setExporting] = react.useState(false);
1852
- const [background, setBackgroundState] = react.useState(
1853
- props.backgroundMode ?? "transparent"
1854
- );
2159
+ const [pattern, setPatternState] = react.useState(props.pattern ?? "dots");
2160
+ react.useEffect(() => {
2161
+ if (props.pattern) {
2162
+ setPatternState(props.pattern);
2163
+ }
2164
+ }, [props.pattern]);
2165
+ const [solidColor, setSolidColorState] = react.useState(props.solidColor ?? null);
1855
2166
  react.useEffect(() => {
1856
- if (props.backgroundMode) {
1857
- setBackgroundState(props.backgroundMode);
2167
+ if (props.solidColor !== void 0) {
2168
+ setSolidColorState(props.solidColor);
1858
2169
  }
1859
- }, [props.backgroundMode]);
2170
+ }, [props.solidColor]);
1860
2171
  const [isFullscreen, setIsFullscreen] = react.useState(false);
1861
2172
  const rootRef = react.useRef(null);
1862
2173
  const searchInputRef = react.useRef(null);
@@ -1913,21 +2224,27 @@ var MermaidViewer = react.forwardRef(
1913
2224
  const exportPng = react.useCallback(async () => {
1914
2225
  setExporting(true);
1915
2226
  try {
1916
- await vm.downloadPng("diagram.png", { scale: 2 });
2227
+ const paper = solidColor ?? defaultSolidColor(dark);
2228
+ const transparent = solidColor === null && pattern === "none";
2229
+ const bgOpt = transparent ? { transparent: true } : { background: paper };
2230
+ await vm.downloadPng("diagram.png", { scale: 2, ...bgOpt });
1917
2231
  } catch (e) {
1918
2232
  onError?.(e instanceof Error ? e : new Error(String(e)));
1919
2233
  } finally {
1920
2234
  setExporting(false);
1921
2235
  }
1922
- }, [vm, onError]);
1923
- const cycleBackground = react.useCallback(() => {
1924
- setBackgroundState((prev) => {
1925
- const i = BACKGROUND_CYCLE.indexOf(prev);
1926
- return BACKGROUND_CYCLE[(i + 1) % BACKGROUND_CYCLE.length];
2236
+ }, [vm, onError, pattern, solidColor, dark]);
2237
+ const cyclePattern = react.useCallback(() => {
2238
+ setPatternState((prev) => {
2239
+ const i = PATTERN_CYCLE.indexOf(prev);
2240
+ return PATTERN_CYCLE[(i + 1) % PATTERN_CYCLE.length];
1927
2241
  });
1928
2242
  }, []);
1929
- const setBackground = react.useCallback((mode) => {
1930
- setBackgroundState(mode);
2243
+ const setPattern = react.useCallback((next) => {
2244
+ setPatternState(next);
2245
+ }, []);
2246
+ const setSolidColor = react.useCallback((color) => {
2247
+ setSolidColorState(color);
1931
2248
  }, []);
1932
2249
  const enterFullscreen = react.useCallback(() => {
1933
2250
  setIsFullscreen((prev) => {
@@ -2047,7 +2364,7 @@ var MermaidViewer = react.forwardRef(
2047
2364
  toggleFullscreen();
2048
2365
  } else if (backgroundEnabled && (e.key === "b" || e.key === "B")) {
2049
2366
  e.preventDefault();
2050
- cycleBackground();
2367
+ cyclePattern();
2051
2368
  }
2052
2369
  };
2053
2370
  root.addEventListener("keydown", onKey);
@@ -2061,7 +2378,7 @@ var MermaidViewer = react.forwardRef(
2061
2378
  isFullscreen,
2062
2379
  exitFullscreen,
2063
2380
  toggleFullscreen,
2064
- cycleBackground,
2381
+ cyclePattern,
2065
2382
  fullscreenEnabled,
2066
2383
  backgroundEnabled
2067
2384
  ]);
@@ -2087,9 +2404,11 @@ var MermaidViewer = react.forwardRef(
2087
2404
  exitFullscreen,
2088
2405
  toggleFullscreen,
2089
2406
  isFullscreen: () => isFullscreen,
2090
- setBackground,
2091
- cycleBackground,
2092
- getBackground: () => background
2407
+ setPattern,
2408
+ cyclePattern,
2409
+ getPattern: () => pattern,
2410
+ setSolidColor,
2411
+ getSolidColor: () => solidColor
2093
2412
  }),
2094
2413
  [
2095
2414
  vm,
@@ -2097,9 +2416,11 @@ var MermaidViewer = react.forwardRef(
2097
2416
  exitFullscreen,
2098
2417
  toggleFullscreen,
2099
2418
  isFullscreen,
2100
- setBackground,
2101
- cycleBackground,
2102
- background
2419
+ setPattern,
2420
+ cyclePattern,
2421
+ pattern,
2422
+ setSolidColor,
2423
+ solidColor
2103
2424
  ]
2104
2425
  );
2105
2426
  let countText = "";
@@ -2111,16 +2432,23 @@ var MermaidViewer = react.forwardRef(
2111
2432
  const rootClassName = [
2112
2433
  "rsm-root",
2113
2434
  dark ? "rsm-dark" : "",
2114
- `rsm-bg-${background}`,
2435
+ `rsm-pattern-${pattern}`,
2115
2436
  isFullscreen ? "rsm-fullscreen" : "",
2116
2437
  className ?? ""
2117
2438
  ].filter(Boolean).join(" ");
2439
+ const ink = patternInk(solidColor, dark);
2440
+ const rootStyle = {
2441
+ ...style ?? {},
2442
+ ["--rsm-grid-dot"]: ink.dot,
2443
+ ["--rsm-grid-line"]: ink.line,
2444
+ ...solidColor ? { ["--rsm-canvas-bg"]: solidColor } : {}
2445
+ };
2118
2446
  return /* @__PURE__ */ jsxRuntime.jsxs(
2119
2447
  "div",
2120
2448
  {
2121
2449
  ref: rootRef,
2122
2450
  className: rootClassName,
2123
- style,
2451
+ style: rootStyle,
2124
2452
  tabIndex: keyboard ? 0 : void 0,
2125
2453
  children: [
2126
2454
  toolbar ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2142,8 +2470,10 @@ var MermaidViewer = react.forwardRef(
2142
2470
  onExportSvg: exportSvg,
2143
2471
  onExportPng: exportPng,
2144
2472
  backgroundEnabled,
2145
- background,
2146
- onCycleBackground: cycleBackground,
2473
+ surface: solidColor,
2474
+ onSurfaceChange: setSolidColor,
2475
+ pattern,
2476
+ onPatternChange: setPattern,
2147
2477
  fullscreenEnabled,
2148
2478
  fullscreen: isFullscreen,
2149
2479
  onToggleFullscreen: toggleFullscreen
@@ -2210,10 +2540,12 @@ var MermaidDiagram = react.forwardRef(
2210
2540
  }
2211
2541
  );
2212
2542
 
2543
+ exports.BACKGROUND_PRESETS = BACKGROUND_PRESETS;
2213
2544
  exports.DEFAULT_THEME_OPTIONS = DEFAULT_THEME_OPTIONS;
2214
2545
  exports.DEFAULT_VIRGIL_FONT_URL = DEFAULT_VIRGIL_FONT_URL;
2215
2546
  exports.MermaidDiagram = MermaidDiagram;
2216
2547
  exports.MermaidViewer = MermaidViewer;
2548
+ exports.PATTERN_OPTIONS = PATTERN_OPTIONS;
2217
2549
  exports.SKETCH_FONT = SKETCH_FONT;
2218
2550
  exports.Toolbar = Toolbar;
2219
2551
  exports.boostLegibility = boostLegibility;