@pixldocs/canvas-renderer 0.5.103 → 0.5.105

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/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
6
  const jsxRuntime = require("react/jsx-runtime");
7
7
  const react = require("react");
8
8
  const reactDom = require("react-dom");
9
+ const sonner = require("sonner");
9
10
  const zustand = require("zustand");
10
11
  const fabric = require("fabric");
11
12
  const client = require("react-dom/client");
@@ -2506,9 +2507,17 @@ const useEditorStore = zustand.create((set, get) => ({
2506
2507
  }));
2507
2508
  let canvasRegistry = /* @__PURE__ */ new Map();
2508
2509
  function registerFabricCanvas(pageId, canvas) {
2509
- canvasRegistry.set(pageId, canvas);
2510
- }
2511
- function unregisterFabricCanvas(pageId) {
2510
+ const existing = canvasRegistry.get(pageId);
2511
+ const registryKey = existing && existing !== canvas ? `${pageId}#${Math.random().toString(36).slice(2, 10)}` : pageId;
2512
+ canvasRegistry.set(registryKey, canvas);
2513
+ return registryKey;
2514
+ }
2515
+ function unregisterFabricCanvas(pageId, canvas) {
2516
+ if (canvas) {
2517
+ const existing = canvasRegistry.get(pageId);
2518
+ if (existing === canvas) canvasRegistry.delete(pageId);
2519
+ return;
2520
+ }
2512
2521
  canvasRegistry.delete(pageId);
2513
2522
  }
