@markdy/renderer-dom 1.0.13 → 1.0.14

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.
Files changed (2) hide show
  1. package/dist/index.js +578 -79
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2503,15 +2503,26 @@ function ensureSceneStyles(doc) {
2503
2503
  will-change: opacity, transform;
2504
2504
  }
2505
2505
  .markdy-footer {
2506
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2506
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
2507
2507
  width: 100%;
2508
2508
  box-sizing: border-box;
2509
+ display: flex;
2510
+ align-items: center;
2511
+ justify-content: space-between;
2512
+ gap: 8px;
2513
+ padding: 6px 12px;
2514
+ background: var(--md-footer-bg, rgba(15, 23, 42, 0.85));
2515
+ backdrop-filter: blur(16px);
2516
+ -webkit-backdrop-filter: blur(16px);
2517
+ border-top: 1px solid var(--md-footer-border, rgba(148, 163, 184, 0.15));
2518
+ z-index: 20;
2509
2519
  }
2510
2520
  .markdy-controls {
2511
2521
  display: flex;
2512
2522
  align-items: center;
2513
- flex-wrap: nowrap;
2514
- gap: 2.5px;
2523
+ justify-content: flex-start;
2524
+ width: 100%;
2525
+ gap: 8px;
2515
2526
  max-width: 100%;
2516
2527
  overflow-x: auto;
2517
2528
  scrollbar-width: none;
@@ -2519,65 +2530,367 @@ function ensureSceneStyles(doc) {
2519
2530
  .markdy-controls::-webkit-scrollbar {
2520
2531
  display: none;
2521
2532
  }
2522
- @media (max-width: 640px) {
2523
- .markdy-controls {
2524
- flex-wrap: wrap;
2525
- justify-content: center;
2526
- gap: 3px;
2527
- }
2533
+ .markdy-controls-group {
2534
+ display: inline-flex;
2535
+ align-items: center;
2536
+ gap: 4px;
2537
+ flex-shrink: 0;
2538
+ }
2539
+ .markdy-controls-playback {
2540
+ display: inline-flex;
2541
+ align-items: center;
2542
+ gap: 4px;
2543
+ flex-shrink: 0;
2544
+ }
2545
+ .markdy-controls-timeline {
2546
+ display: flex;
2547
+ align-items: center;
2548
+ gap: 8px;
2549
+ flex: 1 1 180px;
2550
+ min-width: 110px;
2551
+ max-width: 340px;
2552
+ }
2553
+ .markdy-controls-tools {
2554
+ display: inline-flex;
2555
+ align-items: center;
2556
+ gap: 4px;
2557
+ flex-shrink: 0;
2558
+ margin-left: auto;
2559
+ }
2560
+ .markdy-control-divider {
2561
+ width: 1px;
2562
+ height: 14px;
2563
+ background: var(--md-divider, rgba(148, 163, 184, 0.2));
2564
+ margin: 0 2px;
2565
+ flex-shrink: 0;
2528
2566
  }
2529
2567
  .markdy-controls button {
2530
2568
  appearance: none;
2569
+ -webkit-appearance: none;
2531
2570
  touch-action: manipulation;
2532
2571
  user-select: none;
2533
2572
  -webkit-user-select: none;
2534
- border: 1px solid var(--md-control-border, rgba(148, 163, 184, 0.45));
2535
- border-radius: 5px;
2536
- background: var(--md-control-bg, rgba(248, 250, 252, 0.92));
2537
- color: var(--md-control-text, #334155);
2573
+ border: 1px solid var(--md-control-border, rgba(148, 163, 184, 0.2));
2574
+ border-radius: 7px;
2575
+ background: var(--md-control-bg, rgba(255, 255, 255, 0.04));
2576
+ color: var(--md-control-text, #94a3b8);
2538
2577
  cursor: pointer;
2539
- font: 600 10px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2540
- padding: 2.5px 5.5px;
2541
- min-height: 23px;
2542
- min-width: 22px;
2578
+ font: 600 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2579
+ height: 27px;
2580
+ min-width: 27px;
2581
+ padding: 0 6px;
2543
2582
  display: inline-flex;
2544
2583
  align-items: center;
2545
2584
  justify-content: center;
2546
- gap: 2px;
2585
+ gap: 5px;
2547
2586
  white-space: nowrap;
2548
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
2587
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
2549
2588
  transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
2550
2589
  }
2551
2590
  .markdy-controls button:hover:not([aria-pressed="true"]) {
2552
- background: var(--md-control-hover-bg, #ffffff);
2553
- color: var(--md-control-hover-text, #0f172a);
2554
- border-color: var(--md-control-hover-border, #94a3b8);
2555
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
2591
+ background: var(--md-control-hover-bg, rgba(255, 255, 255, 0.1));
2592
+ color: var(--md-control-hover-text, #f8fafc);
2593
+ border-color: var(--md-control-hover-border, rgba(148, 163, 184, 0.35));
2594
+ transform: translateY(-1px);
2595
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12);
2556
2596
  }
2557
2597
  .markdy-controls button[aria-pressed="true"] {
2558
- background: #0f172a !important;
2598
+ background: var(--accent, #10b981) !important;
2559
2599
  color: #ffffff !important;
2560
- border-color: #0f172a !important;
2561
- box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25) !important;
2600
+ border-color: var(--accent, #10b981) !important;
2601
+ box-shadow: 0 0 10px var(--accent-glow, rgba(16, 185, 129, 0.4)) !important;
2562
2602
  }
2563
2603
  .markdy-controls button:focus-visible {
2564
- outline: 2px solid #3b82f6;
2604
+ outline: 2px solid var(--accent, #10b981);
2565
2605
  outline-offset: 1px;
2566
2606
  }
2567
2607
  .markdy-controls button:active {
2568
2608
  transform: scale(0.95);
2569
2609
  }
2570
- .markdy-control-play::before { content: "\u25B6"; font-size: 7.5px; margin-right: 1px; }
2571
- .markdy-control-play[aria-label*="Pause"]::before,
2572
- .markdy-control-play[title*="Pause"]::before { content: "\u23F8"; font-size: 7.5px; margin-right: 1px; }
2573
- .markdy-control-restart::before { content: "\u21BA"; font-size: 10px; font-weight: 700; margin-right: 1px; }
2574
- .markdy-control-prev-beat::before { content: "\u23EE"; font-size: 7.5px; margin-right: 1px; }
2575
- .markdy-control-next-beat::before { content: "\u23ED"; font-size: 7.5px; margin-right: 1px; }
2576
- .markdy-control-fit::before { content: "\u26F6"; font-size: 8.5px; margin-right: 1px; }
2577
- .markdy-control-reset-view::before { content: "\u2299"; font-size: 8.5px; margin-right: 1px; }
2578
- .markdy-control-fullscreen::before { content: "\u26F6"; font-size: 8.5px; margin-right: 1px; }
2579
- .markdy-control-svg::before { content: "\u{1F4E5}"; font-size: 8.5px; margin-right: 1px; }
2580
- .markdy-control-share::before { content: "\u{1F517}"; font-size: 8.5px; margin-right: 1px; }
2610
+ .markdy-icon {
2611
+ display: inline-block;
2612
+ flex-shrink: 0;
2613
+ vertical-align: middle;
2614
+ }
2615
+ .markdy-control-play {
2616
+ background: var(--accent, #10b981) !important;
2617
+ color: #ffffff !important;
2618
+ border-color: var(--accent, #10b981) !important;
2619
+ padding: 0 10px !important;
2620
+ font-weight: 700 !important;
2621
+ box-shadow: 0 2px 8px var(--accent-glow, rgba(16, 185, 129, 0.35)) !important;
2622
+ }
2623
+ .markdy-control-play:hover {
2624
+ filter: brightness(1.08);
2625
+ box-shadow: 0 2px 12px var(--accent-glow, rgba(16, 185, 129, 0.55)) !important;
2626
+ transform: translateY(-1px) scale(1.02);
2627
+ }
2628
+ .markdy-badge {
2629
+ display: inline-flex;
2630
+ align-items: center;
2631
+ gap: 4px;
2632
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2633
+ font-size: 11px;
2634
+ font-weight: 600;
2635
+ color: var(--md-control-text, #94a3b8);
2636
+ text-decoration: none;
2637
+ padding: 3px 8px;
2638
+ border-radius: 999px;
2639
+ background: var(--md-control-bg, rgba(255, 255, 255, 0.05));
2640
+ border: 1px solid var(--md-control-border, rgba(148, 163, 184, 0.18));
2641
+ margin-left: auto;
2642
+ flex-shrink: 0;
2643
+ transition: all 0.15s ease;
2644
+ white-space: nowrap;
2645
+ }
2646
+ .markdy-badge:hover {
2647
+ color: var(--accent, #10b981);
2648
+ background: var(--accent-light, rgba(16, 185, 129, 0.12));
2649
+ border-color: var(--accent, #10b981);
2650
+ transform: translateY(-1px);
2651
+ }
2652
+ .markdy-speed-group {
2653
+ display: inline-flex;
2654
+ align-items: center;
2655
+ background: var(--md-segmented-bg, rgba(0, 0, 0, 0.18));
2656
+ border: 1px solid var(--md-control-border, rgba(148, 163, 184, 0.18));
2657
+ border-radius: 8px;
2658
+ padding: 2px;
2659
+ gap: 1px;
2660
+ }
2661
+ .markdy-speed-group .markdy-control-rate {
2662
+ border: none;
2663
+ background: transparent;
2664
+ border-radius: 5px;
2665
+ height: 22px;
2666
+ min-width: 25px;
2667
+ padding: 0 5px;
2668
+ font-size: 10px;
2669
+ font-weight: 700;
2670
+ color: var(--md-control-text, #94a3b8);
2671
+ box-shadow: none;
2672
+ }
2673
+ .markdy-speed-group .markdy-control-rate:hover:not([aria-pressed="true"]) {
2674
+ background: rgba(255, 255, 255, 0.08);
2675
+ color: #ffffff;
2676
+ transform: none;
2677
+ }
2678
+ .markdy-speed-group .markdy-control-rate[aria-pressed="true"] {
2679
+ background: var(--accent, #10b981) !important;
2680
+ color: #ffffff !important;
2681
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
2682
+ }
2683
+ .markdy-control-seek {
2684
+ appearance: none;
2685
+ -webkit-appearance: none;
2686
+ width: 100%;
2687
+ height: 5px;
2688
+ border-radius: 999px;
2689
+ background: linear-gradient(
2690
+ to right,
2691
+ var(--accent, #10b981) 0%,
2692
+ var(--accent, #10b981) var(--seek-pct, 0%),
2693
+ rgba(148, 163, 184, 0.22) var(--seek-pct, 0%),
2694
+ rgba(148, 163, 184, 0.22) 100%
2695
+ );
2696
+ outline: none;
2697
+ cursor: pointer;
2698
+ margin: 0;
2699
+ transition: height 0.15s ease;
2700
+ }
2701
+ .markdy-control-seek:hover {
2702
+ height: 7px;
2703
+ }
2704
+ .markdy-control-seek::-webkit-slider-thumb {
2705
+ appearance: none;
2706
+ -webkit-appearance: none;
2707
+ width: 12px;
2708
+ height: 12px;
2709
+ border-radius: 50%;
2710
+ background: #ffffff;
2711
+ border: 2px solid var(--accent, #10b981);
2712
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
2713
+ cursor: pointer;
2714
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
2715
+ }
2716
+ .markdy-control-seek:hover::-webkit-slider-thumb {
2717
+ transform: scale(1.25);
2718
+ box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2719
+ }
2720
+ .markdy-control-seek::-moz-range-thumb {
2721
+ width: 12px;
2722
+ height: 12px;
2723
+ border-radius: 50%;
2724
+ background: #ffffff;
2725
+ border: 2px solid var(--accent, #10b981);
2726
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
2727
+ cursor: pointer;
2728
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
2729
+ }
2730
+ .markdy-control-seek:hover::-moz-range-thumb {
2731
+ transform: scale(1.25);
2732
+ box-shadow: 0 0 8px var(--accent-glow, rgba(16, 185, 129, 0.6));
2733
+ }
2734
+ .markdy-control-time {
2735
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
2736
+ font-size: 10px;
2737
+ font-weight: 600;
2738
+ color: var(--md-control-text, #94a3b8);
2739
+ white-space: nowrap;
2740
+ flex-shrink: 0;
2741
+ letter-spacing: -0.02em;
2742
+ }
2743
+ .markdy-btn-flashed {
2744
+ background: rgba(16, 185, 129, 0.2) !important;
2745
+ color: #10b981 !important;
2746
+ border-color: #10b981 !important;
2747
+ }
2748
+ /* \u2500\u2500 Code panel overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2749
+ .markdy-code-panel-overlay {
2750
+ position: fixed;
2751
+ inset: 0;
2752
+ z-index: 9000;
2753
+ background: rgba(2, 6, 23, 0.55);
2754
+ backdrop-filter: blur(3px);
2755
+ -webkit-backdrop-filter: blur(3px);
2756
+ display: flex;
2757
+ align-items: center;
2758
+ justify-content: center;
2759
+ padding: 24px;
2760
+ box-sizing: border-box;
2761
+ opacity: 0;
2762
+ animation: markdy-fade-in 0.18s ease forwards;
2763
+ }
2764
+ .markdy-code-panel-overlay[data-closing="1"] {
2765
+ animation: markdy-fade-out 0.15s ease forwards;
2766
+ }
2767
+ @keyframes markdy-fade-in {
2768
+ from { opacity: 0; }
2769
+ to { opacity: 1; }
2770
+ }
2771
+ @keyframes markdy-fade-out {
2772
+ from { opacity: 1; }
2773
+ to { opacity: 0; }
2774
+ }
2775
+ .markdy-code-panel {
2776
+ position: relative;
2777
+ background: #0f172a;
2778
+ border: 1px solid rgba(71, 85, 105, 0.7);
2779
+ border-radius: 12px;
2780
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);
2781
+ width: 100%;
2782
+ max-width: 780px;
2783
+ max-height: min(680px, 82vh);
2784
+ display: flex;
2785
+ flex-direction: column;
2786
+ overflow: hidden;
2787
+ transform: translateY(10px) scale(0.98);
2788
+ animation: markdy-panel-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
2789
+ }
2790
+ .markdy-code-panel-overlay[data-closing="1"] .markdy-code-panel {
2791
+ animation: markdy-panel-out 0.15s ease forwards;
2792
+ }
2793
+ @keyframes markdy-panel-in {
2794
+ from { transform: translateY(12px) scale(0.97); opacity: 0; }
2795
+ to { transform: translateY(0) scale(1); opacity: 1; }
2796
+ }
2797
+ @keyframes markdy-panel-out {
2798
+ from { transform: translateY(0) scale(1); opacity: 1; }
2799
+ to { transform: translateY(8px) scale(0.98); opacity: 0; }
2800
+ }
2801
+ .markdy-code-panel__header {
2802
+ display: flex;
2803
+ align-items: center;
2804
+ justify-content: space-between;
2805
+ padding: 12px 16px 10px;
2806
+ border-bottom: 1px solid rgba(71, 85, 105, 0.4);
2807
+ flex-shrink: 0;
2808
+ }
2809
+ .markdy-code-panel__title {
2810
+ font: 600 12px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
2811
+ color: #94a3b8;
2812
+ letter-spacing: 0.06em;
2813
+ text-transform: uppercase;
2814
+ }
2815
+ .markdy-code-panel__actions {
2816
+ display: flex;
2817
+ align-items: center;
2818
+ gap: 6px;
2819
+ }
2820
+ .markdy-code-panel__copy,
2821
+ .markdy-code-panel__playground,
2822
+ .markdy-code-panel__close {
2823
+ appearance: none;
2824
+ border: 1px solid rgba(71, 85, 105, 0.55);
2825
+ border-radius: 6px;
2826
+ background: rgba(30, 41, 59, 0.8);
2827
+ color: #94a3b8;
2828
+ cursor: pointer;
2829
+ font: 600 10.5px/1.2 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
2830
+ text-decoration: none;
2831
+ padding: 3px 9px;
2832
+ min-height: 24px;
2833
+ display: inline-flex;
2834
+ align-items: center;
2835
+ gap: 4px;
2836
+ transition: all 0.15s ease;
2837
+ white-space: nowrap;
2838
+ }
2839
+ .markdy-code-panel__playground {
2840
+ background: var(--accent-light, rgba(16, 185, 129, 0.15));
2841
+ color: var(--accent, #10b981);
2842
+ border-color: var(--accent, #10b981);
2843
+ font-weight: 700;
2844
+ }
2845
+ .markdy-code-panel__playground:hover {
2846
+ background: var(--accent, #10b981);
2847
+ color: #ffffff;
2848
+ border-color: var(--accent, #10b981);
2849
+ transform: translateY(-1px);
2850
+ }
2851
+ .markdy-code-panel__copy:hover,
2852
+ .markdy-code-panel__close:hover {
2853
+ background: rgba(51, 65, 85, 0.95);
2854
+ color: #e2e8f0;
2855
+ border-color: #64748b;
2856
+ }
2857
+ .markdy-code-panel__copy:focus-visible,
2858
+ .markdy-code-panel__playground:focus-visible,
2859
+ .markdy-code-panel__close:focus-visible {
2860
+ outline: 2px solid #3b82f6;
2861
+ outline-offset: 1px;
2862
+ }
2863
+ .markdy-code-panel__body {
2864
+ overflow-y: auto;
2865
+ flex: 1 1 auto;
2866
+ padding: 16px 20px;
2867
+ scrollbar-width: thin;
2868
+ scrollbar-color: rgba(71, 85, 105, 0.5) transparent;
2869
+ }
2870
+ .markdy-code-panel__body::-webkit-scrollbar {
2871
+ width: 5px;
2872
+ }
2873
+ .markdy-code-panel__body::-webkit-scrollbar-track {
2874
+ background: transparent;
2875
+ }
2876
+ .markdy-code-panel__body::-webkit-scrollbar-thumb {
2877
+ background: rgba(71, 85, 105, 0.5);
2878
+ border-radius: 9999px;
2879
+ }
2880
+ .markdy-code-panel__pre {
2881
+ margin: 0;
2882
+ font: 400 12.5px/1.65 "JetBrains Mono", ui-monospace, "Cascadia Code", "Fira Code", monospace;
2883
+ color: #e2e8f0;
2884
+ white-space: pre;
2885
+ tab-size: 2;
2886
+ overflow-x: auto;
2887
+ }
2888
+ /* Syntax tinting \u2014 purely decorative, lightweight regex-free colorization */
2889
+ .markdy-code-panel__pre .t-keyword { color: #7dd3fc; }
2890
+ .markdy-code-panel__pre .t-string { color: #86efac; }
2891
+ .markdy-code-panel__pre .t-comment { color: #64748b; font-style: italic; }
2892
+ .markdy-code-panel__pre .t-edge { color: #f9a8d4; }
2893
+ .markdy-code-panel__pre .t-number { color: #fcd34d; }
2581
2894
  .markdy-control-rate {
2582
2895
  font-family: "JetBrains Mono", monospace, system-ui;
2583
2896
  font-size: 9px;
@@ -2849,7 +3162,9 @@ function createDiagram(opts) {
2849
3162
  resetView: resetViewButton,
2850
3163
  fullscreen: fullscreenButton,
2851
3164
  svg: svgButton,
2852
- share: shareButton
3165
+ share: shareButton,
3166
+ code: codeButton,
3167
+ theme: themeButton
2853
3168
  } = player.controls;
2854
3169
  const copyright = player.chrome.badge;
2855
3170
  const progressMode = player.chrome.progress;
@@ -2926,22 +3241,13 @@ function createDiagram(opts) {
2926
3241
  let badge = null;
2927
3242
  if (copyright) {
2928
3243
  badge = document.createElement("a");
3244
+ badge.className = "markdy-badge";
2929
3245
  badge.href = `${MARKDY_PLAYGROUND_URL}#code=${encodeCodeForPlaygroundHash(code)}`;
2930
3246
  badge.target = "_blank";
2931
3247
  badge.rel = "noopener noreferrer";
2932
- badge.textContent = "Powered by Markdy";
2933
- Object.assign(badge.style, {
2934
- display: "inline-flex",
2935
- alignItems: "center",
2936
- textAlign: "right",
2937
- fontSize: "10px",
2938
- fontFamily: "system-ui, sans-serif",
2939
- color: "#999",
2940
- textDecoration: "none",
2941
- padding: "0",
2942
- opacity: "0.7",
2943
- marginLeft: "auto"
2944
- });
3248
+ badge.title = "Open and edit in Markdy Playground";
3249
+ badge.style.marginLeft = "auto";
3250
+ badge.innerHTML = `<span style="opacity:0.9">\u26A1</span> Markdy`;
2945
3251
  ensureFooter().appendChild(badge);
2946
3252
  }
2947
3253
  ensureSceneStyles(document);
@@ -3169,8 +3475,24 @@ function createDiagram(opts) {
3169
3475
  let controlsPlayButton = null;
3170
3476
  let controlsRateButtons = [];
3171
3477
  let controlsSeekBar = null;
3478
+ let controlsTimeEl = null;
3172
3479
  let controlsFitButton = null;
3173
3480
  let fitViewActive = false;
3481
+ const ICONS2 = {
3482
+ play: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
3483
+ pause: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>',
3484
+ restart: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
3485
+ prevBeat: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg>',
3486
+ nextBeat: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>',
3487
+ fit: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>',
3488
+ resetView: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>',
3489
+ fullscreen: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>',
3490
+ svg: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
3491
+ share: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
3492
+ code: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
3493
+ theme: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3v18"/><path d="M12 3a9 9 0 0 1 0 18z" fill="currentColor"/></svg>',
3494
+ check: '<svg class="markdy-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'
3495
+ };
3174
3496
  function applyViewportTransform() {
3175
3497
  viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
3176
3498
  }
@@ -3257,7 +3579,7 @@ function createDiagram(opts) {
3257
3579
  function syncControls() {
3258
3580
  if (controlsPlayButton) {
3259
3581
  const playing = isPlaying;
3260
- controlsPlayButton.textContent = playing ? "Pause" : "Play";
3582
+ controlsPlayButton.innerHTML = `${playing ? ICONS2.pause : ICONS2.play}${playing ? "Pause" : "Play"}`;
3261
3583
  const playLabel = playing ? "Pause diagram" : "Play diagram";
3262
3584
  controlsPlayButton.setAttribute("aria-label", playLabel);
3263
3585
  controlsPlayButton.title = playLabel;
@@ -3267,7 +3589,16 @@ function createDiagram(opts) {
3267
3589
  const active = Math.abs(rate - playbackRate) < 1e-3;
3268
3590
  button.setAttribute("aria-pressed", active ? "true" : "false");
3269
3591
  }
3270
- if (controlsSeekBar) controlsSeekBar.value = String(sceneMs / 1e3);
3592
+ if (controlsSeekBar) {
3593
+ controlsSeekBar.value = String(sceneMs / 1e3);
3594
+ const pct = totalDurationMs > 0 ? sceneMs / totalDurationMs * 100 : 0;
3595
+ controlsSeekBar.style.setProperty("--seek-pct", `${pct}%`);
3596
+ }
3597
+ if (controlsTimeEl) {
3598
+ const cur = (sceneMs / 1e3).toFixed(1);
3599
+ const tot = durationSeconds.toFixed(1);
3600
+ controlsTimeEl.textContent = `${cur}s / ${tot}s`;
3601
+ }
3271
3602
  if (controlsFitButton) {
3272
3603
  controlsFitButton.setAttribute("aria-pressed", fitViewActive ? "true" : "false");
3273
3604
  }
@@ -3377,17 +3708,21 @@ function createDiagram(opts) {
3377
3708
  diagram.play();
3378
3709
  }
3379
3710
  }
3380
- function makeControlButton(label, ariaLabel) {
3711
+ function makeControlButton(label, ariaLabel, iconSvg) {
3381
3712
  const button = document.createElement("button");
3382
3713
  button.type = "button";
3383
- button.textContent = label;
3384
3714
  button.setAttribute("aria-label", ariaLabel);
3385
3715
  button.title = ariaLabel;
3716
+ if (iconSvg) {
3717
+ button.innerHTML = `${iconSvg}${label}`;
3718
+ } else {
3719
+ button.textContent = label;
3720
+ }
3386
3721
  return button;
3387
3722
  }
3388
3723
  function mountPlayControl(toolbar) {
3389
3724
  if (!playButton) return;
3390
- controlsPlayButton = makeControlButton("Play", "Play diagram");
3725
+ controlsPlayButton = makeControlButton("Play", "Play diagram", ICONS2.play);
3391
3726
  controlsPlayButton.className = "markdy-control-play";
3392
3727
  controlsPlayButton.addEventListener("click", togglePlayback);
3393
3728
  toolbar.appendChild(controlsPlayButton);
@@ -3396,14 +3731,15 @@ function createDiagram(opts) {
3396
3731
  const wanted = position === "prev" ? prevBeatButton : nextBeatButton;
3397
3732
  if (!wanted || plan.beats.length < 2) return;
3398
3733
  const label = position === "prev" ? "Prev" : "Next";
3399
- const button = makeControlButton(label, `${label === "Prev" ? "Previous" : "Next"} beat`);
3734
+ const icon = position === "prev" ? ICONS2.prevBeat : ICONS2.nextBeat;
3735
+ const button = makeControlButton(label, `${label === "Prev" ? "Previous" : "Next"} beat`, icon);
3400
3736
  button.className = `markdy-control-${position}-beat`;
3401
3737
  button.addEventListener("click", () => position === "prev" ? diagram.prevBeat() : diagram.nextBeat());
3402
3738
  toolbar.appendChild(button);
3403
3739
  }
3404
3740
  function mountRestartControl(toolbar) {
3405
3741
  if (!restartButton) return;
3406
- const button = makeControlButton("Restart", "Restart diagram");
3742
+ const button = makeControlButton("Restart", "Restart diagram", ICONS2.restart);
3407
3743
  button.className = "markdy-control-restart";
3408
3744
  button.addEventListener("click", () => {
3409
3745
  diagram.seek(0);
@@ -3423,6 +3759,10 @@ function createDiagram(opts) {
3423
3759
  controlsSeekBar.setAttribute("aria-label", "Seek diagram timeline");
3424
3760
  controlsSeekBar.addEventListener("input", () => diagram.seek(Number(controlsSeekBar?.value ?? 0)));
3425
3761
  toolbar.appendChild(controlsSeekBar);
3762
+ controlsTimeEl = document.createElement("span");
3763
+ controlsTimeEl.className = "markdy-control-time";
3764
+ controlsTimeEl.textContent = `${(sceneMs / 1e3).toFixed(1)}s / ${durationSeconds.toFixed(1)}s`;
3765
+ toolbar.appendChild(controlsTimeEl);
3426
3766
  }
3427
3767
  function mountSpeedControls(toolbar) {
3428
3768
  if (!speedControls) return;
@@ -3437,11 +3777,14 @@ function createDiagram(opts) {
3437
3777
  });
3438
3778
  }
3439
3779
  function flashControlLabel(button, message) {
3440
- const original = button.textContent ?? "";
3441
- button.textContent = message;
3780
+ const originalHtml = button.innerHTML;
3781
+ const isSuccess = !message.toLowerCase().includes("fail");
3782
+ button.innerHTML = isSuccess ? `${ICONS2.check}${message}` : message;
3783
+ button.classList.add("markdy-btn-flashed");
3442
3784
  setTimeout(() => {
3443
- button.textContent = original;
3444
- }, 1400);
3785
+ button.innerHTML = originalHtml;
3786
+ button.classList.remove("markdy-btn-flashed");
3787
+ }, 1500);
3445
3788
  }
3446
3789
  function downloadFile(filename, contents, type) {
3447
3790
  const blob = new Blob([contents], { type });
@@ -3457,7 +3800,7 @@ function createDiagram(opts) {
3457
3800
  }
3458
3801
  function mountSvgControl(toolbar) {
3459
3802
  if (!svgButton) return;
3460
- const button = makeControlButton("SVG", "Export diagram as SVG");
3803
+ const button = makeControlButton("SVG", "Export diagram as SVG", ICONS2.svg);
3461
3804
  button.className = "markdy-control-svg";
3462
3805
  button.addEventListener("click", async () => {
3463
3806
  const resumeAt = sceneMs;
@@ -3469,6 +3812,7 @@ function createDiagram(opts) {
3469
3812
  const svg = exportDiagramAsVectorSvg2(container);
3470
3813
  const name = (plan.title || "markdy-diagram").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3471
3814
  downloadFile(`${name || "markdy-diagram"}.svg`, svg, "image/svg+xml");
3815
+ flashControlLabel(button, "Saved");
3472
3816
  } catch (error) {
3473
3817
  onWarning({ severity: "warning", message: `SVG export failed: ${String(error)}`, line: 0 });
3474
3818
  flashControlLabel(button, "Failed");
@@ -3481,7 +3825,7 @@ function createDiagram(opts) {
3481
3825
  }
3482
3826
  function mountShareControl(toolbar) {
3483
3827
  if (!shareButton) return;
3484
- const button = makeControlButton("Share", "Copy a share link for this diagram");
3828
+ const button = makeControlButton("Share", "Copy a share link for this diagram", ICONS2.share);
3485
3829
  button.className = "markdy-control-share";
3486
3830
  button.addEventListener("click", async () => {
3487
3831
  try {
@@ -3496,9 +3840,132 @@ function createDiagram(opts) {
3496
3840
  });
3497
3841
  toolbar.appendChild(button);
3498
3842
  }
3843
+ function mountThemeControl(toolbar) {
3844
+ if (!themeButton) return;
3845
+ const button = makeControlButton("Theme", "Toggle dark/light theme palette", ICONS2.theme);
3846
+ button.className = "markdy-control-theme";
3847
+ button.addEventListener("click", () => {
3848
+ const darkThemes = ["midnight", "blueprint", "graphite", "nebula", "terminal"];
3849
+ const lightThemes = ["paper", "editorial", "sketchy"];
3850
+ const currentName = plan.theme.name || "paper";
3851
+ const isDark = darkThemes.includes(currentName);
3852
+ const targetThemes = isDark ? lightThemes : darkThemes;
3853
+ const nextTheme = targetThemes[Math.floor(Math.random() * targetThemes.length)];
3854
+ flashControlLabel(button, nextTheme);
3855
+ container.dispatchEvent(
3856
+ new CustomEvent("markdy-theme-switch", {
3857
+ bubbles: true,
3858
+ detail: { theme: nextTheme, isDark: !isDark }
3859
+ })
3860
+ );
3861
+ });
3862
+ toolbar.appendChild(button);
3863
+ }
3864
+ function tintCode(raw) {
3865
+ return raw.split("\n").map((line) => {
3866
+ if (/^\s*\/\//.test(line)) {
3867
+ return `<span class="t-comment">${escHtml(line)}</span>`;
3868
+ }
3869
+ line = line.replace(/(->|<-|~>|--|<~)/g, '<span class="t-edge">$1</span>');
3870
+ line = line.replace(/"([^"]*)"/g, '"<span class="t-string">$1</span>"');
3871
+ line = line.replace(
3872
+ /^(\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/,
3873
+ '$1<span class="t-keyword">$2</span>'
3874
+ );
3875
+ line = line.replace(/\b(\d[\d.]*(?:ms|px|s)?)\b/g, '<span class="t-number">$1</span>');
3876
+ return line;
3877
+ }).join("\n");
3878
+ }
3879
+ function escHtml(str) {
3880
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3881
+ }
3882
+ function mountCodeControl(toolbar) {
3883
+ if (!codeButton) return;
3884
+ const button = makeControlButton("Code", "View MarkdyScript source");
3885
+ button.className = "markdy-control-code";
3886
+ button.setAttribute("aria-haspopup", "dialog");
3887
+ button.addEventListener("click", () => {
3888
+ const overlay = document.createElement("div");
3889
+ overlay.className = "markdy-code-panel-overlay";
3890
+ overlay.setAttribute("role", "dialog");
3891
+ overlay.setAttribute("aria-modal", "true");
3892
+ overlay.setAttribute("aria-label", "MarkdyScript source code");
3893
+ const panel = document.createElement("div");
3894
+ panel.className = "markdy-code-panel";
3895
+ const header = document.createElement("div");
3896
+ header.className = "markdy-code-panel__header";
3897
+ const title = document.createElement("div");
3898
+ title.className = "markdy-code-panel__title";
3899
+ title.textContent = "MarkdyScript source";
3900
+ const actions = document.createElement("div");
3901
+ actions.className = "markdy-code-panel__actions";
3902
+ const copyBtn = document.createElement("button");
3903
+ copyBtn.type = "button";
3904
+ copyBtn.className = "markdy-code-panel__copy";
3905
+ copyBtn.textContent = "Copy";
3906
+ copyBtn.setAttribute("aria-label", "Copy source code to clipboard");
3907
+ copyBtn.addEventListener("click", async () => {
3908
+ try {
3909
+ await navigator.clipboard.writeText(code);
3910
+ copyBtn.textContent = "Copied \u2713";
3911
+ setTimeout(() => {
3912
+ copyBtn.textContent = "Copy";
3913
+ }, 1800);
3914
+ } catch {
3915
+ copyBtn.textContent = "Failed";
3916
+ setTimeout(() => {
3917
+ copyBtn.textContent = "Copy";
3918
+ }, 1800);
3919
+ }
3920
+ });
3921
+ const playgroundBtn = document.createElement("a");
3922
+ playgroundBtn.className = "markdy-code-panel__playground";
3923
+ playgroundBtn.href = `${shareUrl ?? MARKDY_PLAYGROUND_URL}#code=${encodeCodeForPlaygroundHash(code)}`;
3924
+ playgroundBtn.target = "_blank";
3925
+ playgroundBtn.rel = "noopener noreferrer";
3926
+ playgroundBtn.textContent = "Open in Studio \u2197";
3927
+ playgroundBtn.setAttribute("aria-label", "Open diagram in Markdy Studio playground");
3928
+ const closeBtn = document.createElement("button");
3929
+ closeBtn.type = "button";
3930
+ closeBtn.className = "markdy-code-panel__close";
3931
+ closeBtn.textContent = "\u2715 Close";
3932
+ closeBtn.setAttribute("aria-label", "Close code panel");
3933
+ actions.append(copyBtn, playgroundBtn, closeBtn);
3934
+ header.append(title, actions);
3935
+ const body = document.createElement("div");
3936
+ body.className = "markdy-code-panel__body";
3937
+ const pre = document.createElement("pre");
3938
+ pre.className = "markdy-code-panel__pre";
3939
+ pre.innerHTML = tintCode(code);
3940
+ body.appendChild(pre);
3941
+ panel.append(header, body);
3942
+ overlay.appendChild(panel);
3943
+ document.body.appendChild(overlay);
3944
+ closeBtn.focus();
3945
+ function closePanel() {
3946
+ overlay.dataset.closing = "1";
3947
+ overlay.addEventListener("animationend", () => overlay.remove(), { once: true });
3948
+ }
3949
+ closeBtn.addEventListener("click", closePanel);
3950
+ overlay.addEventListener("click", (e) => {
3951
+ if (e.target === overlay) closePanel();
3952
+ });
3953
+ function handleEscape(e) {
3954
+ if (e.key === "Escape") {
3955
+ closePanel();
3956
+ document.removeEventListener("keydown", handleEscape);
3957
+ }
3958
+ }
3959
+ document.addEventListener("keydown", handleEscape);
3960
+ overlay.addEventListener("animationend", () => {
3961
+ if (overlay.dataset.closing === "1") document.removeEventListener("keydown", handleEscape);
3962
+ }, { once: true });
3963
+ });
3964
+ toolbar.appendChild(button);
3965
+ }
3499
3966
  function mountFitControl(toolbar) {
3500
3967
  if (!fitViewButton) return;
3501
- controlsFitButton = makeControlButton("Fit", "Fit all items in view and ignore camera zoom");
3968
+ controlsFitButton = makeControlButton("Fit", "Fit all items in view and ignore camera zoom", ICONS2.fit);
3502
3969
  controlsFitButton.className = "markdy-control-fit";
3503
3970
  controlsFitButton.setAttribute("aria-pressed", "false");
3504
3971
  controlsFitButton.addEventListener("click", toggleFitView);
@@ -3506,14 +3973,14 @@ function createDiagram(opts) {
3506
3973
  }
3507
3974
  function mountResetViewControl(toolbar) {
3508
3975
  if (!resetViewButton) return;
3509
- const button = makeControlButton("Reset", "Reset diagram view");
3976
+ const button = makeControlButton("Reset", "Reset diagram view", ICONS2.resetView);
3510
3977
  button.className = "markdy-control-reset-view";
3511
3978
  button.addEventListener("click", resetViewportTransform);
3512
3979
  toolbar.appendChild(button);
3513
3980
  }
3514
3981
  function mountFullscreenControl(toolbar) {
3515
3982
  if (!fullscreenButton) return;
3516
- const button = makeControlButton("Full", "Toggle fullscreen view");
3983
+ const button = makeControlButton("Full", "Toggle fullscreen view", ICONS2.fullscreen);
3517
3984
  button.className = "markdy-control-fullscreen";
3518
3985
  button.setAttribute("aria-pressed", "false");
3519
3986
  const host = container.parentElement ?? container;
@@ -3594,17 +4061,49 @@ function createDiagram(opts) {
3594
4061
  for (const eventName of ["click", "dblclick", "pointerdown", "pointermove", "pointerup", "wheel"]) {
3595
4062
  toolbar.addEventListener(eventName, (event) => event.stopPropagation());
3596
4063
  }
3597
- mountPlayControl(toolbar);
3598
- mountBeatNavControls(toolbar, "prev");
3599
- mountBeatNavControls(toolbar, "next");
3600
- mountRestartControl(toolbar);
3601
- mountSeekControl(toolbar);
3602
- mountSpeedControls(toolbar);
3603
- mountFitControl(toolbar);
3604
- mountResetViewControl(toolbar);
3605
- mountFullscreenControl(toolbar);
3606
- mountSvgControl(toolbar);
3607
- mountShareControl(toolbar);
4064
+ const playbackGroup = document.createElement("div");
4065
+ playbackGroup.className = "markdy-controls-group markdy-controls-playback";
4066
+ mountPlayControl(playbackGroup);
4067
+ mountBeatNavControls(playbackGroup, "prev");
4068
+ mountBeatNavControls(playbackGroup, "next");
4069
+ mountRestartControl(playbackGroup);
4070
+ if (playbackGroup.children.length > 0) toolbar.appendChild(playbackGroup);
4071
+ if (seekBar) {
4072
+ const timelineGroup = document.createElement("div");
4073
+ timelineGroup.className = "markdy-controls-group markdy-controls-timeline";
4074
+ mountSeekControl(timelineGroup);
4075
+ toolbar.appendChild(timelineGroup);
4076
+ }
4077
+ const toolsGroup = document.createElement("div");
4078
+ toolsGroup.className = "markdy-controls-group markdy-controls-tools";
4079
+ if (speedControls) {
4080
+ const speedGroup = document.createElement("div");
4081
+ speedGroup.className = "markdy-speed-group";
4082
+ mountSpeedControls(speedGroup);
4083
+ toolsGroup.appendChild(speedGroup);
4084
+ }
4085
+ if (fitViewButton || resetViewButton || fullscreenButton) {
4086
+ if (toolsGroup.children.length > 0) {
4087
+ const divider = document.createElement("div");
4088
+ divider.className = "markdy-control-divider";
4089
+ toolsGroup.appendChild(divider);
4090
+ }
4091
+ mountFitControl(toolsGroup);
4092
+ mountResetViewControl(toolsGroup);
4093
+ mountFullscreenControl(toolsGroup);
4094
+ }
4095
+ if (svgButton || shareButton || codeButton || themeButton) {
4096
+ if (toolsGroup.children.length > 0) {
4097
+ const divider = document.createElement("div");
4098
+ divider.className = "markdy-control-divider";
4099
+ toolsGroup.appendChild(divider);
4100
+ }
4101
+ mountThemeControl(toolsGroup);
4102
+ mountSvgControl(toolsGroup);
4103
+ mountShareControl(toolsGroup);
4104
+ mountCodeControl(toolsGroup);
4105
+ }
4106
+ if (toolsGroup.children.length > 0) toolbar.appendChild(toolsGroup);
3608
4107
  ensureFooter().insertBefore(toolbar, badge ?? null);
3609
4108
  syncControls();
3610
4109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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.13"
48
+ "@markdy/core": "1.0.14"
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.13"
55
+ "@markdy/stdlib-systems": "1.0.14"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",