@markdy/renderer-dom 1.0.18 → 1.0.19

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
@@ -13,7 +13,7 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
13
13
  - **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
14
14
  - **Playback-rate controls** — set normalized timeline speed to slow down or speed up diagrams without rebuilding animations
15
15
  - **Interactive viewport** — opt into wheel zoom and drag pan while click-to-pause stays available
16
- - **Embed controls** — opt into a compact playback toolbar with restart, speed, and view reset controls
16
+ - **Embed controls** — opt into only the playback, timeline, viewport, export, and sharing controls a scene needs
17
17
  - **Semantic themes** — `paper`, `editorial`, `nebula`, `midnight`, `blueprint`, and `graphite`, with per-role node and edge colors
18
18
  - **Single dependency** — only `@markdy/core`
19
19
 
@@ -70,21 +70,21 @@ diagram.destroy(); // clean up DOM + cancel animations
70
70
  |---|---|---|---|
71
71
  | `container` | `HTMLElement` | *(required)* | DOM element to mount the scene into |
72
72
  | `code` | `string` | *(required)* | MarkdyScript source code |
73
- | `autoplay` | `boolean` | `plan.meta.autoplay ?? true` | Start playing immediately |
74
- | `loop` | `boolean` | `plan.meta.loop ?? true` | Loop the animation when it reaches the end |
75
- | `copyright` | `boolean` | `plan.meta.copyright ?? true` | Show a small "Powered by Markdy" badge below the animation |
73
+ | `autoplay` | `boolean` | `player.playback.autoplay ?? true` | Override whether playback starts immediately |
74
+ | `loop` | `boolean` | `player.playback.loop ?? true` | Override whether playback loops at the end |
75
+ | `copyright` | `boolean` | `player.chrome.badge ?? true` | Show the linked badge at the footer's right edge |
76
76
  | `progressBar` | `boolean \| string` | `true` | Deprecated compatibility flag for the scene-boundary progress bar |
77
- | `sceneBoundaryProgress` | `boolean \| string` | `true` | Show boundary progress bar, or pass a custom color/gradient string |
78
- | `progressColor` | `string` | `plan.meta.progressColor ?? "rainbow"` | Custom progress bar color (e.g. `"#3b82f6"`) or gradient (e.g. `"#ec4899, #8b5cf6"`) |
79
- | `playbackRate` | `number` | `plan.meta.playbackRate ?? 1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
80
- | `interactiveViewport` | `boolean` | `controls \|\| player.interaction` | Enable wheel zoom and drag pan on the rendered viewport |
81
- | `controls` | `boolean` | `player.controls` | Show the footer toolbar (play, prev/next beat, restart, seek, speed, fit, reset view, SVG, share); also enables viewport interaction |
77
+ | `sceneBoundaryProgress` | `boolean \| string` | `player.chrome.progress` | Override boundary progress, or pass a custom color/gradient |
78
+ | `progressColor` | `string` | `player.chrome.color` or rainbow | Override the progress color or gradient |
79
+ | `playbackRate` | `number` | `player.playback.rate ?? 1` | Override the initial timeline multiplier |
80
+ | `interactiveViewport` | `boolean` | `player.interaction` | `true` supplies default gestures; `false` suppresses script gestures |
81
+ | `controls` | `boolean` | `player.controls` | `true` supplies legacy toolbar defaults; `false` suppresses script controls |
82
82
  | `shareUrl` | `string` | Markdy playground | Base URL used by the Share control when building `#code=` links |