2514
2523
  const LOCAL_FONTS = /* @__PURE__ */ new Set([
@@ -2580,6 +2589,8 @@ const LOCAL_FONTS = /* @__PURE__ */ new Set([
2580
2589
  ]);
2581
2590
  const loadedGoogleFonts = /* @__PURE__ */ new Set();
2582
2591
  const failedGoogleFonts = /* @__PURE__ */ new Set();
2592
+ const loadedFontshareFonts = /* @__PURE__ */ new Set();
2593
+ const failedFontshareFonts = /* @__PURE__ */ new Set();
2583
2594
  const loadingPromises$1 = /* @__PURE__ */ new Map();
2584
2595
  async function loadGoogleFont(fontFamily, weights) {
2585
2596
  if (LOCAL_FONTS.has(fontFamily)) return true;
@@ -2639,6 +2650,346 @@ async function _doLoadGoogleFont(fontFamily, weights) {
2639
2650
  console.warn(`[GoogleFonts] Failed to load: ${fontFamily}`);
2640
2651
  return false;
2641
2652
  }
2653
+ async function loadFontshareFont(fontFamily, slug, weights) {
2654
+ if (loadedFontshareFonts.has(fontFamily)) return true;
2655
+ if (failedFontshareFonts.has(fontFamily)) return false;
2656
+ const existing = loadingPromises$1.get(`fontshare:${fontFamily}`);
2657
+ if (existing) return existing;
2658
+ const promise = (async () => {
2659
+ try {
2660
+ const weightStr = (weights || [300, 400, 500, 700]).join(",");
2661
+ const url = `https://api.fontshare.com/v2/css?f[]=${slug}@${weightStr}&display=swap`;
2662
+ if (document.querySelector(`link[href="${url}"]`)) return true;
2663
+ const link = document.createElement("link");
2664
+ link.rel = "stylesheet";
2665
+ link.href = url;
2666
+ return await new Promise((resolve) => {
2667
+ link.onload = async () => {
2668
+ try {
2669
+ await document.fonts.load(`16px "${fontFamily}"`);
2670
+ await document.fonts.load(`bold 16px "${fontFamily}"`);
2671
+ } catch {
2672
+ }
2673
+ resolve(true);
2674
+ };
2675
+ link.onerror = () => {
2676
+ console.warn(`[Fontshare] Failed to load: ${fontFamily}`);
2677
+ resolve(false);
2678
+ };
2679
+ document.head.appendChild(link);
2680
+ });
2681
+ } catch (e) {
2682
+ console.warn(`[Fontshare] Error loading ${fontFamily}:`, e);
2683
+ return false;
2684
+ }
2685
+ })();
2686
+ loadingPromises$1.set(`fontshare:${fontFamily}`, promise);
2687
+ try {
2688
+ const result = await promise;
2689
+ if (result) loadedFontshareFonts.add(fontFamily);
2690
+ else failedFontshareFonts.add(fontFamily);
2691
+ return result;
2692
+ } finally {
2693
+ loadingPromises$1.delete(`fontshare:${fontFamily}`);
2694
+ }
2695
+ }
2696
+ async function loadFont(fontFamily) {
2697
+ if (LOCAL_FONTS.has(fontFamily)) return true;
2698
+ const entry = findFontEntry(fontFamily);
2699
+ if ((entry == null ? void 0 : entry.source) === "fontshare" && entry.fontshareSlug) {
2700
+ return loadFontshareFont(fontFamily, entry.fontshareSlug);
2701
+ }
2702
+ return loadGoogleFont(fontFamily);
2703
+ }
2704
+ const EXTENDED_FONT_LIST = [
2705
+ // ═══════════════════════════════════════════════════════════════════
2706
+ // PREMIUM (Fontshare) — modern, professional aesthetic
2707
+ // ═══════════════════════════════════════════════════════════════════
2708
+ { name: "Satoshi", category: "Premium", local: false, source: "fontshare", fontshareSlug: "satoshi", popular: true },
2709
+ { name: "Cabinet Grotesk", category: "Premium", local: false, source: "fontshare", fontshareSlug: "cabinet-grotesk", popular: true },
2710
+ { name: "Clash Display", category: "Premium", local: false, source: "fontshare", fontshareSlug: "clash-display", popular: true },
2711
+ { name: "Clash Grotesk", category: "Premium", local: false, source: "fontshare", fontshareSlug: "clash-grotesk", popular: true },
2712
+ { name: "General Sans", category: "Premium", local: false, source: "fontshare", fontshareSlug: "general-sans", popular: true },
2713
+ { name: "Switzer", category: "Premium", local: false, source: "fontshare", fontshareSlug: "switzer" },
2714
+ { name: "Supreme", category: "Premium", local: false, source: "fontshare", fontshareSlug: "supreme" },
2715
+ { name: "Author", category: "Premium", local: false, source: "fontshare", fontshareSlug: "author" },
2716
+ { name: "Boska", category: "Premium", local: false, source: "fontshare", fontshareSlug: "boska" },
2717
+ { name: "Excon", category: "Premium", local: false, source: "fontshare", fontshareSlug: "excon" },
2718
+ { name: "Khand", category: "Premium", local: false, source: "fontshare", fontshareSlug: "khand" },
2719
+ { name: "Sentient", category: "Premium", local: false, source: "fontshare", fontshareSlug: "sentient" },
2720
+ { name: "Synonym", category: "Premium", local: false, source: "fontshare", fontshareSlug: "synonym" },
2721
+ { name: "Erode", category: "Premium", local: false, source: "fontshare", fontshareSlug: "erode" },
2722
+ { name: "Ranade", category: "Premium", local: false, source: "fontshare", fontshareSlug: "ranade" },
2723
+ { name: "Tanker", category: "Premium", local: false, source: "fontshare", fontshareSlug: "tanker" },
2724
+ { name: "Zodiak", category: "Premium", local: false, source: "fontshare", fontshareSlug: "zodiak" },
2725
+ { name: "Gambarino", category: "Premium", local: false, source: "fontshare", fontshareSlug: "gambarino" },
2726
+ { name: "Melodrama", category: "Premium", local: false, source: "fontshare", fontshareSlug: "melodrama" },
2727
+ { name: "Bespoke Serif", category: "Premium", local: false, source: "fontshare", fontshareSlug: "bespoke-serif" },
2728
+ { name: "Bespoke Stencil", category: "Premium", local: false, source: "fontshare", fontshareSlug: "bespoke-stencil" },
2729
+ { name: "Panchang", category: "Premium", local: false, source: "fontshare", fontshareSlug: "panchang" },
2730
+ { name: "Pally", category: "Premium", local: false, source: "fontshare", fontshareSlug: "pally" },
2731
+ { name: "Tabular", category: "Premium", local: false, source: "fontshare", fontshareSlug: "tabular" },
2732
+ { name: "Sharpie", category: "Premium", local: false, source: "fontshare", fontshareSlug: "sharpie" },
2733
+ { name: "Stardom", category: "Premium", local: false, source: "fontshare", fontshareSlug: "stardom" },
2734
+ { name: "Telma", category: "Premium", local: false, source: "fontshare", fontshareSlug: "telma" },
2735
+ { name: "Nippo", category: "Premium", local: false, source: "fontshare", fontshareSlug: "nippo" },
2736
+ // ═══════════════════════════════════════════════════════════════════
2737
+ // SERIF — editorial, classical, elegant
2738
+ // ═══════════════════════════════════════════════════════════════════
2739
+ { name: "Playfair Display", category: "Serif", local: true, popular: true },
2740
+ { name: "Cormorant", category: "Serif", local: false, popular: true },
2741
+ { name: "Cormorant Garamond", category: "Serif", local: false },
2742
+ { name: "Cinzel", category: "Serif", local: false, popular: true },
2743
+ { name: "Cinzel Decorative", category: "Serif", local: false },
2744
+ { name: "Bodoni Moda", category: "Serif", local: false, popular: true },
2745
+ { name: "DM Serif Display", category: "Serif", local: true },
2746
+ { name: "DM Serif Text", category: "Serif", local: false },
2747
+ { name: "Italiana", category: "Serif", local: false },
2748
+ { name: "Marcellus", category: "Serif", local: false },
2749
+ { name: "Marcellus SC", category: "Serif", local: false },
2750
+ { name: "Yeseva One", category: "Serif", local: false },
2751
+ { name: "Prata", category: "Serif", local: false },
2752
+ { name: "Tenor Sans", category: "Serif", local: false },
2753
+ { name: "Fraunces", category: "Serif", local: false, popular: true },
2754
+ { name: "Newsreader", category: "Serif", local: false },
2755
+ { name: "Source Serif Pro", category: "Serif", local: false },
2756
+ { name: "Merriweather", category: "Serif", local: true },
2757
+ { name: "Lora", category: "Serif", local: true },
2758
+ { name: "EB Garamond", category: "Serif", local: true },
2759
+ { name: "Libre Baskerville", category: "Serif", local: true },
2760
+ { name: "Libre Caslon Text", category: "Serif", local: false },
2761
+ { name: "Libre Caslon Display", category: "Serif", local: false },
2762
+ { name: "Crimson Text", category: "Serif", local: true },
2763
+ { name: "Crimson Pro", category: "Serif", local: false },
2764
+ { name: "Noto Serif", category: "Serif", local: false },
2765
+ { name: "Noto Serif Display", category: "Serif", local: false },
2766
+ { name: "PT Serif", category: "Serif", local: false },
2767
+ { name: "Bitter", category: "Serif", local: false },
2768
+ { name: "Spectral", category: "Serif", local: false },
2769
+ { name: "Cardo", category: "Serif", local: false },
2770
+ { name: "Old Standard TT", category: "Serif", local: false },
2771
+ { name: "Vollkorn", category: "Serif", local: false },
2772
+ { name: "Cantata One", category: "Serif", local: false },
2773
+ { name: "Domine", category: "Serif", local: false },
2774
+ { name: "Gentium Plus", category: "Serif", local: false },
2775
+ { name: "Tinos", category: "Serif", local: false },
2776
+ { name: "Trirong", category: "Serif", local: false },
2777
+ { name: "Sorts Mill Goudy", category: "Serif", local: false },
2778
+ { name: "IM Fell English", category: "Serif", local: false },
2779
+ { name: "IM Fell DW Pica", category: "Serif", local: false },
2780
+ { name: "Petrona", category: "Serif", local: false },
2781
+ { name: "Rozha One", category: "Serif", local: false },
2782
+ { name: "Tiro Devanagari Hindi", category: "Serif", local: false },
2783
+ // ═══════════════════════════════════════════════════════════════════
2784
+ // SANS-SERIF — clean, modern, workhorse
2785
+ // ═══════════════════════════════════════════════════════════════════
2786
+ { name: "Inter", category: "Sans-Serif", local: true, popular: true },
2787
+ { name: "Montserrat", category: "Sans-Serif", local: true, popular: true },
2788
+ { name: "Poppins", category: "Sans-Serif", local: true, popular: true },
2789
+ { name: "Open Sans", category: "Sans-Serif", local: true },
2790
+ { name: "Roboto", category: "Sans-Serif", local: true },
2791
+ { name: "Lato", category: "Sans-Serif", local: true },
2792
+ { name: "Raleway", category: "Sans-Serif", local: true },
2793
+ { name: "Nunito", category: "Sans-Serif", local: true },
2794
+ { name: "Source Sans Pro", category: "Sans-Serif", local: true },
2795
+ { name: "Work Sans", category: "Sans-Serif", local: true },
2796
+ { name: "DM Sans", category: "Sans-Serif", local: true, popular: true },
2797
+ { name: "Outfit", category: "Sans-Serif", local: true, popular: true },
2798
+ { name: "Figtree", category: "Sans-Serif", local: true },
2799
+ { name: "Manrope", category: "Sans-Serif", local: true, popular: true },
2800
+ { name: "Space Grotesk", category: "Sans-Serif", local: true, popular: true },
2801
+ { name: "Mulish", category: "Sans-Serif", local: true },
2802
+ { name: "Quicksand", category: "Sans-Serif", local: true },
2803
+ { name: "Rubik", category: "Sans-Serif", local: true },
2804
+ { name: "Karla", category: "Sans-Serif", local: true },
2805
+ { name: "Plus Jakarta Sans", category: "Sans-Serif", local: true },
2806
+ { name: "Libre Franklin", category: "Sans-Serif", local: true },
2807
+ { name: "Sora", category: "Sans-Serif", local: true },
2808
+ { name: "Urbanist", category: "Sans-Serif", local: true },
2809
+ { name: "Lexend", category: "Sans-Serif", local: true },
2810
+ { name: "Albert Sans", category: "Sans-Serif", local: true },
2811
+ { name: "Noto Sans", category: "Sans-Serif", local: true },
2812
+ { name: "Cabin", category: "Sans-Serif", local: true },
2813
+ { name: "Barlow", category: "Sans-Serif", local: true },
2814
+ { name: "Barlow Condensed", category: "Sans-Serif", local: false },
2815
+ { name: "Josefin Sans", category: "Sans-Serif", local: true },
2816
+ { name: "Archivo", category: "Sans-Serif", local: true },
2817
+ { name: "Archivo Narrow", category: "Sans-Serif", local: false },
2818
+ { name: "Overpass", category: "Sans-Serif", local: true },
2819
+ { name: "Exo 2", category: "Sans-Serif", local: true },
2820
+ { name: "Onest", category: "Sans-Serif", local: false, popular: true },
2821
+ { name: "Be Vietnam Pro", category: "Sans-Serif", local: false },
2822
+ { name: "Public Sans", category: "Sans-Serif", local: false },
2823
+ { name: "Red Hat Display", category: "Sans-Serif", local: false },
2824
+ { name: "Red Hat Text", category: "Sans-Serif", local: false },
2825
+ { name: "Sen", category: "Sans-Serif", local: false },
2826
+ { name: "Hanken Grotesk", category: "Sans-Serif", local: false },
2827
+ { name: "Schibsted Grotesk", category: "Sans-Serif", local: false },
2828
+ { name: "Reddit Sans", category: "Sans-Serif", local: false },
2829
+ { name: "Instrument Sans", category: "Sans-Serif", local: false },
2830
+ { name: "Geist", category: "Sans-Serif", local: false, popular: true },
2831
+ { name: "Nunito Sans", category: "Sans-Serif", local: false },
2832
+ { name: "PT Sans", category: "Sans-Serif", local: false },
2833
+ { name: "PT Sans Narrow", category: "Sans-Serif", local: false },
2834
+ { name: "Mukta", category: "Sans-Serif", local: false },
2835
+ { name: "Anek Devanagari", category: "Sans-Serif", local: false },
2836
+ { name: "Hind", category: "Sans-Serif", local: true },
2837
+ { name: "Hind Vadodara", category: "Sans-Serif", local: false },
2838
+ // ═══════════════════════════════════════════════════════════════════
2839
+ // DISPLAY — bold, attention-grabbing headlines
2840
+ // ═══════════════════════════════════════════════════════════════════
2841
+ { name: "Bebas Neue", category: "Display", local: true, popular: true },
2842
+ { name: "Anton", category: "Display", local: true, popular: true },
2843
+ { name: "Oswald", category: "Display", local: true },
2844
+ { name: "Abril Fatface", category: "Display", local: true, popular: true },
2845
+ { name: "League Spartan", category: "Display", local: true },
2846
+ { name: "Teko", category: "Display", local: true },
2847
+ { name: "Righteous", category: "Display", local: false },
2848
+ { name: "Alfa Slab One", category: "Display", local: false, popular: true },
2849
+ { name: "Archivo Black", category: "Display", local: false },
2850
+ { name: "Fredoka", category: "Display", local: false },
2851
+ { name: "Passion One", category: "Display", local: false },
2852
+ { name: "Bowlby One", category: "Display", local: false },
2853
+ { name: "Bowlby One SC", category: "Display", local: false },
2854
+ { name: "Secular One", category: "Display", local: false },
2855
+ { name: "Lilita One", category: "Display", local: false },
2856
+ { name: "Titan One", category: "Display", local: false },
2857
+ { name: "Russo One", category: "Display", local: false },
2858
+ { name: "Staatliches", category: "Display", local: false, popular: true },
2859
+ { name: "Dela Gothic One", category: "Display", local: false },
2860
+ { name: "Ultra", category: "Display", local: false },
2861
+ { name: "Sigmar One", category: "Display", local: false },
2862
+ { name: "Sigmar", category: "Display", local: false },
2863
+ { name: "Modak", category: "Display", local: false },
2864
+ { name: "Bagel Fat One", category: "Display", local: false },
2865
+ { name: "Climate Crisis", category: "Display", local: false },
2866
+ { name: "Yatra One", category: "Display", local: false },
2867
+ { name: "Bungee", category: "Display", local: false },
2868
+ { name: "Bungee Shade", category: "Display", local: false },
2869
+ { name: "Bungee Outline", category: "Display", local: false },
2870
+ { name: "Bungee Inline", category: "Display", local: false },
2871
+ { name: "Monoton", category: "Display", local: false, popular: true },
2872
+ { name: "Black Ops One", category: "Display", local: false },
2873
+ { name: "Faster One", category: "Display", local: false },
2874
+ { name: "Rubik Glitch", category: "Display", local: false },
2875
+ { name: "Rubik Mono One", category: "Display", local: false },
2876
+ { name: "Rubik Wet Paint", category: "Display", local: false },
2877
+ { name: "Rubik Bubbles", category: "Display", local: false },
2878
+ { name: "Rubik Beastly", category: "Display", local: false },
2879
+ { name: "Rubik Burned", category: "Display", local: false },
2880
+ { name: "Rubik Distressed", category: "Display", local: false },
2881
+ { name: "Rubik Iso", category: "Display", local: false },
2882
+ { name: "Rubik Marker Hatch", category: "Display", local: false },
2883
+ { name: "Rubik Maze", category: "Display", local: false },
2884
+ { name: "Rubik Pixels", category: "Display", local: false },
2885
+ { name: "Rubik Puddles", category: "Display", local: false },
2886
+ { name: "Rubik Spray Paint", category: "Display", local: false },
2887
+ { name: "Rubik Vinyl", category: "Display", local: false },
2888
+ { name: "Saira Stencil One", category: "Display", local: false },
2889
+ { name: "Audiowide", category: "Display", local: false },
2890
+ { name: "Orbitron", category: "Display", local: false },
2891
+ { name: "Plaster", category: "Display", local: false },
2892
+ // ═══════════════════════════════════════════════════════════════════
2893
+ // HANDWRITING / SCRIPT — fluid, personal, calligraphic
2894
+ // ═══════════════════════════════════════════════════════════════════
2895
+ { name: "Dancing Script", category: "Handwriting", local: true, popular: true },
2896
+ { name: "Pacifico", category: "Handwriting", local: true, popular: true },
2897
+ { name: "Great Vibes", category: "Handwriting", local: true, popular: true },
2898
+ { name: "Sacramento", category: "Handwriting", local: true },
2899
+ { name: "Alex Brush", category: "Handwriting", local: true },
2900
+ { name: "Allura", category: "Handwriting", local: true },
2901
+ { name: "Caveat", category: "Handwriting", local: true },
2902
+ { name: "Caveat Brush", category: "Handwriting", local: false },
2903
+ { name: "Lobster", category: "Handwriting", local: true },
2904
+ { name: "Lobster Two", category: "Handwriting", local: false },
2905
+ { name: "Comfortaa", category: "Handwriting", local: true },
2906
+ { name: "Tangerine", category: "Handwriting", local: false, popular: true },
2907
+ { name: "Yellowtail", category: "Handwriting", local: false, popular: true },
2908
+ { name: "Kaushan Script", category: "Handwriting", local: false, popular: true },
2909
+ { name: "Parisienne", category: "Handwriting", local: false },
2910
+ { name: "Petit Formal Script", category: "Handwriting", local: false },
2911
+ { name: "Pinyon Script", category: "Handwriting", local: false },
2912
+ { name: "Mrs Saint Delafield", category: "Handwriting", local: false },
2913
+ { name: "Marck Script", category: "Handwriting", local: false },
2914
+ { name: "Niconne", category: "Handwriting", local: false },
2915
+ { name: "Homemade Apple", category: "Handwriting", local: false },
2916
+ { name: "Permanent Marker", category: "Handwriting", local: false },
2917
+ { name: "Reenie Beanie", category: "Handwriting", local: false },
2918
+ { name: "Satisfy", category: "Handwriting", local: false },
2919
+ { name: "Kalam", category: "Handwriting", local: false },
2920
+ { name: "Indie Flower", category: "Handwriting", local: false },
2921
+ { name: "Courgette", category: "Handwriting", local: false },
2922
+ { name: "Cookie", category: "Handwriting", local: false },
2923
+ { name: "Shadows Into Light", category: "Handwriting", local: false },
2924
+ { name: "Patrick Hand", category: "Handwriting", local: false },
2925
+ { name: "Amatic SC", category: "Handwriting", local: false },
2926
+ { name: "Architects Daughter", category: "Handwriting", local: false },
2927
+ { name: "Gloria Hallelujah", category: "Handwriting", local: false },
2928
+ { name: "La Belle Aurore", category: "Handwriting", local: false },
2929
+ { name: "Mr Dafoe", category: "Handwriting", local: false },
2930
+ { name: "Italianno", category: "Handwriting", local: false },
2931
+ { name: "Rouge Script", category: "Handwriting", local: false },
2932
+ { name: "Grand Hotel", category: "Handwriting", local: false },
2933
+ { name: "Bilbo Swash Caps", category: "Handwriting", local: false },
2934
+ // ═══════════════════════════════════════════════════════════════════
2935
+ // DECORATIVE / FUN — quirky, themed, special-occasion
2936
+ // ═══════════════════════════════════════════════════════════════════
2937
+ { name: "Frijole", category: "Decorative", local: false },
2938
+ { name: "Creepster", category: "Decorative", local: false },
2939
+ { name: "Nosifer", category: "Decorative", local: false },
2940
+ { name: "Ewert", category: "Decorative", local: false },
2941
+ { name: "Lakki Reddy", category: "Decorative", local: false },
2942
+ { name: "Henny Penny", category: "Decorative", local: false },
2943
+ { name: "Special Elite", category: "Decorative", local: false, popular: true },
2944
+ { name: "Vast Shadow", category: "Decorative", local: false },
2945
+ { name: "Almendra Display", category: "Decorative", local: false },
2946
+ { name: "Eater", category: "Decorative", local: false },
2947
+ { name: "Butcherman", category: "Decorative", local: false },
2948
+ { name: "Pirata One", category: "Decorative", local: false },
2949
+ { name: "Metamorphous", category: "Decorative", local: false },
2950
+ { name: "MedievalSharp", category: "Decorative", local: false },
2951
+ { name: "Fascinate", category: "Decorative", local: false },
2952
+ { name: "Fascinate Inline", category: "Decorative", local: false },
2953
+ { name: "Sancreek", category: "Decorative", local: false },
2954
+ { name: "Smokum", category: "Decorative", local: false },
2955
+ { name: "Vampiro One", category: "Decorative", local: false },
2956
+ { name: "Mountains of Christmas", category: "Decorative", local: false },
2957
+ { name: "Caesar Dressing", category: "Decorative", local: false },
2958
+ { name: "Megrim", category: "Decorative", local: false },
2959
+ // ═══════════════════════════════════════════════════════════════════
2960
+ // BLACKLETTER — gothic, medieval, formal
2961
+ // ═══════════════════════════════════════════════════════════════════
2962
+ { name: "UnifrakturCook", category: "Blackletter", local: false },
2963
+ { name: "UnifrakturMaguntia", category: "Blackletter", local: false },
2964
+ // ═══════════════════════════════════════════════════════════════════
2965
+ // MONOSPACE — code, technical
2966
+ // ═══════════════════════════════════════════════════════════════════
2967
+ { name: "Roboto Mono", category: "Monospace", local: true },
2968
+ { name: "Fira Code", category: "Monospace", local: true },
2969
+ { name: "JetBrains Mono", category: "Monospace", local: true },
2970
+ { name: "Source Code Pro", category: "Monospace", local: true },
2971
+ { name: "IBM Plex Mono", category: "Monospace", local: true },
2972
+ { name: "Space Mono", category: "Monospace", local: true },
2973
+ { name: "Geist Mono", category: "Monospace", local: false },
2974
+ { name: "DM Mono", category: "Monospace", local: false },
2975
+ { name: "Inconsolata", category: "Monospace", local: false },
2976
+ { name: "Cousine", category: "Monospace", local: false },
2977
+ { name: "Anonymous Pro", category: "Monospace", local: false },
2978
+ { name: "Cutive Mono", category: "Monospace", local: false },
2979
+ { name: "Major Mono Display", category: "Monospace", local: false },
2980
+ { name: "VT323", category: "Monospace", local: false },
2981
+ { name: "Share Tech Mono", category: "Monospace", local: false }
2982
+ ];
2983
+ const _fontLookupCache = /* @__PURE__ */ new Map();
2984
+ function findFontEntry(name) {
2985
+ const key = name.toLowerCase().replace(/[\s\-_]/g, "");
2986
+ if (_fontLookupCache.has(key)) return _fontLookupCache.get(key);
2987
+ const entry = EXTENDED_FONT_LIST.find(
2988
+ (f) => f.name.toLowerCase().replace(/[\s\-_]/g, "") === key
2989
+ );
2990
+ if (entry) _fontLookupCache.set(key, entry);
2991
+ return entry;
2992
+ }
2642
2993
  const getObjectId = (obj) => obj.__docuforgeId;
2643
2994
  const setObjectData = (obj, id) => {
2644
2995
  obj.__docuforgeId = id;
@@ -2860,9 +3211,27 @@ const ensureFontLoaded = async (fontFamily) => {
2860
3211
  return;
2861
3212
  }
2862
3213
  try {
2863
- await loadGoogleFont(fontFamily);
3214
+ await loadFont(fontFamily);
2864
3215
  } catch (e) {
2865
- console.warn(`Failed to load Google Font: ${fontFamily}`, e);
3216
+ console.warn(`Failed to load font: ${fontFamily}`, e);
3217
+ try {
3218
+ await loadGoogleFont(fontFamily);
3219
+ } catch {
3220
+ }
3221
+ }
3222
+ try {
3223
+ if (document.fonts) {
3224
+ await Promise.race([
3225
+ Promise.all([
3226
+ document.fonts.load(`16px "${fontFamily}"`),
3227
+ document.fonts.load(`bold 16px "${fontFamily}"`),
3228
+ document.fonts.load(`italic 16px "${fontFamily}"`),
3229
+ document.fonts.load(`bold italic 16px "${fontFamily}"`)
3230
+ ]),
3231
+ new Promise((r) => setTimeout(r, 2500))
3232
+ ]);
3233
+ }
3234
+ } catch {
2866
3235
  }
2867
3236
  };
2868
3237
  const setupFontLoadingListener = (canvas, afterRerender) => {
@@ -5363,6 +5732,172 @@ function buildRoundedTrianglePath(w, h, rTop, rBR, rBL) {
5363
5732
  ];
5364
5733
  return parts.join(" ");
5365
5734
  }
5735
+ let activeThemeColors = {};
5736
+ function setMarkdownThemeColors(c) {
5737
+ activeThemeColors = { ...c };
5738
+ }
5739
+ function resolveColorToken(token, theme) {
5740
+ const raw = token.trim();
5741
+ const t = raw.toLowerCase();
5742
+ if (t === "primary") return theme.primary;
5743
+ if (t === "secondary") return theme.secondary;
5744
+ if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
5745
+ if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) return raw;
5746
+ if (/^[a-z]+$/i.test(raw)) return raw;
5747
+ return void 0;
5748
+ }
5749
+ function mergeStyle(a, b) {
5750
+ return { ...a, ...b };
5751
+ }
5752
+ function tokenize(input, theme) {
5753
+ const runs = [];
5754
+ const stack = [];
5755
+ let buf = "";
5756
+ const activeStyle = () => {
5757
+ let s = {};
5758
+ for (const e of stack) s = mergeStyle(s, e.style);
5759
+ return s;
5760
+ };
5761
+ const flush = () => {
5762
+ if (buf.length === 0) return;
5763
+ runs.push({ text: buf, style: activeStyle() });
5764
+ buf = "";
5765
+ };
5766
+ let i = 0;
5767
+ const n = input.length;
5768
+ const peek = (s, at = i) => input.startsWith(s, at);
5769
+ const findUnescaped = (needle, from) => {
5770
+ let p = from;
5771
+ while (p < n) {
5772
+ if (input[p] === "\\" && p + 1 < n) {
5773
+ p += 2;
5774
+ continue;
5775
+ }
5776
+ if (input.startsWith(needle, p)) return p;
5777
+ p++;
5778
+ }
5779
+ return -1;
5780
+ };
5781
+ const tryOpenBracket = () => {
5782
+ if (input[i] !== "[") return -1;
5783
+ const m = /^\[(c|bg)=([^\]]+)\]/.exec(input.slice(i));
5784
+ if (!m) return -1;
5785
+ const kind = m[1];
5786
+ const tokenRaw = m[2];
5787
+ const closer = kind === "c" ? "[/c]" : "[/bg]";
5788
+ if (findUnescaped(closer, i + m[0].length) === -1) return -1;
5789
+ const color = resolveColorToken(tokenRaw, theme);
5790
+ const style = {};
5791
+ if (color) {
5792
+ if (kind === "c") style.fill = color;
5793
+ else style.textBackgroundColor = color;
5794
+ }
5795
+ flush();
5796
+ stack.push({ kind, style, closer });
5797
+ return i + m[0].length;
5798
+ };
5799
+ const tryCloseBracket = () => {
5800
+ for (let s = stack.length - 1; s >= 0; s--) {
5801
+ const top = stack[s];
5802
+ if (top.kind !== "c" && top.kind !== "bg") continue;
5803
+ if (peek(top.closer)) {
5804
+ if (s !== stack.length - 1) stack.length = s + 1;
5805
+ flush();
5806
+ stack.pop();
5807
+ return i + top.closer.length;
5808
+ }
5809
+ break;
5810
+ }
5811
+ return -1;
5812
+ };
5813
+ const toggle = (delim, kind, style) => {
5814
+ if (!peek(delim)) return null;
5815
+ const topIdx = stack.findIndex((e) => e.kind === kind);
5816
+ if (topIdx >= 0) {
5817
+ if (topIdx !== stack.length - 1) return null;
5818
+ flush();
5819
+ stack.length = topIdx;
5820
+ return i + delim.length;
5821
+ }
5822
+ if (findUnescaped(delim, i + delim.length) === -1) return null;
5823
+ flush();
5824
+ stack.push({ kind, style, closer: delim });
5825
+ return i + delim.length;
5826
+ };
5827
+ while (i < n) {
5828
+ const ch = input[i];
5829
+ if (ch === "\\" && i + 1 < n) {
5830
+ buf += input[i + 1];
5831
+ i += 2;
5832
+ continue;
5833
+ }
5834
+ if (ch === "[") {
5835
+ const closed = tryCloseBracket();
5836
+ if (closed > 0) {
5837
+ i = closed;
5838
+ continue;
5839
+ }
5840
+ const opened = tryOpenBracket();
5841
+ if (opened > 0) {
5842
+ i = opened;
5843
+ continue;
5844
+ }
5845
+ }
5846
+ let next;
5847
+ if ((next = toggle("**", "bold", { fontWeight: 700 })) !== null) {
5848
+ i = next;
5849
+ continue;
5850
+ }
5851
+ if ((next = toggle("__", "under", { underline: true })) !== null) {
5852
+ i = next;
5853
+ continue;
5854
+ }
5855
+ if ((next = toggle("~~", "strike", { linethrough: true })) !== null) {
5856
+ i = next;
5857
+ continue;
5858
+ }
5859
+ if ((next = toggle("==", "highlight", { textBackgroundColor: theme.secondary || "#ffe066" })) !== null) {
5860
+ i = next;
5861
+ continue;
5862
+ }
5863
+ if ((next = toggle("*", "italic", { fontStyle: "italic" })) !== null) {
5864
+ i = next;
5865
+ continue;
5866
+ }
5867
+ buf += ch;
5868
+ i++;
5869
+ }
5870
+ flush();
5871
+ return runs;
5872
+ }
5873
+ function parseTextMarkdown(input, themeColors) {
5874
+ const theme = activeThemeColors;
5875
+ const runs = tokenize(input ?? "", theme);
5876
+ let plain = "";
5877
+ const styles = {};
5878
+ let lineIdx = 0;
5879
+ let charIdx = 0;
5880
+ let hasFormatting = false;
5881
+ for (const run of runs) {
5882
+ const styleHasContent = Object.keys(run.style).length > 0;
5883
+ if (styleHasContent) hasFormatting = true;
5884
+ for (const ch of run.text) {
5885
+ if (ch === "\n") {
5886
+ plain += "\n";
5887
+ lineIdx++;
5888
+ charIdx = 0;
5889
+ continue;
5890
+ }
5891
+ plain += ch;
5892
+ if (styleHasContent) {
5893
+ if (!styles[lineIdx]) styles[lineIdx] = {};
5894
+ styles[lineIdx][charIdx] = { ...run.style };
5895
+ }
5896
+ charIdx++;
5897
+ }
5898
+ }
5899
+ return { plainText: plain, styles, hasFormatting };
5900
+ }
5366
5901
  const roundDiag = (value) => {
5367
5902
  if (typeof value !== "number") return value;
5368
5903
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -5501,6 +6036,13 @@ function createText(element) {
5501
6036
  let fontSize = element.fontSize || 16;
5502
6037
  const minFontSize = element.minFontSize || 8;
5503
6038
  const maxLines = element.maxLines || 3;
6039
+ const formattingEnabled = element.formattingEnabled === true;
6040
+ let parsedStyles = {};
6041
+ if (formattingEnabled) {
6042
+ const parsed = parseTextMarkdown(text);
6043
+ text = parsed.plainText || " ";
6044
+ parsedStyles = parsed.styles;
6045
+ }
5504
6046
  const baseWidth = element.width && element.width > 0 ? element.width : 200;
5505
6047
  const baseHeight = element.height;
5506
6048
  const fixedWidth = Math.max(baseWidth, 1);
@@ -5521,7 +6063,8 @@ function createText(element) {
5521
6063
  fontStyle: element.fontStyle || "normal",
5522
6064
  lineHeight: element.lineHeight || 1.2,
5523
6065
  charSpacing: element.charSpacing || 0,
5524
- splitByGrapheme: false
6066
+ splitByGrapheme: false,
6067
+ ...formattingEnabled ? { styles: parsedStyles } : {}
5525
6068
  });
5526
6069
  testTextbox.initDimensions();
5527
6070
  const textHeight = testTextbox.height || 0;
@@ -5633,8 +6176,16 @@ function createText(element) {
5633
6176
  objectCaching: false,
5634
6177
  noScaleCache: true,
5635
6178
  splitByGrapheme,
5636
- ...element.styles ? { styles: element.styles } : {}
6179
+ // When inline markdown formatting is enabled, the displayed text is the
6180
+ // PARSED plain text (markdown source lives separately on the element).
6181
+ // Allowing canvas inline editing would let the user edit that plain text
6182
+ // and on commit we'd save it back as the new markdown source — wiping all
6183
+ // formatting tokens (**, __, [c=...], etc). Disable inline edit and steer
6184
+ // users to the right-panel text field which exposes the raw markdown.
6185
+ editable: !formattingEnabled,
6186
+ ...formattingEnabled ? { styles: parsedStyles } : element.styles ? { styles: element.styles } : {}
5637
6187
  });
6188
+ textbox.__formattingEnabled = formattingEnabled;
5638
6189
  textbox.initDimensions();
5639
6190
  textbox.set({
5640
6191
  width: targetWidth,
@@ -5655,6 +6206,15 @@ function createText(element) {
5655
6206
  textbox.setCoords();
5656
6207
  }
5657
6208
  textbox.dirty = true;
6209
+ if (formattingEnabled) {
6210
+ try {
6211
+ textbox._forceClearCache = true;
6212
+ if (typeof textbox._clearCache === "function") {
6213
+ textbox._clearCache();
6214
+ }
6215
+ } catch {
6216
+ }
6217
+ }
5658
6218
  if (overflowPolicy === "auto-shrink" && typeof window !== "undefined" && window.__pixldocsDebugAutoShrink === true) {
5659
6219
  console.log("[auto-shrink][final-textbox] " + stringifyDiag({
5660
6220
  id: element.id,
@@ -6269,7 +6829,6 @@ function bakeEdgeFade(source, fade) {
6269
6829
  let x = 0, y = 0, rectW = mask.width, rectH = mask.height;
6270
6830
  const STOPS = 64;
6271
6831
  const gamma = 1 / hardness;
6272
- const seamGuardPx = 2;
6273
6832
  const innerAlpha = (t) => {
6274
6833
  const keepProgress = Math.pow(t, gamma);
6275
6834
  const erase = (1 - amount) * (1 - keepProgress);
@@ -6307,16 +6866,6 @@ function bakeEdgeFade(source, fade) {
6307
6866
  }
6308
6867
  mctx.fillStyle = g;
6309
6868
  mctx.fillRect(x, y, rectW, rectH);
6310
- {
6311
- const edgePx = Math.min(seamGuardPx, side === "top" || side === "bottom" ? rectH : rectW);
6312
- mctx.fillStyle = "rgba(0,0,0,0)";
6313
- mctx.globalCompositeOperation = "copy";
6314
- if (side === "top") mctx.fillRect(0, 0, mask.width, edgePx);
6315
- if (side === "bottom") mctx.fillRect(0, mask.height - edgePx, mask.width, edgePx);
6316
- if (side === "left") mctx.fillRect(0, 0, edgePx, mask.height);
6317
- if (side === "right") mctx.fillRect(mask.width - edgePx, 0, edgePx, mask.height);
6318
- mctx.globalCompositeOperation = "source-over";
6319
- }
6320
6869
  ctx.globalCompositeOperation = "destination-in";
6321
6870
  ctx.drawImage(mask, 0, 0);
6322
6871
  ctx.globalCompositeOperation = "source-over";
@@ -6402,6 +6951,10 @@ const PageCanvas = react.forwardRef(
6402
6951
  skipFontReadyWait = false,
6403
6952
  onReady
6404
6953
  }, ref) => {
6954
+ setMarkdownThemeColors({
6955
+ primary: projectSettings.primaryColor || "#7c3aed",
6956
+ secondary: projectSettings.secondaryColor || "#ffe066"
6957
+ });
6405
6958
  const isEditorMode = mode === "editor";
6406
6959
  const isPreviewMode = mode === "preview";
6407
6960
  const allowEditing = isEditorMode;
@@ -6497,8 +7050,8 @@ const PageCanvas = react.forwardRef(
6497
7050
  }, [selectedIds]);
6498
7051
  react.useEffect(() => {
6499
7052
  isActiveRef.current = isActive;
6500
- if (isActive && fabricRef.current) ;
6501
- }, [isActive, pageId]);
7053
+ if (isEditorMode && isActive && fabricRef.current) ;
7054
+ }, [isActive, isEditorMode, pageId]);
6502
7055
  const getObjId = react.useCallback((obj) => {
6503
7056
  return obj.__docuforgeId;
6504
7057
  }, []);
@@ -6636,11 +7189,18 @@ const PageCanvas = react.forwardRef(
6636
7189
  const targetWidth = Math.max(1, Number(element.width) > 0 ? Number(element.width) : Number(obj.width ?? 200));
6637
7190
  const overflowPolicy = element.overflowPolicy || "grow-and-push";
6638
7191
  const splitByGrapheme = overflowPolicy === "auto-shrink" ? false : element.splitByGrapheme ?? element.wordWrap === "break-word";
7192
+ let reflowText = element.text || "Text";
7193
+ let reflowParsedStyles = null;
7194
+ if (element.formattingEnabled === true) {
7195
+ const parsed = parseTextMarkdown(reflowText);
7196
+ reflowText = parsed.plainText || " ";
7197
+ reflowParsedStyles = parsed.styles;
7198
+ }
6639
7199
  obj.set({
6640
7200
  width: targetWidth,
6641
7201
  minWidth: 1,
6642
7202
  dynamicMinWidth: 0,
6643
- text: element.text || "Text",
7203
+ text: reflowText,
6644
7204
  fontSize: element.fontSize || 16,
6645
7205
  fontFamily: element.fontFamily || "Open Sans",
6646
7206
  fontWeight: element.fontWeight || 400,
@@ -6649,6 +7209,11 @@ const PageCanvas = react.forwardRef(
6649
7209
  charSpacing: element.charSpacing || 0,
6650
7210
  splitByGrapheme
6651
7211
  });
7212
+ if (element.formattingEnabled === true) {
7213
+ obj.styles = reflowParsedStyles || {};
7214
+ }
7215
+ obj.editable = element.formattingEnabled !== true;
7216
+ obj.__formattingEnabled = element.formattingEnabled === true;
6652
7217
  obj.initDimensions();
6653
7218
  if (Math.abs((obj.width ?? 0) - targetWidth) > 0.01) {
6654
7219
  obj.width = targetWidth;
@@ -6656,10 +7221,26 @@ const PageCanvas = react.forwardRef(
6656
7221
  obj.dynamicMinWidth = 0;
6657
7222
  obj.setCoords();
6658
7223
  obj.dirty = true;
7224
+ try {
7225
+ obj._forceClearCache = true;
7226
+ if (typeof obj._clearCache === "function") obj._clearCache();
7227
+ } catch {
7228
+ }
6659
7229
  didReflow = true;
6660
7230
  };
6661
7231
  canvas2.getObjects().forEach(reflowObject);
6662
- if (didReflow) canvas2.requestRenderAll();
7232
+ if (didReflow) {
7233
+ canvas2.requestRenderAll();
7234
+ try {
7235
+ requestAnimationFrame(() => {
7236
+ try {
7237
+ canvas2.requestRenderAll();
7238
+ } catch {
7239
+ }
7240
+ });
7241
+ } catch {
7242
+ }
7243
+ }
6663
7244
  }, []);
6664
7245
  react.useEffect(() => {
6665
7246
  if (!canvasElRef.current) return;
@@ -6706,7 +7287,8 @@ const PageCanvas = react.forwardRef(
6706
7287
  absolutePositioned: true
6707
7288
  });
6708
7289
  fabricRef.current = fabricCanvas;
6709
- registerFabricCanvas(pageId, fabricCanvas);
7290
+ const storeRegistryKey = registerFabricCanvas(pageId, fabricCanvas);
7291
+ fabricCanvas.__storeRegistryKey = storeRegistryKey;
6710
7292
  const initFonts = async () => {
6711
7293
  try {
6712
7294
  await preloadAllFonts();
@@ -6999,7 +7581,10 @@ const PageCanvas = react.forwardRef(
6999
7581
  if (!window.__fabricCanvasRegistry || !(window.__fabricCanvasRegistry instanceof Map)) {
7000
7582
  window.__fabricCanvasRegistry = /* @__PURE__ */ new Map();
7001
7583
  }
7002
- window.__fabricCanvasRegistry.set(pageId, {
7584
+ const reg = window.__fabricCanvasRegistry;
7585
+ const registryKey = reg.has(pageId) ? `${pageId}#${Math.random().toString(36).slice(2, 10)}` : pageId;
7586
+ fabricCanvas.__registryKey = registryKey;
7587
+ reg.set(registryKey, {
7003
7588
  canvas: fabricCanvas,
7004
7589
  forceUnlockEdits
7005
7590
  });
@@ -7769,9 +8354,6 @@ const PageCanvas = react.forwardRef(
7769
8354
  scaleY: finalScaleY,
7770
8355
  transformMatrix: finalAbsoluteMatrix
7771
8356
  };
7772
- if (obj instanceof fabric__namespace.Textbox) {
7773
- elementUpdate.text = obj.text || "";
7774
- }
7775
8357
  if (sourceElement && sourceElement.opacity !== void 0) {
7776
8358
  elementUpdate.opacity = sourceElement.opacity;
7777
8359
  }
@@ -7850,6 +8432,12 @@ const PageCanvas = react.forwardRef(
7850
8432
  }
7851
8433
  if (target && target instanceof fabric__namespace.Textbox) {
7852
8434
  const elementId = getObjectId(target);
8435
+ if (target.__formattingEnabled === true || target.editable === false) {
8436
+ sonner.toast.info("Inline formatting is on — edit the text in the right panel.", {
8437
+ description: "This protects your **bold**, [c=...] and other formatting tokens."
8438
+ });
8439
+ return;
8440
+ }
7853
8441
  editingTextIdRef.current = elementId || null;
7854
8442
  target.enterEditing();
7855
8443
  target.selectAll();
@@ -7914,7 +8502,19 @@ const PageCanvas = react.forwardRef(
7914
8502
  });
7915
8503
  return () => {
7916
8504
  setReady(false);
7917
- unregisterFabricCanvas(pageId);
8505
+ unregisterFabricCanvas(fabricCanvas.__storeRegistryKey ?? pageId, fabricCanvas);
8506
+ try {
8507
+ if (typeof window !== "undefined") {
8508
+ const reg = window.__fabricCanvasRegistry;
8509
+ if (reg instanceof Map) {
8510
+ const key = fabricCanvas.__registryKey ?? pageId;
8511
+ const entry = reg.get(key);
8512
+ const entryCanvas = (entry == null ? void 0 : entry.canvas) ?? entry;
8513
+ if (entryCanvas === fabricCanvas) reg.delete(key);
8514
+ }
8515
+ }
8516
+ } catch {
8517
+ }
7918
8518
  if (fabricCanvas.__fontCleanup) {
7919
8519
  fabricCanvas.__fontCleanup();
7920
8520
  }
@@ -9276,6 +9876,12 @@ const PageCanvas = react.forwardRef(
9276
9876
  } else if (obj instanceof fabric__namespace.Textbox) {
9277
9877
  const overflowPolicy = element.overflowPolicy || "grow-and-push";
9278
9878
  let text = element.text || "Text";
9879
+ let parsedStyles = null;
9880
+ if (element.formattingEnabled === true) {
9881
+ const parsed = parseTextMarkdown(text);
9882
+ text = parsed.plainText || " ";
9883
+ parsedStyles = parsed.styles;
9884
+ }
9279
9885
  let fontSize = element.fontSize || 16;
9280
9886
  element.minFontSize || 8;
9281
9887
  const maxLines = element.maxLines || 3;
@@ -9372,6 +9978,11 @@ const PageCanvas = react.forwardRef(
9372
9978
  splitByGrapheme,
9373
9979
  text
9374
9980
  });
9981
+ if (element.formattingEnabled === true) {
9982
+ obj.styles = parsedStyles || {};
9983
+ } else {
9984
+ obj.styles = element.styles || {};
9985
+ }
9375
9986
  obj.initDimensions();
9376
9987
  if (Math.abs((obj.width ?? 0) - textboxWidth) > 0.01) {
9377
9988
  obj.width = textboxWidth;
@@ -9733,7 +10344,6 @@ const PageCanvas = react.forwardRef(
9733
10344
  const eraseAtEdge = 1 - amount;
9734
10345
  const STOPS = 64;
9735
10346
  const gamma = 1 / hardness;
9736
- const seamGuardPx = 2;
9737
10347
  const addStops = (g) => {
9738
10348
  for (let i = 0; i <= STOPS; i++) {
9739
10349
  const t = i / STOPS;
@@ -9748,32 +10358,24 @@ const PageCanvas = react.forwardRef(
9748
10358
  addStops(gradient);
9749
10359
  ctx.fillStyle = gradient;
9750
10360
  ctx.fillRect(x, y, w, band);
9751
- ctx.fillStyle = "rgba(0,0,0,1)";
9752
- ctx.fillRect(x, y, w, Math.min(seamGuardPx, band));
9753
10361
  } else if (side === "bottom") {
9754
10362
  const band = Math.max(1, h * size);
9755
10363
  gradient = ctx.createLinearGradient(0, y + h, 0, y + h - band);
9756
10364
  addStops(gradient);
9757
10365
  ctx.fillStyle = gradient;
9758
10366
  ctx.fillRect(x, y + h - band, w, band);
9759
- ctx.fillStyle = "rgba(0,0,0,1)";
9760
- ctx.fillRect(x, y + h - Math.min(seamGuardPx, band), w, Math.min(seamGuardPx, band));
9761
10367
  } else if (side === "left") {
9762
10368
  const band = Math.max(1, w * size);
9763
10369
  gradient = ctx.createLinearGradient(x, 0, x + band, 0);
9764
10370
  addStops(gradient);
9765
10371
  ctx.fillStyle = gradient;
9766
10372
  ctx.fillRect(x, y, band, h);
9767
- ctx.fillStyle = "rgba(0,0,0,1)";
9768
- ctx.fillRect(x, y, Math.min(seamGuardPx, band), h);
9769
10373
  } else {
9770
10374
  const band = Math.max(1, w * size);
9771
10375
  gradient = ctx.createLinearGradient(x + w, 0, x + w - band, 0);
9772
10376
  addStops(gradient);
9773
10377
  ctx.fillStyle = gradient;
9774
10378
  ctx.fillRect(x + w - band, y, band, h);
9775
- ctx.fillStyle = "rgba(0,0,0,1)";
9776
- ctx.fillRect(x + w - Math.min(seamGuardPx, band), y, Math.min(seamGuardPx, band), h);
9777
10379
  }
9778
10380
  ctx.restore();
9779
10381
  };
@@ -10385,6 +10987,7 @@ function PreviewCanvas({
10385
10987
  zoom = 1,
10386
10988
  absoluteZoom = false,
10387
10989
  skipFontReadyWait = false,
10990
+ pageIdOverride,
10388
10991
  className,
10389
10992
  onDynamicFieldClick,
10390
10993
  onReady
@@ -10451,13 +11054,19 @@ function PreviewCanvas({
10451
11054
  backgroundGradient: (_b2 = page == null ? void 0 : page.settings) == null ? void 0 : _b2.backgroundGradient
10452
11055
  };
10453
11056
  }, [(_d = page == null ? void 0 : page.settings) == null ? void 0 : _d.backgroundColor, (_e = page == null ? void 0 : page.settings) == null ? void 0 : _e.backgroundGradient]);
10454
- const projectSettings = react.useMemo(() => ({
10455
- showGrid: false,
10456
- snapToGrid: false,
10457
- gridSize: 10,
10458
- snapToGuides: false,
10459
- snapThreshold: 5
10460
- }), []);
11057
+ const projectSettings = react.useMemo(() => {
11058
+ var _a2, _b2, _c2;
11059
+ const vars = ((_a2 = config.themeConfig) == null ? void 0 : _a2.variables) || {};
11060
+ return {
11061
+ showGrid: false,
11062
+ snapToGrid: false,
11063
+ gridSize: 10,
11064
+ snapToGuides: false,
11065
+ snapThreshold: 5,
11066
+ primaryColor: (_b2 = vars.primary) == null ? void 0 : _b2.value,
11067
+ secondaryColor: (_c2 = vars.secondary) == null ? void 0 : _c2.value
11068
+ };
11069
+ }, [config.themeConfig]);
10461
11070
  const handleDynamicFieldClick = react.useCallback((elementId) => {
10462
11071
  const fieldInfo = elementToFieldMap.get(elementId);
10463
11072
  if (fieldInfo && onDynamicFieldClick) {
@@ -10527,7 +11136,7 @@ function PreviewCanvas({
10527
11136
  PageCanvas,
10528
11137
  {
10529
11138
  ref: canvasRef,
10530
- pageId: page.id || `page-${pageIndex}`,
11139
+ pageId: pageIdOverride || page.id || `page-${pageIndex}`,
10531
11140
  elements,
10532
11141
  pageChildren: laidOutPageChildren,
10533
11142
  pageSettings,
@@ -12418,6 +13027,17 @@ function normalizeFontFamily(fontStack) {
12418
13027
  const first = fontStack.split(",")[0].trim();
12419
13028
  return first.replace(/^['"]|['"]$/g, "");
12420
13029
  }
13030
+ function appendStylesheet(url, rejectOnError = true) {
13031
+ return new Promise((resolve, reject) => {
13032
+ const link = document.createElement("link");
13033
+ link.rel = "stylesheet";
13034
+ link.href = url;
13035
+ link.crossOrigin = "anonymous";
13036
+ link.onload = () => resolve();
13037
+ link.onerror = () => rejectOnError ? reject(new Error(`Failed to load stylesheet: ${url}`)) : resolve();
13038
+ document.head.appendChild(link);
13039
+ });
13040
+ }
12421
13041
  const loadedFonts = /* @__PURE__ */ new Set();
12422
13042
  const loadingPromises = /* @__PURE__ */ new Map();
12423
13043
  function withTimeout(promise, timeoutMs = 4e3) {
@@ -12443,28 +13063,19 @@ async function loadGoogleFontCSS(rawFontFamily) {
12443
13063
  const fontshareSlug = FONTSHARE_SLUGS[fontFamily];
12444
13064
  if (fontshareSlug) {
12445
13065
  const url2 = `https://api.fontshare.com/v2/css?f[]=${fontshareSlug}@300,400,500,700&display=swap`;
12446
- const link2 = document.createElement("link");
12447
- link2.rel = "stylesheet";
12448
- link2.href = url2;
12449
- await new Promise((resolve, reject) => {
12450
- link2.onload = () => resolve();
12451
- link2.onerror = () => reject(new Error(`Failed to load Fontshare font: ${fontFamily}`));
12452
- document.head.appendChild(link2);
12453
- });
13066
+ await appendStylesheet(url2);
13067
+ const italicUrl = `https://api.fontshare.com/v2/css?f[]=${fontshareSlug}@300i,400i,500i,700i&display=swap`;
13068
+ await withTimeout(appendStylesheet(italicUrl, false), 1500);
12454
13069
  loadedFonts.add(fontFamily);
12455
13070
  return;
12456
13071
  }
12457
13072
  const encoded = encodeURIComponent(fontFamily);
12458
13073
  const url = `https://fonts.googleapis.com/css?family=${encoded}:300,400,500,600,700&display=swap`;
12459
- const link = document.createElement("link");
12460
- link.rel = "stylesheet";
12461
- link.href = url;
12462
- link.crossOrigin = "anonymous";
12463
- await new Promise((resolve, reject) => {
12464
- link.onload = () => resolve();
12465
- link.onerror = () => reject(new Error(`Failed to load font: ${fontFamily}`));
12466
- document.head.appendChild(link);
12467
- });
13074
+ await appendStylesheet(url);
13075
+ await withTimeout(Promise.all([300, 400, 500, 600, 700].map((weight) => {
13076
+ const italicUrl = `https://fonts.googleapis.com/css2?family=${encoded}:ital,wght@1,${weight}&display=swap`;
13077
+ return appendStylesheet(italicUrl, false);
13078
+ })), 1800);
12468
13079
  loadedFonts.add(fontFamily);
12469
13080
  } catch (e) {
12470
13081
  console.warn(`[@pixldocs/canvas-renderer] Font load failed: ${fontFamily}`, e);
@@ -12534,6 +13145,23 @@ function collectFontDescriptorsFromConfig(config) {
12534
13145
  if (node.type === "text") {
12535
13146
  for (const w of [300, 400, 500, 600, 700]) {
12536
13147
  add(node.fontFamily, w, node.fontStyle);
13148
+ add(node.fontFamily, w, "italic");
13149
+ }
13150
+ }
13151
+ }
13152
+ if (node.formattingEnabled === true && node.fontFamily) {
13153
+ const parsed = parseTextMarkdown(String(node.text ?? ""));
13154
+ const parsedStyleEntries = Object.values(parsed.styles || {});
13155
+ for (const lineStyle of parsedStyleEntries) {
13156
+ if (lineStyle && typeof lineStyle === "object") {
13157
+ for (const charStyle of Object.values(lineStyle)) {
13158
+ if (!charStyle || typeof charStyle !== "object") continue;
13159
+ add(
13160
+ charStyle.fontFamily || node.fontFamily,
13161
+ charStyle.fontWeight ?? node.fontWeight,
13162
+ charStyle.fontStyle ?? node.fontStyle
13163
+ );
13164
+ }
12537
13165
  }
12538
13166
  }
12539
13167
  }
@@ -13443,11 +14071,20 @@ function PixldocsPreview(props) {
13443
14071
  !canvasSettled && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
13444
14072
  ] });
13445
14073
  }
13446
- const PACKAGE_VERSION = "0.5.103";
14074
+ const PACKAGE_VERSION = "0.5.105";
13447
14075
  const roundParityValue = (value) => {
13448
14076
  if (typeof value !== "number") return value;
13449
14077
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
13450
14078
  };
14079
+ function isolatePageForCapture(config, pageIndex) {
14080
+ var _a;
14081
+ const capturePageId = `__pixldocs_capture_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}_${pageIndex}`;
14082
+ const cloned = JSON.parse(JSON.stringify(config));
14083
+ if ((_a = cloned.pages) == null ? void 0 : _a[pageIndex]) {
14084
+ cloned.pages[pageIndex].id = capturePageId;
14085
+ }
14086
+ return { config: cloned, pageId: capturePageId };
14087
+ }
13451
14088
  function logJsonLine(tag, payload) {
13452
14089
  try {
13453
14090
  console.log(`${tag} ${JSON.stringify(payload, (_key, value) => roundParityValue(value))}`);
@@ -13485,6 +14122,9 @@ function installUnderlineFix(fab) {
13485
14122
  const hasOwn = !!this[type];
13486
14123
  const hasStyled = typeof this.styleHas === "function" && this.styleHas(type);
13487
14124
  if (!hasOwn && !hasStyled) return;
14125
+ if (!hasOwn && hasStyled) {
14126
+ return original.call(this, ctx, type);
14127
+ }
13488
14128
  const lines = this._textLines;
13489
14129
  const offsets = this.offsets;
13490
14130
  if (!Array.isArray(lines) || !offsets) {
@@ -13496,7 +14136,7 @@ function installUnderlineFix(fab) {
13496
14136
  let topOffset = this._getTopOffset();
13497
14137
  for (let i = 0, len = lines.length; i < len; i++) {
13498
14138
  const heightOfLine = this.getHeightOfLine(i);
13499
- const lineHas = !!this[type] || typeof this.styleHas === "function" && this.styleHas(type, i);
14139
+ const lineHas = !!this[type];
13500
14140
  if (!lineHas) {
13501
14141
  topOffset += heightOfLine;
13502
14142
  continue;
@@ -14026,9 +14666,11 @@ class PixldocsRenderer {
14026
14666
  }
14027
14667
  async renderPageViaPreviewCanvas(config, pageIndex, pixelRatio, format, quality, options = {}) {
14028
14668
  const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
14029
- const canvasWidth = config.canvas.width;
14030
- const canvasHeight = config.canvas.height;
14031
- const hasAutoShrink = configHasAutoShrinkText(config);
14669
+ const capture = isolatePageForCapture(config, pageIndex);
14670
+ const renderConfig = capture.config;
14671
+ const canvasWidth = renderConfig.canvas.width;
14672
+ const canvasHeight = renderConfig.canvas.height;
14673
+ const hasAutoShrink = configHasAutoShrinkText(renderConfig);
14032
14674
  let firstMountSettled = false;
14033
14675
  let lateFontSettleDetected = false;
14034
14676
  if (typeof document !== "undefined" && document.fonts && hasAutoShrink) {
@@ -14076,12 +14718,12 @@ class PixldocsRenderer {
14076
14718
  root = client.createRoot(container);
14077
14719
  await new Promise((settle) => {
14078
14720
  const onReadyOnce = () => {
14079
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14721
+ this.waitForCanvasScene(container, renderConfig, pageIndex).then(async () => {
14080
14722
  const fabricInstance = this.getFabricCanvasFromContainer(container);
14081
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14723
+ const expectedImageCount = this.getExpectedImageCount(renderConfig, pageIndex);
14082
14724
  await this.waitForCanvasImages(container, expectedImageCount);
14083
- await this.waitForStableTextMetrics(container, config);
14084
- await this.waitForCanvasScene(container, config, pageIndex);
14725
+ await this.waitForStableTextMetrics(container, renderConfig);
14726
+ await this.waitForCanvasScene(container, renderConfig, pageIndex);
14085
14727
  if (!fabricInstance) return settle();
14086
14728
  settle();
14087
14729
  }).catch(() => settle());
@@ -14089,8 +14731,9 @@ class PixldocsRenderer {
14089
14731
  root.render(
14090
14732
  react.createElement(PreviewCanvas2, {
14091
14733
  key: `remount-${mountKey}`,
14092
- config,
14734
+ config: renderConfig,
14093
14735
  pageIndex,
14736
+ pageIdOverride: capture.pageId,
14094
14737
  zoom: pixelRatio,
14095
14738
  absoluteZoom: true,
14096
14739
  skipFontReadyWait: false,
@@ -14100,13 +14743,13 @@ class PixldocsRenderer {
14100
14743
  });
14101
14744
  };
14102
14745
  const onReady = () => {
14103
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14746
+ this.waitForCanvasScene(container, renderConfig, pageIndex).then(async () => {
14104
14747
  try {
14105
14748
  const fabricInstance = this.getFabricCanvasFromContainer(container);
14106
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14749
+ const expectedImageCount = this.getExpectedImageCount(renderConfig, pageIndex);
14107
14750
  await this.waitForCanvasImages(container, expectedImageCount);
14108
- await this.waitForStableTextMetrics(container, config);
14109
- await this.waitForCanvasScene(container, config, pageIndex);
14751
+ await this.waitForStableTextMetrics(container, renderConfig);
14752
+ await this.waitForCanvasScene(container, renderConfig, pageIndex);
14110
14753
  firstMountSettled = true;
14111
14754
  if (hasAutoShrink && lateFontSettleDetected) {
14112
14755
  console.log("[canvas-renderer][parity] late font-settle detected — remounting for auto-shrink reflow");
@@ -14132,7 +14775,7 @@ class PixldocsRenderer {
14132
14775
  }
14133
14776
  exportCtx.save();
14134
14777
  exportCtx.scale(sourceCanvasAfter.width / canvasWidth, sourceCanvasAfter.height / canvasHeight);
14135
- this.paintPageBackground(exportCtx, config.pages[pageIndex], canvasWidth, canvasHeight);
14778
+ this.paintPageBackground(exportCtx, renderConfig.pages[pageIndex], canvasWidth, canvasHeight);
14136
14779
  exportCtx.restore();
14137
14780
  exportCtx.drawImage(sourceCanvasAfter, 0, 0);
14138
14781
  const mimeType = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
@@ -14148,8 +14791,9 @@ class PixldocsRenderer {
14148
14791
  root = client.createRoot(container);
14149
14792
  root.render(
14150
14793
  react.createElement(PreviewCanvas2, {
14151
- config,
14794
+ config: renderConfig,
14152
14795
  pageIndex,
14796
+ pageIdOverride: capture.pageId,
14153
14797
  zoom: pixelRatio,
14154
14798
  absoluteZoom: true,
14155
14799
  skipFontReadyWait: false,
@@ -14170,6 +14814,8 @@ class PixldocsRenderer {
14170
14814
  captureSvgViaPreviewCanvas(config, pageIndex, canvasWidth, canvasHeight) {
14171
14815
  return new Promise(async (resolve, reject) => {
14172
14816
  const { PreviewCanvas: PreviewCanvas2 } = await Promise.resolve().then(() => PreviewCanvas$1);
14817
+ const capture = isolatePageForCapture(config, pageIndex);
14818
+ const renderConfig = capture.config;
14173
14819
  const container = document.createElement("div");
14174
14820
  container.style.cssText = `
14175
14821
  position: fixed; left: -99999px; top: -99999px;
@@ -14196,8 +14842,9 @@ class PixldocsRenderer {
14196
14842
  root.render(
14197
14843
  react.createElement(PreviewCanvas2, {
14198
14844
  key: `svg-capture-${mountKey}`,
14199
- config,
14845
+ config: renderConfig,
14200
14846
  pageIndex,
14847
+ pageIdOverride: capture.pageId,
14201
14848
  zoom: 1,
14202
14849
  absoluteZoom: true,
14203
14850
  skipFontReadyWait: false,
@@ -14206,13 +14853,13 @@ class PixldocsRenderer {
14206
14853
  );
14207
14854
  };
14208
14855
  const onReady = () => {
14209
- this.waitForCanvasScene(container, config, pageIndex).then(async () => {
14856
+ this.waitForCanvasScene(container, renderConfig, pageIndex).then(async () => {
14210
14857
  var _a, _b;
14211
14858
  try {
14212
- const expectedImageCount = this.getExpectedImageCount(config, pageIndex);
14859
+ const expectedImageCount = this.getExpectedImageCount(renderConfig, pageIndex);
14213
14860
  await this.waitForCanvasImages(container, expectedImageCount);
14214
- await this.waitForStableTextMetrics(container, config);
14215
- await this.waitForCanvasScene(container, config, pageIndex);
14861
+ await this.waitForStableTextMetrics(container, renderConfig);
14862
+ await this.waitForCanvasScene(container, renderConfig, pageIndex);
14216
14863
  const fabricInstance = this.getFabricCanvasFromContainer(container);
14217
14864
  if (!fabricInstance) {
14218
14865
  cleanup();
@@ -14301,7 +14948,7 @@ class PixldocsRenderer {
14301
14948
  );
14302
14949
  if (prevVPT) fabricInstance.viewportTransform = prevVPT;
14303
14950
  fabricInstance.svgViewportTransformation = prevSvgVPT;
14304
- const page = config.pages[pageIndex];
14951
+ const page = renderConfig.pages[pageIndex];
14305
14952
  const backgroundColor = ((_a = page == null ? void 0 : page.settings) == null ? void 0 : _a.backgroundColor) || "#ffffff";
14306
14953
  const backgroundGradient = (_b = page == null ? void 0 : page.settings) == null ? void 0 : _b.backgroundGradient;
14307
14954
  cleanup();
@@ -14983,8 +15630,9 @@ function getFontPathForWeight(files, weight, isItalic = false) {
14983
15630
  }
14984
15631
  return files.regular;
14985
15632
  }
14986
- function isItalicPath(files, path) {
14987
- return path === files.italic || path === files.boldItalic || path === files.lightItalic || path === files.mediumItalic || path === files.semiboldItalic;
15633
+ function isExactWeightItalicMatch(files, weight, isItalic, path) {
15634
+ const exactKey = isItalic ? weight === 300 ? "lightItalic" : weight === 500 ? "mediumItalic" : weight === 600 ? "semiboldItalic" : weight === 700 ? "boldItalic" : "italic" : weight === 300 ? "light" : weight === 500 ? "medium" : weight === 600 ? "semibold" : weight === 700 ? "bold" : "regular";
15635
+ return files[exactKey] === path;
14988
15636
  }
14989
15637
  function getJsPDFFontName(fontName) {
14990
15638
  return fontName.replace(/\s+/g, "");
@@ -15057,10 +15705,10 @@ async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
15057
15705
  const resolvedWeight = resolveFontWeight(weight);
15058
15706
  const fontPath = getFontPathForWeight(fontFiles, resolvedWeight, isItalic);
15059
15707
  if (!fontPath) return false;
15060
- const hasItalicFile = isItalic && isItalicPath(fontFiles, fontPath);
15061
- const jsPdfFontName = getEmbeddedJsPDFFontName(fontName, weight, hasItalicFile);
15708
+ if (!isExactWeightItalicMatch(fontFiles, resolvedWeight, isItalic, fontPath)) return false;
15709
+ const jsPdfFontName = getEmbeddedJsPDFFontName(fontName, weight, isItalic);
15062
15710
  const label = FONT_WEIGHT_LABELS[resolvedWeight];
15063
- const italicSuffix = hasItalicFile ? "Italic" : "";
15711
+ const italicSuffix = isItalic ? "Italic" : "";
15064
15712
  const fileName = `${getJsPDFFontName(fontName)}-${label}${italicSuffix}.ttf`;
15065
15713
  const url = baseUrl + fontPath;
15066
15714
  try {
@@ -15075,6 +15723,7 @@ async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
15075
15723
  }
15076
15724
  }
15077
15725
  registeredFamilies.add(fontName);
15726
+ registeredVariants.add(variantKey(fontName, resolvedWeight, isItalic));
15078
15727
  return true;
15079
15728
  } catch (e) {
15080
15729
  console.warn(`[pdf-fonts] Failed to embed ${fontName} w${weight}:`, e);
@@ -15083,6 +15732,18 @@ async function embedFont(pdf, fontName, weight, fontBaseUrl, isItalic = false) {
15083
15732
  }
15084
15733
  const googleFontNotFound = /* @__PURE__ */ new Set();
15085
15734
  const fontshareNotFound = /* @__PURE__ */ new Set();
15735
+ const remoteVariantKey = (family, weight, isItalic) => `${family}|${resolveFontWeight(weight)}|${isItalic ? "i" : "n"}`;
15736
+ const registeredVariants = /* @__PURE__ */ new Set();
15737
+ const variantKey = (family, weight, italic) => `${family}|${resolveFontWeight(weight)}|${italic ? "i" : "n"}`;
15738
+ const resolveBestRegisteredVariant = (family, weight, italic) => {
15739
+ const want = resolveFontWeight(weight);
15740
+ const weights = [300, 400, 500, 600, 700];
15741
+ if (registeredVariants.has(variantKey(family, want, italic))) return { weight: want, italic };
15742
+ const nearest = [...weights].sort((a, b) => Math.abs(a - want) - Math.abs(b - want) || (want >= 500 ? b - a : a - b));
15743
+ for (const w of nearest) if (registeredVariants.has(variantKey(family, w, italic))) return { weight: w, italic };
15744
+ for (const w of nearest) if (registeredVariants.has(variantKey(family, w, !italic))) return { weight: w, italic: !italic };
15745
+ return null;
15746
+ };
15086
15747
  function bytesToBase64(bytes) {
15087
15748
  let binary = "";
15088
15749
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
@@ -15111,7 +15772,8 @@ async function fetchTtfViaProxy(family, weight, isItalic, source) {
15111
15772
  async function fetchGoogleFontTTF(fontFamily, weight, isItalic = false) {
15112
15773
  const cacheKey = `gf:${fontFamily}:${weight}:${isItalic ? "i" : "n"}`;
15113
15774
  if (ttfCache.has(cacheKey)) return ttfCache.get(cacheKey);
15114
- if (googleFontNotFound.has(fontFamily)) return null;
15775
+ const notFoundKey = remoteVariantKey(fontFamily, weight, isItalic);
15776
+ if (googleFontNotFound.has(notFoundKey)) return null;
15115
15777
  const proxyBytes = await fetchTtfViaProxy(fontFamily, weight, isItalic, "google");
15116
15778
  if (proxyBytes) {
15117
15779
  const b64 = bytesToBase64(proxyBytes);
@@ -15129,7 +15791,7 @@ async function fetchGoogleFontTTF(fontFamily, weight, isItalic = false) {
15129
15791
  }
15130
15792
  });
15131
15793
  if (!cssRes.ok) {
15132
- if (cssRes.status === 400 || cssRes.status === 404) googleFontNotFound.add(fontFamily);
15794
+ if (cssRes.status === 400 || cssRes.status === 404) googleFontNotFound.add(notFoundKey);
15133
15795
  return null;
15134
15796
  }
15135
15797
  const css = await cssRes.text();
@@ -15207,6 +15869,7 @@ function registerJsPdfFont(pdf, fontName, resolvedWeight, isItalic, base64) {
15207
15869
  }
15208
15870
  }
15209
15871
  registeredFamilies.add(fontName);
15872
+ registeredVariants.add(variantKey(fontName, resolvedWeight, isItalic));
15210
15873
  return true;
15211
15874
  } catch (err) {
15212
15875
  console.warn(`[pdf-fonts] registerJsPdfFont failed for ${fontName}:`, err);
@@ -15221,16 +15884,8 @@ async function embedFontWithGoogleFallback(pdf, fontName, weight = 400, fontBase
15221
15884
  const resolved = resolveFontWeight(weight);
15222
15885
  const fsB64 = await fetchFontshareTTF(fontName, resolved, isItalic);
15223
15886
  if (fsB64) return registerJsPdfFont(pdf, fontName, resolved, isItalic, fsB64);
15224
- if (isItalic) {
15225
- const fsUpright = await fetchFontshareTTF(fontName, resolved, false);
15226
- if (fsUpright) return registerJsPdfFont(pdf, fontName, resolved, isItalic, fsUpright);
15227
- }
15228
15887
  const b64 = await fetchGoogleFontTTF(fontName, resolved, isItalic);
15229
15888
  if (b64) return registerJsPdfFont(pdf, fontName, resolved, isItalic, b64);
15230
- if (isItalic) {
15231
- const uprightB64 = await fetchGoogleFontTTF(fontName, resolved, false);
15232
- if (uprightB64) return registerJsPdfFont(pdf, fontName, resolved, isItalic, uprightB64);
15233
- }
15234
15889
  return false;
15235
15890
  }
15236
15891
  async function embedFontsForConfig(pdf, config, fontBaseUrl) {
@@ -15250,9 +15905,15 @@ async function embedFontsForConfig(pdf, config, fontBaseUrl) {
15250
15905
  };
15251
15906
  const walkElements = (elements) => {
15252
15907
  for (const el of elements) {
15908
+ const addFontVariant = (family, weight, italic) => {
15909
+ fontKeys.add(`${family}${SEP}${weight}${SEP}${italic ? "italic" : "normal"}`);
15910
+ };
15253
15911
  if (el.fontFamily) {
15254
15912
  const w = normalizeWeight(el.fontWeight);
15255
- fontKeys.add(`${el.fontFamily}${SEP}${w}`);
15913
+ addFontVariant(el.fontFamily, w, /italic|oblique/i.test(String(el.fontStyle ?? "")));
15914
+ addFontVariant(el.fontFamily, 700, false);
15915
+ addFontVariant(el.fontFamily, 400, true);
15916
+ addFontVariant(el.fontFamily, 700, true);
15256
15917
  }
15257
15918
  if (el.styles && typeof el.styles === "object") {
15258
15919
  for (const lineKey of Object.keys(el.styles)) {
@@ -15260,9 +15921,10 @@ async function embedFontsForConfig(pdf, config, fontBaseUrl) {
15260
15921
  if (lineStyles && typeof lineStyles === "object") {
15261
15922
  for (const charKey of Object.keys(lineStyles)) {
15262
15923
  const s = lineStyles[charKey];
15263
- if (s == null ? void 0 : s.fontFamily) {
15264
- const w = normalizeWeight(s.fontWeight);
15265
- fontKeys.add(`${s.fontFamily}${SEP}${w}`);
15924
+ const styledFamily = (s == null ? void 0 : s.fontFamily) || el.fontFamily;
15925
+ if (styledFamily) {
15926
+ const w = normalizeWeight(s.fontWeight ?? el.fontWeight);
15927
+ addFontVariant(styledFamily, w, /italic|oblique/i.test(String(s.fontStyle ?? el.fontStyle ?? "")));
15266
15928
  }
15267
15929
  }
15268
15930
  }
@@ -15276,19 +15938,21 @@ async function embedFontsForConfig(pdf, config, fontBaseUrl) {
15276
15938
  if (page.children) walkElements(page.children);
15277
15939
  if (page.elements) walkElements(page.elements);
15278
15940
  }
15279
- fontKeys.add(`${FONT_FALLBACK_SYMBOLS}${SEP}400`);
15941
+ fontKeys.add(`${FONT_FALLBACK_SYMBOLS}${SEP}400${SEP}normal`);
15280
15942
  for (const w of [300, 400, 500, 600, 700]) {
15281
- fontKeys.add(`${FONT_FALLBACK_DEVANAGARI}${SEP}${w}`);
15943
+ fontKeys.add(`${FONT_FALLBACK_DEVANAGARI}${SEP}${w}${SEP}normal`);
15282
15944
  }
15283
15945
  const embedded = /* @__PURE__ */ new Set();
15284
15946
  const tasks = [];
15285
15947
  for (const key of fontKeys) {
15286
15948
  const sep = key.indexOf(SEP);
15949
+ const secondSep = key.indexOf(SEP, sep + 1);
15287
15950
  const fontName = key.slice(0, sep);
15288
- const weight = parseInt(key.slice(sep + 1), 10);
15951
+ const weight = parseInt(key.slice(sep + 1, secondSep), 10);
15952
+ const italic = key.slice(secondSep + 1) === "italic";
15289
15953
  if (!isFontAvailable(fontName)) continue;
15290
15954
  tasks.push(
15291
- embedFont(pdf, fontName, weight, fontBaseUrl).then((ok) => {
15955
+ embedFont(pdf, fontName, weight, fontBaseUrl, italic).then((ok) => {
15292
15956
  if (ok) embedded.add(key);
15293
15957
  })
15294
15958
  );
@@ -15344,7 +16008,7 @@ function splitIntoRuns(text) {
15344
16008
  return runs;
15345
16009
  }
15346
16010
  function rewriteSvgFontsForJsPDF(svgStr) {
15347
- var _a, _b;
16011
+ var _a, _b, _c;
15348
16012
  const parser = new DOMParser();
15349
16013
  const doc = parser.parseFromString(svgStr, "image/svg+xml");
15350
16014
  const allTextEls = Array.from(doc.querySelectorAll("text, tspan, textPath"));
@@ -15376,6 +16040,13 @@ function rewriteSvgFontsForJsPDF(svgStr) {
15376
16040
  stylePairs.push(`font-style: normal`);
15377
16041
  return stylePairs.join("; ");
15378
16042
  };
16043
+ const applySyntheticItalicTransform = (el) => {
16044
+ const x = Number.parseFloat(resolveInheritedValue(el, "x") || "0") || 0;
16045
+ const y = Number.parseFloat(resolveInheritedValue(el, "y") || "0") || 0;
16046
+ const existing = el.getAttribute("transform") || "";
16047
+ const synth = `translate(${x} ${y}) skewX(-12) translate(${-x} ${-y})`;
16048
+ el.setAttribute("transform", existing ? `${existing} ${synth}` : synth);
16049
+ };
15379
16050
  const getDepth = (el) => {
15380
16051
  let depth = 0;
15381
16052
  let current = el.parentElement;
@@ -15395,20 +16066,40 @@ function rewriteSvgFontsForJsPDF(svgStr) {
15395
16066
  const weightRaw = resolveInheritedValue(el, "font-weight") || "400";
15396
16067
  const styleRaw = resolveInheritedValue(el, "font-style") || "normal";
15397
16068
  const weight = resolveWeightNum(weightRaw);
15398
- const resolved = resolveFontWeight(weight);
15399
- const isItalic = /italic|oblique/i.test(styleRaw);
15400
- const jsPdfName = getEmbeddedJsPDFFontName(clean, resolved, isItalic);
15401
- sourceMeta.set(el, { clean, resolved, isItalic, jsPdfName, inlineStyle, weight });
16069
+ const requested = resolveFontWeight(weight);
16070
+ const requestedItalic = /italic|oblique/i.test(styleRaw);
16071
+ const best = resolveBestRegisteredVariant(clean, requested, requestedItalic);
16072
+ if (!best) continue;
16073
+ const jsPdfName = getEmbeddedJsPDFFontName(clean, best.weight, best.italic);
16074
+ sourceMeta.set(el, { clean, requested, resolved: best.weight, isItalic: requestedItalic, actualItalic: best.italic, jsPdfName, inlineStyle, weight });
15402
16075
  }
15403
16076
  const textEls = allTextEls.sort((a, b) => getDepth(b) - getDepth(a));
15404
16077
  for (const el of textEls) {
15405
16078
  if (!el.isConnected) continue;
15406
16079
  const meta = sourceMeta.get(el);
15407
16080
  if (!meta) continue;
15408
- const { clean, resolved, isItalic, jsPdfName, inlineStyle, weight } = meta;
16081
+ const { clean, requested, resolved, isItalic, actualItalic, jsPdfName, inlineStyle, weight } = meta;
15409
16082
  el.setAttribute("data-source-font-family", clean);
15410
- el.setAttribute("data-source-font-weight", String(resolved));
16083
+ el.setAttribute("data-source-font-weight", String(requested));
15411
16084
  el.setAttribute("data-source-font-style", isItalic ? "italic" : "normal");
16085
+ if (requested >= 600 && resolved < 600) {
16086
+ const fill = resolveInheritedValue(el, "fill") || readStyleToken(inlineStyle, "fill") || "#000000";
16087
+ el.setAttribute("stroke", fill);
16088
+ el.setAttribute("stroke-width", String(requested === 700 ? 0.7 : 0.5));
16089
+ el.setAttribute("stroke-linejoin", "round");
16090
+ }
16091
+ if (isItalic && !actualItalic) {
16092
+ applySyntheticItalicTransform(el);
16093
+ try {
16094
+ console.log("[Vector PDF][synthetic-italic]", {
16095
+ tag: el.tagName,
16096
+ family: clean,
16097
+ weight: requested,
16098
+ textPreview: (el.textContent || "").slice(0, 40)
16099
+ });
16100
+ } catch {
16101
+ }
16102
+ }
15412
16103
  const directText = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.textContent || "").join("");
15413
16104
  const hasDevanagari = containsDevanagari(directText);
15414
16105
  const hasSymbol = containsSymbol(directText);
@@ -15451,6 +16142,28 @@ function rewriteSvgFontsForJsPDF(svgStr) {
15451
16142
  el.setAttribute("style", buildStyleString(inlineStyle, jsPdfName));
15452
16143
  }
15453
16144
  }
16145
+ const SVG_NS = "http://www.w3.org/2000/svg";
16146
+ const transformedTspans = Array.from(doc.querySelectorAll("tspan[transform]"));
16147
+ for (const tspan of transformedTspans) {
16148
+ const parentText = tspan.parentElement;
16149
+ if (!parentText || parentText.tagName.toLowerCase() !== "text") continue;
16150
+ const transformVal = tspan.getAttribute("transform") || "";
16151
+ if (!/skewX/i.test(transformVal)) continue;
16152
+ const wrapper = doc.createElementNS(SVG_NS, "text");
16153
+ let parentTransform = "";
16154
+ for (const attr of Array.from(parentText.attributes)) {
16155
+ if (attr.name === "transform") {
16156
+ parentTransform = attr.value;
16157
+ continue;
16158
+ }
16159
+ wrapper.setAttribute(attr.name, attr.value);
16160
+ }
16161
+ const combined = parentTransform ? `${parentTransform} ${transformVal}` : transformVal;
16162
+ wrapper.setAttribute("transform", combined);
16163
+ tspan.removeAttribute("transform");
16164
+ wrapper.appendChild(tspan);
16165
+ (_c = parentText.parentNode) == null ? void 0 : _c.insertBefore(wrapper, parentText.nextSibling);
16166
+ }
15454
16167
  return new XMLSerializer().serializeToString(doc.documentElement);
15455
16168
  }
15456
16169
  function extractFontFamiliesFromSvgs(svgs) {
@@ -15481,14 +16194,29 @@ async function embedFontsInPdf(pdf, fontFamilies, fontBaseUrl) {
15481
16194
  if (ok) embedded.add(`${family}${w}`);
15482
16195
  })
15483
16196
  );
16197
+ tasks.push(
16198
+ embedFont(pdf, family, w, fontBaseUrl, true).then((ok) => {
16199
+ if (ok) embedded.add(`${family}${w}i`);
16200
+ })
16201
+ );
15484
16202
  }
15485
16203
  } else {
15486
- tasks.push(
15487
- embedFontWithGoogleFallback(pdf, family, 400, fontBaseUrl, false).then((ok) => {
15488
- if (ok) embedded.add(`${family}400`);
15489
- else console.warn(`[pdf-fonts] No TTF (local/Google/Fontshare) for "${family}" — will use Helvetica fallback`);
15490
- })
15491
- );
16204
+ const variants = [
16205
+ { w: 400, italic: false },
16206
+ { w: 700, italic: false },
16207
+ { w: 400, italic: true },
16208
+ { w: 700, italic: true }
16209
+ ];
16210
+ for (const v of variants) {
16211
+ tasks.push(
16212
+ embedFontWithGoogleFallback(pdf, family, v.w, fontBaseUrl, v.italic).then((ok) => {
16213
+ if (ok) embedded.add(`${family}${v.w}${v.italic ? "i" : ""}`);
16214
+ else if (v.w === 400 && !v.italic) {
16215
+ console.warn(`[pdf-fonts] No TTF (local/Google/Fontshare) for "${family}" — will use Helvetica fallback`);
16216
+ }
16217
+ })
16218
+ );
16219
+ }
15492
16220
  }
15493
16221
  }
15494
16222
  await Promise.all(tasks);
@@ -15509,6 +16237,7 @@ const pdfFonts = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProp
15509
16237
  getEmbeddedJsPDFFontName,
15510
16238
  getFontPathForWeight,
15511
16239
  isFontAvailable,
16240
+ resolveBestRegisteredVariant,
15512
16241
  resolveFontWeight,
15513
16242
  rewriteSvgFontsForJsPDF
15514
16243
  }, Symbol.toStringTag, { value: "Module" }));
@@ -15548,9 +16277,14 @@ function dumpSvgTextDiagnostics(svgStr, pageIndex, tag, stage, maxItems = 30) {
15548
16277
  const sample = texts.slice(0, maxItems).map((t, idx) => {
15549
16278
  var _a, _b;
15550
16279
  const tspans = Array.from(t.querySelectorAll("tspan"));
15551
- const tspanInfo = tspans.slice(0, 4).map((s) => ({
16280
+ const tspanInfo = tspans.slice(0, 8).map((s) => ({
15552
16281
  x: s.getAttribute("x"),
15553
16282
  y: s.getAttribute("y"),
16283
+ fs: s.getAttribute("font-style"),
16284
+ fw: s.getAttribute("font-weight"),
16285
+ ff: s.getAttribute("font-family"),
16286
+ td: s.getAttribute("text-decoration"),
16287
+ style: (s.getAttribute("style") || "").slice(0, 120),
15554
16288
  text: (s.textContent || "").slice(0, 40)
15555
16289
  }));
15556
16290
  let containerWidth = null;
@@ -16585,13 +17319,15 @@ async function convertTextDecorationsToLines(svg) {
16585
17319
  const textDecOnText = (textEl.getAttribute("text-decoration") || "").toLowerCase();
16586
17320
  const textStyleDec = (getInlineStyleValue(textEl, "text-decoration") || "").toLowerCase();
16587
17321
  const textHasUnderline = textDecOnText.includes("underline") || textStyleDec.includes("underline");
17322
+ const textHasLinethrough = textDecOnText.includes("line-through") || textStyleDec.includes("line-through");
16588
17323
  for (let si = 0; si < tspans.length; si++) {
16589
17324
  const tspan = tspans[si];
16590
17325
  const liveTspan = liveTspans == null ? void 0 : liveTspans[si];
16591
17326
  const tspanDec = (tspan.getAttribute("text-decoration") || "").toLowerCase();
16592
17327
  const tspanStyleDec = (getInlineStyleValue(tspan, "text-decoration") || "").toLowerCase();
16593
17328
  const hasUnderline = tspanDec.includes("underline") || tspanStyleDec.includes("underline") || textHasUnderline;
16594
- if (!hasUnderline) continue;
17329
+ const hasLinethrough = tspanDec.includes("line-through") || tspanStyleDec.includes("line-through") || textHasLinethrough;
17330
+ if (!hasUnderline && !hasLinethrough) continue;
16595
17331
  const content = tspan.textContent || "";
16596
17332
  if (!content.trim()) continue;
16597
17333
  const xAttr = tspan.getAttribute("x") ?? textEl.getAttribute("x") ?? "0";
@@ -16646,23 +17382,26 @@ async function convertTextDecorationsToLines(svg) {
16646
17382
  lineStartX = x - (textAnchor === "middle" ? textWidth / 2 : textAnchor === "end" ? textWidth : 0);
16647
17383
  lineEndX = lineStartX + textWidth;
16648
17384
  }
16649
- const underlineY = baselineY + fontSize * 0.15;
16650
17385
  const thickness = Math.max(0.5, fontSize * 0.066667);
16651
- const line = doc.createElementNS("http://www.w3.org/2000/svg", "line");
16652
- line.setAttribute("x1", String(lineStartX));
16653
- line.setAttribute("y1", String(underlineY));
16654
- line.setAttribute("x2", String(lineEndX));
16655
- line.setAttribute("y2", String(underlineY));
16656
- line.setAttribute("stroke", fill.startsWith("url(") ? "#000000" : fill);
16657
- line.setAttribute("stroke-width", String(thickness));
16658
- line.setAttribute("stroke-linecap", "butt");
16659
- line.setAttribute("fill", "none");
16660
- if (fillOpacity) line.setAttribute("stroke-opacity", fillOpacity);
16661
- if (textEl.parentElement) {
16662
- textEl.parentElement.insertBefore(line, textEl.nextSibling);
16663
- }
17386
+ const makeLine = (yPos) => {
17387
+ const line = doc.createElementNS("http://www.w3.org/2000/svg", "line");
17388
+ line.setAttribute("x1", String(lineStartX));
17389
+ line.setAttribute("y1", String(yPos));
17390
+ line.setAttribute("x2", String(lineEndX));
17391
+ line.setAttribute("y2", String(yPos));
17392
+ line.setAttribute("stroke", fill.startsWith("url(") ? "#000000" : fill);
17393
+ line.setAttribute("stroke-width", String(thickness));
17394
+ line.setAttribute("stroke-linecap", "butt");
17395
+ line.setAttribute("fill", "none");
17396
+ if (fillOpacity) line.setAttribute("stroke-opacity", fillOpacity);
17397
+ if (textEl.parentElement) {
17398
+ textEl.parentElement.insertBefore(line, textEl.nextSibling);
17399
+ }
17400
+ };
17401
+ if (hasUnderline) makeLine(baselineY + fontSize * 0.15);
17402
+ if (hasLinethrough) makeLine(baselineY - fontSize * 0.3);
16664
17403
  stripTextDecoration(tspan);
16665
- if (textHasUnderline) stripTextDecoration(textEl);
17404
+ if (textHasUnderline || textHasLinethrough) stripTextDecoration(textEl);
16666
17405
  }
16667
17406
  }
16668
17407
  if (tempContainer) {
@@ -16676,7 +17415,7 @@ async function convertSvgTextDecorationsToLinesString(svgStr) {
16676
17415
  if (typeof DOMParser === "undefined" || typeof XMLSerializer === "undefined") {
16677
17416
  return svgStr;
16678
17417
  }
16679
- if (!/text-decoration/i.test(svgStr) && !/underline/i.test(svgStr)) {
17418
+ if (!/text-decoration/i.test(svgStr) && !/underline/i.test(svgStr) && !/line-through/i.test(svgStr)) {
16680
17419
  return svgStr;
16681
17420
  }
16682
17421
  try {
@@ -16968,7 +17707,7 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
16968
17707
  const hasGradient = !!((_b = (_a = page.backgroundGradient) == null ? void 0 : _a.stops) == null ? void 0 : _b.length);
16969
17708
  drawPageBackground(pdf, i, page.width, page.height, page.backgroundColor, page.backgroundGradient);
16970
17709
  const shouldStripBg = stripPageBackground ?? hasGradient;
16971
- const textMode = options.textMode ?? (options.outlineText ? "pixel-perfect" : "selectable");
17710
+ const textMode = options.textMode ?? (options.outlineText === false ? "selectable" : "selectable");
16972
17711
  const shouldOutlineText = textMode === "pixel-perfect" || textMode === "auto";
16973
17712
  const outlineSubMode = textMode === "auto" ? "complex-only" : "all";
16974
17713
  let pageSvg = page.svg;
@@ -16982,7 +17721,7 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
16982
17721
  }
16983
17722
  if (shouldOutlineText) {
16984
17723
  try {
16985
- const { convertAllTextToPath } = await Promise.resolve().then(() => require("./svgTextToPath-CWlhIf-q.cjs"));
17724
+ const { convertAllTextToPath } = await Promise.resolve().then(() => require("./svgTextToPath-DTKsddnS.cjs"));
16986
17725
  pageSvg = await convertAllTextToPath(pageSvg, fontBaseUrl, { mode: outlineSubMode });
16987
17726
  try {
16988
17727
  dumpSvgTextDiagnostics(pageSvg, i, PARITY_TAG, "STAGE-1b-after-text-to-path-raw");