83
83
  | `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
84
84
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
85
85
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
86
86
 
87
- > **Note:** Directives declared inside the MarkdyScript code (e.g. `controls true`, `interactive true`, `progressColor "#3b82f6"`) apply automatically if the corresponding JavaScript option is omitted (`undefined`). Options passed to `createDiagram()` override the in-script declarations.
87
+ > **Note:** Prefer grouped `player:` configuration in MarkdyScript. Omitted host options preserve it; host `false` suppresses controls or interaction, while host `true` supplies legacy defaults for leaves the script does not set. A speed selector renders only with at least two distinct positive `speeds` values.
88
88
 
89
89
  ### `Diagram`
90
90
 
package/dist/index.d.ts CHANGED
@@ -26,11 +26,11 @@ interface DiagramOptions {
26
26
  progressBarColor?: string;
27
27
  /** Playback speed multiplier. Defaults to 1, where 1 is Markdy's normal pace. */
28
28
  playbackRate?: number;
29
- /** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
29
+ /** Host interaction override. `true` enables default gestures; `false` suppresses script interaction. */
30
30
  interactiveViewport?: boolean;
31
31
  /** Base URL for the Share control. Defaults to the Markdy playground. */
32
32
  shareUrl?: string;
33
- /** Show built-in playback and viewport controls. Also enables viewport interaction. Defaults to false. */
33
+ /** Host controls override. `true` enables the legacy default set; `false` suppresses script controls. */
34
34
  controls?: boolean;
35
35
  onWarning?: (warning: Diagnostic) => void;
36
36
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
package/dist/index.js CHANGED
@@ -2511,6 +2511,7 @@ function ensureSceneStyles(doc) {
2511
2511
  justify-content: space-between;
2512
2512
  gap: 8px;
2513
2513
  padding: 6px 12px;
2514
+ color: var(--md-text, #f8fafc);
2514
2515
  background: var(--md-footer-bg, rgba(15, 23, 42, 0.85));
2515
2516
  backdrop-filter: blur(16px);
2516
2517
  -webkit-backdrop-filter: blur(16px);
@@ -2521,7 +2522,9 @@ function ensureSceneStyles(doc) {
2521
2522
  display: flex;
2522
2523
  align-items: center;
2523
2524
  justify-content: flex-start;
2524
- width: 100%;
2525
+ flex: 1 1 auto;
2526
+ width: auto;
2527
+ min-width: 0;
2525
2528
  gap: 8px;
2526
2529
  max-width: 100%;
2527
2530
  overflow-x: auto;
@@ -2555,7 +2558,6 @@ function ensureSceneStyles(doc) {
2555
2558
  align-items: center;
2556
2559
  gap: 4px;
2557
2560
  flex-shrink: 0;
2558
- margin-left: auto;
2559
2561
  }
2560
2562
  .markdy-control-divider {
2561
2563
  width: 1px;
@@ -2626,37 +2628,24 @@ function ensureSceneStyles(doc) {
2626
2628
  transform: translateY(-1px) scale(1.02);
2627
2629
  }
2628
2630
  .markdy-badge {
2629
- position: absolute;
2630
- top: 10px;
2631
- right: 12px;
2632
- z-index: 25;
2633
2631
  display: inline-flex;
2634
2632
  align-items: center;
2635
- gap: 4px;
2636
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2633
+ text-align: right;
2634
+ font-family: system-ui, sans-serif;
2637
2635
  font-size: 10px;
2638
- font-weight: 500;
2639
- letter-spacing: 0.02em;
2640
- color: var(--md-text-muted, #94a3b8);
2636
+ font-weight: 400;
2637
+ color: var(--md-control-text, #94a3b8);
2641
2638
  text-decoration: none;
2642
- padding: 2.5px 7px;
2643
- border-radius: 5px;
2644
- background: var(--md-surface, rgba(15, 23, 42, 0.45));
2645
- border: 1px solid var(--md-border, rgba(148, 163, 184, 0.15));
2646
- backdrop-filter: blur(4px);
2647
- -webkit-backdrop-filter: blur(4px);
2648
- opacity: 0.65;
2649
- transition: all 0.2s ease;
2650
- pointer-events: auto;
2651
- user-select: none;
2639
+ padding: 0;
2640
+ opacity: 0.7;
2641
+ margin-left: auto;
2642
+ flex-shrink: 0;
2643
+ transition: opacity 0.15s ease, color 0.15s ease;
2644
+ white-space: nowrap;
2652
2645
  }
2653
2646
  .markdy-badge:hover {
2654
2647
  opacity: 1;
2655
- color: var(--accent, #10b981);
2656
- border-color: var(--accent, #10b981);
2657
- background: var(--md-surface-raised, rgba(15, 23, 42, 0.75));
2658
- transform: translateY(-1px);
2659
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
2648
+ color: var(--md-text, #f8fafc);
2660
2649
  }
2661
2650
  .markdy-speed-group {
2662
2651
  display: inline-flex;
@@ -2689,57 +2678,6 @@ function ensureSceneStyles(doc) {
2689
2678
  color: #ffffff !important;
2690
2679
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
2691
2680
  }
2692
- .markdy-control-seek {
2693
- appearance: none;
2694
- -webkit-appearance: none;
2695
- width: 100%;
2696
- height: 5px;
2697
- border-radius: 999px;
2698
- background: linear-gradient(
2699
- to right,
2700
- var(--accent, #10b981) 0%,
2701
- var(--accent, #10b981) var(--seek-pct, 0%),
2702
- rgba(148, 163, 184, 0.22) var(--seek-pct, 0%),
2703
- rgba(148, 163, 184, 0.22) 100%
2704
- );
2705
- outline: none;
2706
- cursor: pointer;
2707
- margin: 0;
2708
- transition: height 0.15s ease;
2709
- }
2710
- .markdy-control-seek:hover {
2711
- height: 7px;
2712
- }
2713
- .markdy-control-seek::-webkit-slider-thumb {
2714
- appearance: none;
2715
- -webkit-appearance: none;
2716
- width: 12px;
2717
- height: 12px;
2718
- border-radius: 50%;
2719
- background: #ffffff;
2720
- border: 2px solid var(--accent, #10b981);
2721
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
2722
- cursor: pointer;
2723
- transition: transform 0.15s ease, box-shadow 0.15s ease;
2724
- }
2725
- .markdy-control-seek:hover::-webkit-slider-thumb {
2726
- transform: scale(1.25);
2727
- box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2728
- }
2729
- .markdy-control-seek::-moz-range-thumb {
2730
- width: 12px;
2731
- height: 12px;
2732
- border-radius: 50%;
2733
- background: #ffffff;
2734
- border: 2px solid var(--accent, #10b981);
2735
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
2736
- cursor: pointer;
2737
- transition: transform 0.15s ease, box-shadow 0.15s ease;
2738
- }
2739
- .markdy-control-seek:hover::-moz-range-thumb {
2740
- transform: scale(1.25);
2741
- box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2742
- }
2743
2681
  .markdy-control-time {
2744
2682
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
2745
2683
  font-size: 10px;
@@ -2925,44 +2863,61 @@ function ensureSceneStyles(doc) {
2925
2863
  .markdy-control-seek::-webkit-slider-runnable-track {
2926
2864
  width: 100%;
2927
2865
  height: 4.5px;
2928
- background: rgba(148, 163, 184, 0.35);
2866
+ background: linear-gradient(
2867
+ to right,
2868
+ var(--accent, #10b981) 0%,
2869
+ var(--accent, #10b981) var(--seek-pct, 0%),
2870
+ rgba(148, 163, 184, 0.35) var(--seek-pct, 0%),
2871
+ rgba(148, 163, 184, 0.35) 100%
2872
+ );
2929
2873
  border-radius: 9999px;
2930
- transition: background 0.15s ease;
2931
- }
2932
- .markdy-control-seek:hover::-webkit-slider-runnable-track {
2933
- background: rgba(148, 163, 184, 0.55);
2934
2874
  }
2935
2875
  .markdy-control-seek::-webkit-slider-thumb {
2876
+ appearance: none;
2936
2877
  -webkit-appearance: none;
2937
2878
  height: 13px;
2938
2879
  width: 13px;
2939
2880
  border-radius: 50%;
2940
- background: #2563eb;
2881
+ background: var(--accent, #10b981);
2941
2882
  border: 2px solid #ffffff;
2942
2883
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
2943
2884
  margin-top: -4.25px;
2944
2885
  cursor: grab;
2945
- transition: transform 0.1s ease, background 0.15s ease;
2886
+ transition: transform 0.1s ease, box-shadow 0.15s ease;
2887
+ }
2888
+ .markdy-control-seek:hover::-webkit-slider-thumb {
2889
+ transform: scale(1.2);
2890
+ box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2946
2891
  }
2947
2892
  .markdy-control-seek:active::-webkit-slider-thumb {
2948
2893
  cursor: grabbing;
2949
2894
  transform: scale(1.2);
2950
- background: #1d4ed8;
2951
2895
  }
2952
2896
  .markdy-control-seek::-moz-range-track {
2953
2897
  width: 100%;
2954
2898
  height: 4.5px;
2955
- background: rgba(148, 163, 184, 0.35);
2899
+ background: linear-gradient(
2900
+ to right,
2901
+ var(--accent, #10b981) 0%,
2902
+ var(--accent, #10b981) var(--seek-pct, 0%),
2903
+ rgba(148, 163, 184, 0.35) var(--seek-pct, 0%),
2904
+ rgba(148, 163, 184, 0.35) 100%
2905
+ );
2956
2906
  border-radius: 9999px;
2957
2907
  }
2958
2908
  .markdy-control-seek::-moz-range-thumb {
2959
2909
  height: 13px;
2960
2910
  width: 13px;
2961
2911
  border-radius: 50%;
2962
- background: #2563eb;
2912
+ background: var(--accent, #10b981);
2963
2913
  border: 2px solid #ffffff;
2964
2914
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
2965
2915
  cursor: grab;
2916
+ transition: transform 0.1s ease, box-shadow 0.15s ease;
2917
+ }
2918
+ .markdy-control-seek:hover::-moz-range-thumb {
2919
+ transform: scale(1.2);
2920
+ box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2966
2921
  }
2967
2922
 
2968
2923
  [data-markdy-theme="midnight"] .markdy-controls button,
@@ -2983,9 +2938,9 @@ function ensureSceneStyles(doc) {
2983
2938
  [data-markdy-theme="nebula"] .markdy-controls button:hover:not([aria-pressed="true"]),
2984
2939
  :root[data-theme="dark"] .markdy-controls button:hover:not([aria-pressed="true"]),
2985
2940
  .theme-dark .markdy-controls button:hover:not([aria-pressed="true"]) {
2986
- background: rgba(51, 65, 85, 0.95);
2987
- color: #f8fafc;
2988
- border-color: #94a3b8;
2941
+ background: var(--md-control-hover-bg, rgba(51, 65, 85, 0.95));
2942
+ color: var(--md-control-hover-text, #f8fafc);
2943
+ border-color: var(--md-control-hover-border, #94a3b8);
2989
2944
  }
2990
2945
  [data-markdy-theme="midnight"] .markdy-controls button[aria-pressed="true"],
2991
2946
  [data-markdy-theme="blueprint"] .markdy-controls button[aria-pressed="true"],
@@ -2994,10 +2949,10 @@ function ensureSceneStyles(doc) {
2994
2949
  [data-markdy-theme="nebula"] .markdy-controls button[aria-pressed="true"],
2995
2950
  :root[data-theme="dark"] .markdy-controls button[aria-pressed="true"],
2996
2951
  .theme-dark .markdy-controls button[aria-pressed="true"] {
2997
- background: #3b82f6 !important;
2952
+ background: var(--accent, #3b82f6) !important;
2998
2953
  color: #ffffff !important;
2999
- border-color: #3b82f6 !important;
3000
- box-shadow: 0 0 10px rgba(59, 130, 246, 0.5) !important;
2954
+ border-color: var(--accent, #3b82f6) !important;
2955
+ box-shadow: 0 0 10px var(--accent-glow, rgba(59, 130, 246, 0.5)) !important;
3001
2956
  }
3002
2957
  .markdy-footer a {
3003
2958
  transition: opacity 0.15s ease, color 0.15s ease;
@@ -3091,41 +3046,56 @@ function ensureSceneStyles(doc) {
3091
3046
  `;
3092
3047
  doc.head.appendChild(style);
3093
3048
  }
3094
- function applyThemeToScene(scene, theme) {
3095
- scene.style.background = theme.canvas;
3096
- scene.style.color = theme.text;
3097
- scene.style.setProperty("--md-canvas", theme.canvas);
3098
- scene.style.setProperty("--md-surface", theme.surface);
3099
- scene.style.setProperty("--md-surface-raised", theme.surfaceRaised);
3100
- scene.style.setProperty("--md-border", theme.border);
3101
- scene.style.setProperty("--md-text", theme.text);
3102
- scene.style.setProperty("--md-text-muted", theme.textMuted);
3103
- scene.style.setProperty("--md-paper", theme.paper ?? theme.canvas);
3104
- scene.style.setProperty("--md-ink", theme.ink ?? theme.text);
3105
- scene.style.setProperty("--md-muted", theme.muted ?? theme.textMuted);
3106
- scene.style.setProperty("--md-rule", theme.rule ?? theme.border);
3107
- scene.style.setProperty("--md-grid-minor", theme.gridMinor);
3108
- scene.style.setProperty("--md-grid-major", theme.gridMajor);
3109
- scene.style.setProperty("--md-vignette", theme.vignette);
3110
- scene.style.setProperty("--md-accent", theme.accent);
3111
- if (theme.link) scene.style.setProperty("--md-link", theme.link);
3112
- if (theme.soft) scene.style.setProperty("--md-soft", theme.soft);
3113
- if (theme.accentTint) scene.style.setProperty("--md-accent-tint", theme.accentTint);
3114
- scene.style.setProperty("--md-node-surface", theme.nodeSurface ?? theme.surface);
3115
- scene.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
3116
- scene.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
3117
- scene.style.setProperty("--md-shadow", theme.shadow ?? "rgba(2, 6, 23, 0.55)");
3118
- if (theme.fonts?.title) scene.style.setProperty("--md-font-title", theme.fonts.title);
3119
- if (theme.fonts?.nodeName) scene.style.setProperty("--md-font-node", theme.fonts.nodeName);
3120
- if (theme.fonts?.mono) scene.style.setProperty("--md-font-mono", theme.fonts.mono);
3121
- if (theme.radiusMd) scene.style.setProperty("--md-radius-md", `${theme.radiusMd}px`);
3049
+ function applyThemeVariables(element, theme) {
3050
+ element.style.setProperty("--md-canvas", theme.canvas);
3051
+ element.style.setProperty("--md-surface", theme.surface);
3052
+ element.style.setProperty("--md-surface-raised", theme.surfaceRaised);
3053
+ element.style.setProperty("--md-border", theme.border);
3054
+ element.style.setProperty("--md-text", theme.text);
3055
+ element.style.setProperty("--md-text-muted", theme.textMuted);
3056
+ element.style.setProperty("--md-paper", theme.paper ?? theme.canvas);
3057
+ element.style.setProperty("--md-ink", theme.ink ?? theme.text);
3058
+ element.style.setProperty("--md-muted", theme.muted ?? theme.textMuted);
3059
+ element.style.setProperty("--md-rule", theme.rule ?? theme.border);
3060
+ element.style.setProperty("--md-grid-minor", theme.gridMinor);
3061
+ element.style.setProperty("--md-grid-major", theme.gridMajor);
3062
+ element.style.setProperty("--md-vignette", theme.vignette);
3063
+ element.style.setProperty("--md-accent", theme.accent);
3064
+ element.style.setProperty("--accent", theme.accent);
3065
+ element.style.setProperty("--accent-glow", `color-mix(in srgb, ${theme.accent} 40%, transparent)`);
3066
+ element.style.setProperty("--accent-light", theme.accentTint ?? `color-mix(in srgb, ${theme.accent} 15%, transparent)`);
3067
+ if (theme.link) element.style.setProperty("--md-link", theme.link);
3068
+ if (theme.soft) element.style.setProperty("--md-soft", theme.soft);
3069
+ if (theme.accentTint) element.style.setProperty("--md-accent-tint", theme.accentTint);
3070
+ element.style.setProperty("--md-node-surface", theme.nodeSurface ?? theme.surface);
3071
+ element.style.setProperty("--md-node-surface-raised", theme.nodeSurfaceRaised ?? theme.surfaceRaised);
3072
+ element.style.setProperty("--md-hairline", theme.hairline ?? `color-mix(in srgb, ${theme.border} 50%, transparent)`);
3073
+ element.style.setProperty("--md-shadow", theme.shadow ?? "rgba(2, 6, 23, 0.55)");
3074
+ element.style.setProperty("--md-footer-bg", `color-mix(in srgb, ${theme.surface} 92%, transparent)`);
3075
+ element.style.setProperty("--md-footer-border", theme.border);
3076
+ element.style.setProperty("--md-control-bg", theme.surfaceRaised);
3077
+ element.style.setProperty("--md-control-border", theme.border);
3078
+ element.style.setProperty("--md-control-text", theme.textMuted);
3079
+ element.style.setProperty("--md-control-hover-bg", theme.canvas);
3080
+ element.style.setProperty("--md-control-hover-text", theme.text);
3081
+ element.style.setProperty("--md-control-hover-border", theme.accent);
3082
+ element.style.setProperty("--md-divider", theme.border);
3083
+ if (theme.fonts?.title) element.style.setProperty("--md-font-title", theme.fonts.title);
3084
+ if (theme.fonts?.nodeName) element.style.setProperty("--md-font-node", theme.fonts.nodeName);
3085
+ if (theme.fonts?.mono) element.style.setProperty("--md-font-mono", theme.fonts.mono);
3086
+ if (theme.radiusMd) element.style.setProperty("--md-radius-md", `${theme.radiusMd}px`);
3122
3087
  if (theme.spacing) {
3123
3088
  for (const [key, value] of Object.entries(theme.spacing)) {
3124
- scene.style.setProperty(`--md-space-${key}`, `${value}px`);
3089
+ element.style.setProperty(`--md-space-${key}`, `${value}px`);
3125
3090
  }
3126
3091
  }
3127
- scene.dataset.markdyTheme = theme.name;
3128
- if (theme.flatCards) scene.dataset.flat = "1";
3092
+ element.dataset.markdyTheme = theme.name;
3093
+ if (theme.flatCards) element.dataset.flat = "1";
3094
+ }
3095
+ function applyThemeToScene(scene, theme) {
3096
+ applyThemeVariables(scene, theme);
3097
+ scene.style.background = theme.canvas;
3098
+ scene.style.color = theme.text;
3129
3099
  }
3130
3100
 
3131
3101
  // src/diagram.ts
@@ -3296,11 +3266,12 @@ function createDiagram(opts) {
3296
3266
  if (footer) return footer;
3297
3267
  footer = document.createElement("div");
3298
3268
  footer.className = "markdy-footer";
3269
+ applyThemeVariables(footer, plan.theme);
3299
3270
  Object.assign(footer.style, {
3300
3271
  display: "flex",
3301
3272
  alignItems: "center",
3302
3273
  justifyContent: "space-between",
3303
- flexWrap: "wrap",
3274
+ flexWrap: "nowrap",
3304
3275
  gap: "6px",
3305
3276
  width: "100%",
3306
3277
  boxSizing: "border-box",
@@ -3310,6 +3281,21 @@ function createDiagram(opts) {
3310
3281
  else container.appendChild(footer);
3311
3282
  return footer;
3312
3283
  }
3284
+ let badge = null;
3285
+ if (copyright) {
3286
+ badge = document.createElement("a");
3287
+ badge.className = "markdy-badge";
3288
+ badge.href = `${MARKDY_PLAYGROUND_URL}#code=${encodeCodeForPlaygroundHash(code)}`;
3289
+ badge.target = "_blank";
3290
+ badge.rel = "noopener noreferrer";
3291
+ badge.title = "Open and edit in Markdy Playground";
3292
+ badge.textContent = "Powered by Markdy";
3293
+ ensureFooter().appendChild(badge);
3294
+ const badgeLink = badge;
3295
+ void compressMarkdyToUrlHash(code).then((hash) => {
3296
+ badgeLink.href = `${MARKDY_PLAYGROUND_URL}#code=${hash}`;
3297
+ }).catch(() => void 0);
3298
+ }
3313
3299
  ensureSceneStyles(document);
3314
3300
  ensureNodeStyles(document);
3315
3301
  const scene = document.createElement("div");
@@ -3326,17 +3312,6 @@ function createDiagram(opts) {
3326
3312
  });
3327
3313
  applyThemeToScene(scene, plan.theme);
3328
3314
  viewport.appendChild(scene);
3329
- let badge = null;
3330
- if (copyright) {
3331
- badge = document.createElement("a");
3332
- badge.className = "markdy-badge";
3333
- badge.href = "https://markdy.com";
3334
- badge.target = "_blank";
3335
- badge.rel = "noopener noreferrer";
3336
- badge.title = "Powered by Markdy - Diagram as Code";
3337
- badge.textContent = "Powered by Markdy";
3338
- viewport.appendChild(badge);
3339
- }
3340
3315
  const viewportTransform = document.createElement("div");
3341
3316
  viewportTransform.className = "markdy-viewport-transform";
3342
3317
  Object.assign(viewportTransform.style, {
@@ -3548,6 +3523,7 @@ function createDiagram(opts) {
3548
3523
  let controlsSeekBar = null;
3549
3524
  let controlsTimeEl = null;
3550
3525
  let controlsFitButton = null;
3526
+ let closeCodePanel = null;
3551
3527
  let fitViewActive = false;
3552
3528
  const ICONS2 = {
3553
3529
  play: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
@@ -3767,8 +3743,9 @@ function createDiagram(opts) {
3767
3743
  for (const anim of allAnims) anim.cancel();
3768
3744
  resizeObserver?.disconnect();
3769
3745
  removeFullscreenListeners?.();
3746
+ closeCodePanel?.();
3747
+ closeCodePanel = null;
3770
3748
  if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
3771
- if (badge?.parentNode) badge.parentNode.removeChild(badge);
3772
3749
  if (footer?.parentNode) footer.parentNode.removeChild(footer);
3773
3750
  if (viewport.parentNode === container) container.removeChild(viewport);
3774
3751
  }
@@ -3935,19 +3912,33 @@ function createDiagram(opts) {
3935
3912
  }
3936
3913
  function tintCode(raw) {
3937
3914
  return raw.split("\n").map((line) => {
3938
- if (/^\s*\/\//.test(line)) {
3939
- return `<span class="t-comment">${escHtml(line)}</span>`;
3940
- }
3941
- line = line.replace(/(->|<-|~>|--|<~)/g, '<span class="t-edge">$1</span>');
3942
- line = line.replace(/"([^"]*)"/g, '"<span class="t-string">$1</span>"');
3943
- line = line.replace(
3944
- /^(\s*)(scene|beat|group|player|show|frame|glow|focus|layout|edge|annotation|start|end|decision|service|browser|gateway|database|cache|queue|worker|function|pod|user|client|hub|station|metric|mobile)\b/,
3945
- '$1<span class="t-keyword">$2</span>'
3915
+ if (/^\s*\/\//.test(line)) return `<span class="t-comment">${escHtml(line)}</span>`;
3916
+ const keyword = line.match(
3917
+ /^(\s*)(scene|beat|group|player|show|frame|glow|focus|layout|edge|annotation|start|end|decision|service|browser|gateway|database|cache|queue|worker|function|pod|user|client|hub|station|metric|mobile)\b/
3946
3918
  );
3947
- line = line.replace(/\b(\d[\d.]*(?:ms|px|s)?)\b/g, '<span class="t-number">$1</span>');
3948
- return line;
3919
+ const prefix = keyword ? `${escHtml(keyword[1])}<span class="t-keyword">${keyword[2]}</span>` : "";
3920
+ return prefix + tintInlineCode(line.slice(keyword?.[0].length ?? 0));
3949
3921
  }).join("\n");
3950
3922
  }
3923
+ function tintInlineCode(raw) {
3924
+ const tokenPattern = /"[^"]*"|->|<-|~>|--|<~|\b\d[\d.]*(?:ms|px|s)?\b/g;
3925
+ let output = "";
3926
+ let cursor = 0;
3927
+ for (const match of raw.matchAll(tokenPattern)) {
3928
+ const token = match[0];
3929
+ const index = match.index ?? 0;
3930
+ output += escHtml(raw.slice(cursor, index));
3931
+ if (token.startsWith('"')) {
3932
+ output += `"<span class="t-string">${escHtml(token.slice(1, -1))}</span>"`;
3933
+ } else if (/^\d/.test(token)) {
3934
+ output += `<span class="t-number">${token}</span>`;
3935
+ } else {
3936
+ output += `<span class="t-edge">${escHtml(token)}</span>`;
3937
+ }
3938
+ cursor = index + token.length;
3939
+ }
3940
+ return output + escHtml(raw.slice(cursor));
3941
+ }
3951
3942
  function escHtml(str) {
3952
3943
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3953
3944
  }
@@ -3957,6 +3948,7 @@ function createDiagram(opts) {
3957
3948
  button.className = "markdy-control-code";
3958
3949
  button.setAttribute("aria-haspopup", "dialog");
3959
3950
  button.addEventListener("click", () => {
3951
+ closeCodePanel?.();
3960
3952
  const overlay = document.createElement("div");
3961
3953
  overlay.className = "markdy-code-panel-overlay";
3962
3954
  overlay.setAttribute("role", "dialog");
@@ -4014,24 +4006,28 @@ function createDiagram(opts) {
4014
4006
  overlay.appendChild(panel);
4015
4007
  document.body.appendChild(overlay);
4016
4008
  closeBtn.focus();
4009
+ let closing = false;
4010
+ function removePanel() {
4011
+ document.removeEventListener("keydown", handleEscape);
4012
+ overlay.remove();
4013
+ if (closeCodePanel === removePanel) closeCodePanel = null;
4014
+ }
4017
4015
  function closePanel() {
4016
+ if (closing) return;
4017
+ closing = true;
4018
4018
  overlay.dataset.closing = "1";
4019
- overlay.addEventListener("animationend", () => overlay.remove(), { once: true });
4019
+ document.removeEventListener("keydown", handleEscape);
4020
+ overlay.addEventListener("animationend", removePanel, { once: true });
4020
4021
  }
4021
4022
  closeBtn.addEventListener("click", closePanel);
4022
4023
  overlay.addEventListener("click", (e) => {
4023
4024
  if (e.target === overlay) closePanel();
4024
4025
  });
4025
4026
  function handleEscape(e) {
4026
- if (e.key === "Escape") {
4027
- closePanel();
4028
- document.removeEventListener("keydown", handleEscape);
4029
- }
4027
+ if (e.key === "Escape") closePanel();
4030
4028
  }
4031
4029
  document.addEventListener("keydown", handleEscape);
4032
- overlay.addEventListener("animationend", () => {
4033
- if (overlay.dataset.closing === "1") document.removeEventListener("keydown", handleEscape);
4034
- }, { once: true });
4030
+ closeCodePanel = removePanel;
4035
4031
  });
4036
4032
  toolbar.appendChild(button);
4037
4033
  }
@@ -4238,7 +4234,7 @@ function createDiagram(opts) {
4238
4234
  mountCodeControl(toolsGroup);
4239
4235
  }
4240
4236
  if (toolsGroup.children.length > 0) toolbar.appendChild(toolsGroup);
4241
- ensureFooter().appendChild(toolbar);
4237
+ ensureFooter().insertBefore(toolbar, badge);
4242
4238
  syncControls();
4243
4239
  }
4244
4240
  viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "html2canvas": "^1.4.1",
48
- "@markdy/core": "1.0.18"
48
+ "@markdy/core": "1.0.19"
49
49
  },
50
50
  "devDependencies": {
51
51
  "jsdom": "^29.1.1",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "^5.9.3",
54
54
  "vitest": "^4.1.7",
55
- "@markdy/stdlib-systems": "1.0.18"
55
+ "@markdy/stdlib-systems": "1.0.19"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",