@pixldocs/canvas-renderer 0.5.457 → 0.5.459

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.
@@ -295,6 +295,172 @@ function findCommonAncestorGroup(selectedIds, pageChildren) {
295
295
  }
296
296
  return null;
297
297
  }
298
+ let activeThemeColors = {};
299
+ function setMarkdownThemeColors(c) {
300
+ activeThemeColors = { ...c };
301
+ }
302
+ function resolveColorToken(token, theme) {
303
+ const raw = token.trim();
304
+ const t = raw.toLowerCase();
305
+ if (t === "primary") return theme.primary;
306
+ if (t === "secondary") return theme.secondary;
307
+ if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
308
+ if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) return raw;
309
+ if (/^[a-z]+$/i.test(raw)) return raw;
310
+ return void 0;
311
+ }
312
+ function mergeStyle(a, b) {
313
+ return { ...a, ...b };
314
+ }
315
+ function tokenize(input, theme) {
316
+ const runs = [];
317
+ const stack = [];
318
+ let buf = "";
319
+ const activeStyle = () => {
320
+ let s = {};
321
+ for (const e of stack) s = mergeStyle(s, e.style);
322
+ return s;
323
+ };
324
+ const flush = () => {
325
+ if (buf.length === 0) return;
326
+ runs.push({ text: buf, style: activeStyle() });
327
+ buf = "";
328
+ };
329
+ let i = 0;
330
+ const n = input.length;
331
+ const peek = (s, at = i) => input.startsWith(s, at);
332
+ const findUnescaped = (needle, from) => {
333
+ let p = from;
334
+ while (p < n) {
335
+ if (input[p] === "\\" && p + 1 < n) {
336
+ p += 2;
337
+ continue;
338
+ }
339
+ if (input.startsWith(needle, p)) return p;
340
+ p++;
341
+ }
342
+ return -1;
343
+ };
344
+ const tryOpenBracket = () => {
345
+ if (input[i] !== "[") return -1;
346
+ const m = /^\[(c|bg)=([^\]]+)\]/.exec(input.slice(i));
347
+ if (!m) return -1;
348
+ const kind = m[1];
349
+ const tokenRaw = m[2];
350
+ const closer = kind === "c" ? "[/c]" : "[/bg]";
351
+ if (findUnescaped(closer, i + m[0].length) === -1) return -1;
352
+ const color = resolveColorToken(tokenRaw, theme);
353
+ const style = {};
354
+ if (color) {
355
+ if (kind === "c") style.fill = color;
356
+ else style.textBackgroundColor = color;
357
+ }
358
+ flush();
359
+ stack.push({ kind, style, closer });
360
+ return i + m[0].length;
361
+ };
362
+ const tryCloseBracket = () => {
363
+ for (let s = stack.length - 1; s >= 0; s--) {
364
+ const top = stack[s];
365
+ if (top.kind !== "c" && top.kind !== "bg") continue;
366
+ if (peek(top.closer)) {
367
+ if (s !== stack.length - 1) stack.length = s + 1;
368
+ flush();
369
+ stack.pop();
370
+ return i + top.closer.length;
371
+ }
372
+ break;
373
+ }
374
+ return -1;
375
+ };
376
+ const toggle = (delim, kind, style) => {
377
+ if (!peek(delim)) return null;
378
+ const topIdx = stack.findIndex((e) => e.kind === kind);
379
+ if (topIdx >= 0) {
380
+ if (topIdx !== stack.length - 1) return null;
381
+ flush();
382
+ stack.length = topIdx;
383
+ return i + delim.length;
384
+ }
385
+ if (findUnescaped(delim, i + delim.length) === -1) return null;
386
+ flush();
387
+ stack.push({ kind, style, closer: delim });
388
+ return i + delim.length;
389
+ };
390
+ while (i < n) {
391
+ const ch = input[i];
392
+ if (ch === "\\" && i + 1 < n) {
393
+ buf += input[i + 1];
394
+ i += 2;
395
+ continue;
396
+ }
397
+ if (ch === "[") {
398
+ const closed = tryCloseBracket();
399
+ if (closed > 0) {
400
+ i = closed;
401
+ continue;
402
+ }
403
+ const opened = tryOpenBracket();
404
+ if (opened > 0) {
405
+ i = opened;
406
+ continue;
407
+ }
408
+ }
409
+ let next;
410
+ if ((next = toggle("**", "bold", { fontWeight: 700 })) !== null) {
411
+ i = next;
412
+ continue;
413
+ }
414
+ if ((next = toggle("__", "under", { underline: true })) !== null) {
415
+ i = next;
416
+ continue;
417
+ }
418
+ if ((next = toggle("~~", "strike", { linethrough: true })) !== null) {
419
+ i = next;
420
+ continue;
421
+ }
422
+ if ((next = toggle("==", "highlight", { textBackgroundColor: theme.secondary || "#ffe066" })) !== null) {
423
+ i = next;
424
+ continue;
425
+ }
426
+ if ((next = toggle("*", "italic", { fontStyle: "italic" })) !== null) {
427
+ i = next;
428
+ continue;
429
+ }
430
+ buf += ch;
431
+ i++;
432
+ }
433
+ flush();
434
+ return runs;
435
+ }
436
+ function parseTextMarkdown(input, themeColors) {
437
+ const theme = activeThemeColors;
438
+ const runs = tokenize(input ?? "", theme);
439
+ let plain = "";
440
+ const styles = {};
441
+ let lineIdx = 0;
442
+ let charIdx = 0;
443
+ let hasFormatting = false;
444
+ for (const run of runs) {
445
+ const styleHasContent = Object.keys(run.style).length > 0;
446
+ if (styleHasContent) hasFormatting = true;
447
+ for (const ch of run.text) {
448
+ if (ch === "\n") {
449
+ plain += "\n";
450
+ lineIdx++;
451
+ charIdx = 0;
452
+ continue;
453
+ }
454
+ plain += ch;
455
+ if (styleHasContent) {
456
+ if (!styles[lineIdx]) styles[lineIdx] = {};
457
+ styles[lineIdx][charIdx] = { ...run.style };
458
+ }
459
+ charIdx++;
460
+ }
461
+ }
462
+ return { plainText: plain, styles, hasFormatting };
463
+ }
298
464
  const heightCache = /* @__PURE__ */ new Map();
299
465
  const CACHE_TTL = 5e3;
300
466
  const WIDTH_BUCKET_PX = 8;
@@ -351,10 +517,21 @@ function getTextboxWidthFitMetrics(textbox, targetWidth) {
351
517
  fitsWidth: maxLineWidth <= targetWidth + 1
352
518
  };
353
519
  }
520
+ function getRenderedTextForMeasurement(element) {
521
+ const raw = element.text ?? "";
522
+ if (!raw) return "";
523
+ if (element.formattingEnabled !== true) return raw;
524
+ try {
525
+ const { plainText } = parseTextMarkdown(raw);
526
+ return plainText || "";
527
+ } catch {
528
+ return raw;
529
+ }
530
+ }
354
531
  function getCacheKey(element) {
355
532
  const widthPx = typeof element.width === "number" ? element.width : 200;
356
533
  return JSON.stringify({
357
- text: element.text,
534
+ text: getRenderedTextForMeasurement(element),
358
535
  widthBucket: bucketWidth(widthPx),
359
536
  fontSize: element.fontSize,
360
537
  fontFamily: element.fontFamily,
@@ -378,7 +555,8 @@ function measureTextHeight(element) {
378
555
  if (element.type !== "text") {
379
556
  return element.height || 20;
380
557
  }
381
- const textToMeasure = element.text || " ";
558
+ const rendered = getRenderedTextForMeasurement(element);
559
+ const textToMeasure = rendered || " ";
382
560
  const cacheKey = getCacheKey(element);
383
561
  const cached = heightCache.get(cacheKey);
384
562
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
@@ -9312,225 +9490,59 @@ function buildRoundedRectPath$1(width, height, radii) {
9312
9490
  `L ${bl} ${height}`,
9313
9491
  bl > 0 ? `A ${bl} ${bl} 0 0 1 0 ${height - bl}` : `L 0 ${height}`,
9314
9492
  `L 0 ${tl}`,
9315
- tl > 0 ? `A ${tl} ${tl} 0 0 1 ${tl} 0` : `L 0 0`,
9316
- "Z"
9317
- ].join(" ");
9318
- }
9319
- function getTrianglePoints(x, y, width, height) {
9320
- return [
9321
- { x: x + width / 2, y },
9322
- { x: x + width, y: y + height },
9323
- { x, y: y + height }
9324
- ];
9325
- }
9326
- function buildRoundedTrianglePath(w, h, rTop, rBR, rBL) {
9327
- const A = { x: w / 2, y: 0 };
9328
- const B = { x: w, y: h };
9329
- const C = { x: 0, y: h };
9330
- const maxR = Math.min(w, h) / 2;
9331
- const rt = Math.min(Math.max(0, rTop), maxR);
9332
- const rbr = Math.min(Math.max(0, rBR), maxR);
9333
- const rbl = Math.min(Math.max(0, rBL), maxR);
9334
- const unitVec = (p1, p2) => {
9335
- const dx = p2.x - p1.x;
9336
- const dy = p2.y - p1.y;
9337
- const len = Math.sqrt(dx * dx + dy * dy);
9338
- return len > 0 ? { x: dx / len, y: dy / len } : { x: 0, y: 0 };
9339
- };
9340
- const cornerSegment = (prev, curr, next, r) => {
9341
- if (r <= 0) {
9342
- return `L ${curr.x} ${curr.y}`;
9343
- }
9344
- const uPrev = unitVec(curr, prev);
9345
- const uNext = unitVec(curr, next);
9346
- const startX2 = curr.x + uPrev.x * r;
9347
- const startY2 = curr.y + uPrev.y * r;
9348
- const endX = curr.x + uNext.x * r;
9349
- const endY = curr.y + uNext.y * r;
9350
- return `L ${startX2} ${startY2} Q ${curr.x} ${curr.y} ${endX} ${endY}`;
9351
- };
9352
- const uCA = unitVec(C, A);
9353
- const startX = C.x + uCA.x * (rbl > 0 ? rbl : 0);
9354
- const startY = C.y + uCA.y * (rbl > 0 ? rbl : 0);
9355
- const actualStartX = rbl > 0 ? startX : C.x;
9356
- const actualStartY = rbl > 0 ? startY : C.y;
9357
- const parts = [
9358
- `M ${actualStartX} ${actualStartY}`,
9359
- cornerSegment(C, A, B, rt),
9360
- // top corner
9361
- cornerSegment(A, B, C, rbr),
9362
- // bottom-right corner
9363
- cornerSegment(B, C, A, rbl),
9364
- // bottom-left corner
9365
- "Z"
9366
- ];
9367
- return parts.join(" ");
9368
- }
9369
- let activeThemeColors = {};
9370
- function setMarkdownThemeColors(c) {
9371
- activeThemeColors = { ...c };
9372
- }
9373
- function resolveColorToken(token, theme) {
9374
- const raw = token.trim();
9375
- const t = raw.toLowerCase();
9376
- if (t === "primary") return theme.primary;
9377
- if (t === "secondary") return theme.secondary;
9378
- if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
9379
- if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) return raw;
9380
- if (/^[a-z]+$/i.test(raw)) return raw;
9381
- return void 0;
9382
- }
9383
- function mergeStyle(a, b) {
9384
- return { ...a, ...b };
9385
- }
9386
- function tokenize(input, theme) {
9387
- const runs = [];
9388
- const stack = [];
9389
- let buf = "";
9390
- const activeStyle = () => {
9391
- let s = {};
9392
- for (const e of stack) s = mergeStyle(s, e.style);
9393
- return s;
9394
- };
9395
- const flush = () => {
9396
- if (buf.length === 0) return;
9397
- runs.push({ text: buf, style: activeStyle() });
9398
- buf = "";
9399
- };
9400
- let i = 0;
9401
- const n = input.length;
9402
- const peek = (s, at = i) => input.startsWith(s, at);
9403
- const findUnescaped = (needle, from) => {
9404
- let p = from;
9405
- while (p < n) {
9406
- if (input[p] === "\\" && p + 1 < n) {
9407
- p += 2;
9408
- continue;
9409
- }
9410
- if (input.startsWith(needle, p)) return p;
9411
- p++;
9412
- }
9413
- return -1;
9414
- };
9415
- const tryOpenBracket = () => {
9416
- if (input[i] !== "[") return -1;
9417
- const m = /^\[(c|bg)=([^\]]+)\]/.exec(input.slice(i));
9418
- if (!m) return -1;
9419
- const kind = m[1];
9420
- const tokenRaw = m[2];
9421
- const closer = kind === "c" ? "[/c]" : "[/bg]";
9422
- if (findUnescaped(closer, i + m[0].length) === -1) return -1;
9423
- const color = resolveColorToken(tokenRaw, theme);
9424
- const style = {};
9425
- if (color) {
9426
- if (kind === "c") style.fill = color;
9427
- else style.textBackgroundColor = color;
9428
- }
9429
- flush();
9430
- stack.push({ kind, style, closer });
9431
- return i + m[0].length;
9432
- };
9433
- const tryCloseBracket = () => {
9434
- for (let s = stack.length - 1; s >= 0; s--) {
9435
- const top = stack[s];
9436
- if (top.kind !== "c" && top.kind !== "bg") continue;
9437
- if (peek(top.closer)) {
9438
- if (s !== stack.length - 1) stack.length = s + 1;
9439
- flush();
9440
- stack.pop();
9441
- return i + top.closer.length;
9442
- }
9443
- break;
9444
- }
9445
- return -1;
9446
- };
9447
- const toggle = (delim, kind, style) => {
9448
- if (!peek(delim)) return null;
9449
- const topIdx = stack.findIndex((e) => e.kind === kind);
9450
- if (topIdx >= 0) {
9451
- if (topIdx !== stack.length - 1) return null;
9452
- flush();
9453
- stack.length = topIdx;
9454
- return i + delim.length;
9455
- }
9456
- if (findUnescaped(delim, i + delim.length) === -1) return null;
9457
- flush();
9458
- stack.push({ kind, style, closer: delim });
9459
- return i + delim.length;
9460
- };
9461
- while (i < n) {
9462
- const ch = input[i];
9463
- if (ch === "\\" && i + 1 < n) {
9464
- buf += input[i + 1];
9465
- i += 2;
9466
- continue;
9467
- }
9468
- if (ch === "[") {
9469
- const closed = tryCloseBracket();
9470
- if (closed > 0) {
9471
- i = closed;
9472
- continue;
9473
- }
9474
- const opened = tryOpenBracket();
9475
- if (opened > 0) {
9476
- i = opened;
9477
- continue;
9478
- }
9479
- }
9480
- let next;
9481
- if ((next = toggle("**", "bold", { fontWeight: 700 })) !== null) {
9482
- i = next;
9483
- continue;
9484
- }
9485
- if ((next = toggle("__", "under", { underline: true })) !== null) {
9486
- i = next;
9487
- continue;
9488
- }
9489
- if ((next = toggle("~~", "strike", { linethrough: true })) !== null) {
9490
- i = next;
9491
- continue;
9492
- }
9493
- if ((next = toggle("==", "highlight", { textBackgroundColor: theme.secondary || "#ffe066" })) !== null) {
9494
- i = next;
9495
- continue;
9496
- }
9497
- if ((next = toggle("*", "italic", { fontStyle: "italic" })) !== null) {
9498
- i = next;
9499
- continue;
9500
- }
9501
- buf += ch;
9502
- i++;
9503
- }
9504
- flush();
9505
- return runs;
9506
- }
9507
- function parseTextMarkdown(input, themeColors) {
9508
- const theme = activeThemeColors;
9509
- const runs = tokenize(input ?? "", theme);
9510
- let plain = "";
9511
- const styles = {};
9512
- let lineIdx = 0;
9513
- let charIdx = 0;
9514
- let hasFormatting = false;
9515
- for (const run of runs) {
9516
- const styleHasContent = Object.keys(run.style).length > 0;
9517
- if (styleHasContent) hasFormatting = true;
9518
- for (const ch of run.text) {
9519
- if (ch === "\n") {
9520
- plain += "\n";
9521
- lineIdx++;
9522
- charIdx = 0;
9523
- continue;
9524
- }
9525
- plain += ch;
9526
- if (styleHasContent) {
9527
- if (!styles[lineIdx]) styles[lineIdx] = {};
9528
- styles[lineIdx][charIdx] = { ...run.style };
9529
- }
9530
- charIdx++;
9493
+ tl > 0 ? `A ${tl} ${tl} 0 0 1 ${tl} 0` : `L 0 0`,
9494
+ "Z"
9495
+ ].join(" ");
9496
+ }
9497
+ function getTrianglePoints(x, y, width, height) {
9498
+ return [
9499
+ { x: x + width / 2, y },
9500
+ { x: x + width, y: y + height },
9501
+ { x, y: y + height }
9502
+ ];
9503
+ }
9504
+ function buildRoundedTrianglePath(w, h, rTop, rBR, rBL) {
9505
+ const A = { x: w / 2, y: 0 };
9506
+ const B = { x: w, y: h };
9507
+ const C = { x: 0, y: h };
9508
+ const maxR = Math.min(w, h) / 2;
9509
+ const rt = Math.min(Math.max(0, rTop), maxR);
9510
+ const rbr = Math.min(Math.max(0, rBR), maxR);
9511
+ const rbl = Math.min(Math.max(0, rBL), maxR);
9512
+ const unitVec = (p1, p2) => {
9513
+ const dx = p2.x - p1.x;
9514
+ const dy = p2.y - p1.y;
9515
+ const len = Math.sqrt(dx * dx + dy * dy);
9516
+ return len > 0 ? { x: dx / len, y: dy / len } : { x: 0, y: 0 };
9517
+ };
9518
+ const cornerSegment = (prev, curr, next, r) => {
9519
+ if (r <= 0) {
9520
+ return `L ${curr.x} ${curr.y}`;
9531
9521
  }
9532
- }
9533
- return { plainText: plain, styles, hasFormatting };
9522
+ const uPrev = unitVec(curr, prev);
9523
+ const uNext = unitVec(curr, next);
9524
+ const startX2 = curr.x + uPrev.x * r;
9525
+ const startY2 = curr.y + uPrev.y * r;
9526
+ const endX = curr.x + uNext.x * r;
9527
+ const endY = curr.y + uNext.y * r;
9528
+ return `L ${startX2} ${startY2} Q ${curr.x} ${curr.y} ${endX} ${endY}`;
9529
+ };
9530
+ const uCA = unitVec(C, A);
9531
+ const startX = C.x + uCA.x * (rbl > 0 ? rbl : 0);
9532
+ const startY = C.y + uCA.y * (rbl > 0 ? rbl : 0);
9533
+ const actualStartX = rbl > 0 ? startX : C.x;
9534
+ const actualStartY = rbl > 0 ? startY : C.y;
9535
+ const parts = [
9536
+ `M ${actualStartX} ${actualStartY}`,
9537
+ cornerSegment(C, A, B, rt),
9538
+ // top corner
9539
+ cornerSegment(A, B, C, rbr),
9540
+ // bottom-right corner
9541
+ cornerSegment(B, C, A, rbl),
9542
+ // bottom-left corner
9543
+ "Z"
9544
+ ];
9545
+ return parts.join(" ");
9534
9546
  }
9535
9547
  function angleToCoords(angleDeg) {
9536
9548
  const rad = angleDeg * Math.PI / 180;
@@ -21133,9 +21145,14 @@ function setInTree(nodes, elementId, targetProperty, value) {
21133
21145
  }
21134
21146
  if (targetProperty === "text" && node.type === "text") {
21135
21147
  const overflowPolicy = String(node.overflowPolicy ?? "grow-and-push");
21136
- if (overflowPolicy !== "auto-shrink") {
21137
- delete node.height;
21148
+ if (overflowPolicy === "auto-shrink") {
21149
+ const explicitH = typeof node.height === "number" ? node.height : 0;
21150
+ const existingMin = Math.max(0, Number(node.minBoxHeight) || 0);
21151
+ if (explicitH > 0 && existingMin <= 0) {
21152
+ node.minBoxHeight = explicitH;
21153
+ }
21138
21154
  }
21155
+ delete node.height;
21139
21156
  }
21140
21157
  return true;
21141
21158
  }
@@ -25236,6 +25253,89 @@ const previewBlur = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineP
25236
25253
  injectPreviewBlur,
25237
25254
  resolveBlurElementExactIdsFromFlatFormKeys
25238
25255
  }, Symbol.toStringTag, { value: "Module" }));
25256
+ function collectImageUrls(config) {
25257
+ const urls = [];
25258
+ const walk = (nodes) => {
25259
+ for (const node of nodes) {
25260
+ if (!node || node.visible === false) continue;
25261
+ const src = typeof node.src === "string" ? node.src.trim() : "";
25262
+ const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
25263
+ if (node.type === "image") {
25264
+ const url = src || imageUrl;
25265
+ if (url) urls.push(url);
25266
+ }
25267
+ if (Array.isArray(node.children) && node.children.length > 0) {
25268
+ walk(node.children);
25269
+ }
25270
+ }
25271
+ };
25272
+ for (const page of config.pages || []) {
25273
+ walk(page.children || []);
25274
+ }
25275
+ return urls;
25276
+ }
25277
+ function normalizeAssetUrl(rawUrl, imageProxyUrl) {
25278
+ if (!rawUrl) return null;
25279
+ if (rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) return null;
25280
+ if (rawUrl.startsWith("/") && !rawUrl.startsWith("//")) {
25281
+ if (typeof window !== "undefined") return new URL(rawUrl, window.location.origin).toString();
25282
+ return null;
25283
+ }
25284
+ try {
25285
+ const h = new URL(rawUrl).hostname.toLowerCase();
25286
+ if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(h)) {
25287
+ if (typeof window !== "undefined" && new URL(rawUrl).origin === window.location.origin) {
25288
+ return rawUrl;
25289
+ }
25290
+ return null;
25291
+ }
25292
+ } catch {
25293
+ return null;
25294
+ }
25295
+ const supabaseUrl = typeof globalThis.__VITE_SUPABASE_URL === "string" ? globalThis.__VITE_SUPABASE_URL : "";
25296
+ if (supabaseUrl && rawUrl.includes(supabaseUrl)) {
25297
+ const signedMatch = rawUrl.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
25298
+ if (signedMatch) return `${supabaseUrl}/storage/v1/object/public/${signedMatch[1]}`;
25299
+ if (rawUrl.includes("/storage/v1/object/public/")) return rawUrl;
25300
+ }
25301
+ const proxyBase = imageProxyUrl ? imageProxyUrl.replace(/\/image-proxy(?:\?.*)?$/, "") : exports.API_URL;
25302
+ if (proxyBase) {
25303
+ return `${proxyBase}/image-proxy?url=${encodeURIComponent(rawUrl)}`;
25304
+ }
25305
+ return rawUrl;
25306
+ }
25307
+ const CONCURRENCY = 6;
25308
+ async function prefetchUrls(urls, signal) {
25309
+ const unique = [...new Set(urls)];
25310
+ if (unique.length === 0) return;
25311
+ let i = 0;
25312
+ const next = async () => {
25313
+ while (i < unique.length) {
25314
+ if (signal == null ? void 0 : signal.aborted) return;
25315
+ const url = unique[i++];
25316
+ try {
25317
+ await fetch(url, { signal, mode: "cors", credentials: "omit" });
25318
+ } catch {
25319
+ }
25320
+ }
25321
+ };
25322
+ const workers = Array.from({ length: Math.min(CONCURRENCY, unique.length) }, () => next());
25323
+ await Promise.all(workers);
25324
+ }
25325
+ async function warmResolvedTemplateForPreview(config, options) {
25326
+ const { signal, imageProxyUrl } = options ?? {};
25327
+ await ensureFontsForResolvedConfig(config);
25328
+ if (signal == null ? void 0 : signal.aborted) return;
25329
+ const rawUrls = collectImageUrls(config);
25330
+ const resolvedUrls = rawUrls.map((u) => normalizeAssetUrl(u, imageProxyUrl)).filter((u) => u !== null);
25331
+ await prefetchUrls(resolvedUrls, signal);
25332
+ }
25333
+ async function warmTemplateFromForm(options) {
25334
+ const { signal, imageProxyUrl, ...resolveOpts } = options;
25335
+ const resolved = await resolveFromForm(resolveOpts);
25336
+ if (signal == null ? void 0 : signal.aborted) return;
25337
+ await warmResolvedTemplateForPreview(resolved.config, { signal, imageProxyUrl });
25338
+ }
25239
25339
  const PREVIEW_DEBUG_PREFIX = "[canvas-renderer][preview-debug]";
25240
25340
  function computeFontSignature(config) {
25241
25341
  var _a2;
@@ -25251,6 +25351,39 @@ function computeFontSignature(config) {
25251
25351
  for (const page of config.pages) walk(page.children || []);
25252
25352
  return Array.from(fams).sort().join("|");
25253
25353
  }
25354
+ function computeImageSignature(config) {
25355
+ var _a2;
25356
+ if (!((_a2 = config == null ? void 0 : config.pages) == null ? void 0 : _a2.length)) return "";
25357
+ try {
25358
+ const urls = collectImageUrls(config);
25359
+ return urls.length === 0 ? "" : urls.slice().sort().join("|");
25360
+ } catch {
25361
+ return "";
25362
+ }
25363
+ }
25364
+ function preloadImageUrl(url, proxyBase) {
25365
+ return new Promise((resolve) => {
25366
+ if (!url || url.startsWith("data:") || url.startsWith("blob:")) return resolve();
25367
+ let done = false;
25368
+ const finish = () => {
25369
+ if (!done) {
25370
+ done = true;
25371
+ resolve();
25372
+ }
25373
+ };
25374
+ const isHttp = /^https?:/i.test(url);
25375
+ const target = isHttp && proxyBase && !url.includes("/image-proxy?") ? `${proxyBase.replace(/\/+$/, "")}?url=${encodeURIComponent(url)}` : url;
25376
+ const img = new Image();
25377
+ try {
25378
+ img.crossOrigin = "anonymous";
25379
+ } catch {
25380
+ }
25381
+ img.onload = finish;
25382
+ img.onerror = finish;
25383
+ img.src = target;
25384
+ setTimeout(finish, 6e3);
25385
+ });
25386
+ }
25254
25387
  function countUnderlinedNodes(config) {
25255
25388
  var _a2;
25256
25389
  if (!((_a2 = config == null ? void 0 : config.pages) == null ? void 0 : _a2.length)) return 0;
@@ -25413,6 +25546,38 @@ function PixldocsPreview(props) {
25413
25546
  const config = isResolveMode ? resolvedConfig : props.config;
25414
25547
  const previewKey = react.useMemo(() => `${pageIndex}`, [pageIndex]);
25415
25548
  const fontSignature = react.useMemo(() => computeFontSignature(config), [config]);
25549
+ const imageSignature = react.useMemo(() => computeImageSignature(config), [config]);
25550
+ const [imagesReady, setImagesReady] = react.useState(true);
25551
+ const firstImageRef = react.useMemo(() => ({ first: true }), []);
25552
+ react.useEffect(() => {
25553
+ if (!config) {
25554
+ setImagesReady(true);
25555
+ return;
25556
+ }
25557
+ if (firstImageRef.first) {
25558
+ firstImageRef.first = false;
25559
+ setImagesReady(true);
25560
+ return;
25561
+ }
25562
+ let urls = [];
25563
+ try {
25564
+ urls = collectImageUrls(config);
25565
+ } catch {
25566
+ urls = [];
25567
+ }
25568
+ if (urls.length === 0) {
25569
+ setImagesReady(true);
25570
+ return;
25571
+ }
25572
+ setImagesReady(false);
25573
+ let cancelled = false;
25574
+ Promise.all(urls.map((u) => preloadImageUrl(u, imageProxyUrl))).then(() => {
25575
+ if (!cancelled) setImagesReady(true);
25576
+ });
25577
+ return () => {
25578
+ cancelled = true;
25579
+ };
25580
+ }, [imageSignature, imageProxyUrl]);
25416
25581
  react.useEffect(() => {
25417
25582
  if (isResolveMode) return;
25418
25583
  if (!config) {
@@ -25495,7 +25660,7 @@ function PixldocsPreview(props) {
25495
25660
  /* @__PURE__ */ jsxRuntime.jsxs(
25496
25661
  "div",
25497
25662
  {
25498
- style: hasOverlays ? { visibility: canvasSettled ? "visible" : "hidden", position: "relative", width: canvasW * zoom, height: canvasH * zoom } : { visibility: canvasSettled ? "visible" : "hidden" },
25663
+ style: hasOverlays ? { visibility: canvasSettled && imagesReady ? "visible" : "hidden", position: "relative", width: canvasW * zoom, height: canvasH * zoom } : { visibility: canvasSettled && imagesReady ? "visible" : "hidden" },
25499
25664
  children: [
25500
25665
  /* @__PURE__ */ jsxRuntime.jsx(
25501
25666
  PreviewCanvas,
@@ -25541,7 +25706,7 @@ function PixldocsPreview(props) {
25541
25706
  ]
25542
25707
  }
25543
25708
  ),
25544
- !canvasSettled && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: loadingFallback ?? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
25709
+ (!canvasSettled || !imagesReady) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: loadingFallback ?? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
25545
25710
  ] });
25546
25711
  }
25547
25712
  function normalizeSvgDimensions(svg, targetWidth, targetHeight) {
@@ -26062,9 +26227,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
26062
26227
  }
26063
26228
  return svgString;
26064
26229
  }
26065
- const resolvedPackageVersion = "0.5.457";
26230
+ const resolvedPackageVersion = "0.5.459";
26066
26231
  const PACKAGE_VERSION = resolvedPackageVersion;
26067
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.457";
26232
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.459";
26068
26233
  const roundParityValue = (value) => {
26069
26234
  if (typeof value !== "number") return value;
26070
26235
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -26878,7 +27043,7 @@ class PixldocsRenderer {
26878
27043
  await this.waitForCanvasScene(container, cloned, i);
26879
27044
  }
26880
27045
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
26881
- const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-Qi65Srzj.cjs"));
27046
+ const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-CpKSMd8E.cjs"));
26882
27047
  const prepared = preparePagesForExport(
26883
27048
  cloned.pages,
26884
27049
  canvasWidth,
@@ -29198,7 +29363,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
29198
29363
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
29199
29364
  sanitizeSvgTreeForPdf(svgToDraw);
29200
29365
  try {
29201
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-Qi65Srzj.cjs"));
29366
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-CpKSMd8E.cjs"));
29202
29367
  try {
29203
29368
  await logTextMeasurementDiagnostic(svgToDraw);
29204
29369
  } catch {
@@ -29446,89 +29611,6 @@ async function getPublishedTemplate(options) {
29446
29611
  const rows = await res.json();
29447
29612
  return rows[0] ?? null;
29448
29613
  }
29449
- function collectImageUrls(config) {
29450
- const urls = [];
29451
- const walk = (nodes) => {
29452
- for (const node of nodes) {
29453
- if (!node || node.visible === false) continue;
29454
- const src = typeof node.src === "string" ? node.src.trim() : "";
29455
- const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
29456
- if (node.type === "image") {
29457
- const url = src || imageUrl;
29458
- if (url) urls.push(url);
29459
- }
29460
- if (Array.isArray(node.children) && node.children.length > 0) {
29461
- walk(node.children);
29462
- }
29463
- }
29464
- };
29465
- for (const page of config.pages || []) {
29466
- walk(page.children || []);
29467
- }
29468
- return urls;
29469
- }
29470
- function normalizeAssetUrl(rawUrl, imageProxyUrl) {
29471
- if (!rawUrl) return null;
29472
- if (rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) return null;
29473
- if (rawUrl.startsWith("/") && !rawUrl.startsWith("//")) {
29474
- if (typeof window !== "undefined") return new URL(rawUrl, window.location.origin).toString();
29475
- return null;
29476
- }
29477
- try {
29478
- const h = new URL(rawUrl).hostname.toLowerCase();
29479
- if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(h)) {
29480
- if (typeof window !== "undefined" && new URL(rawUrl).origin === window.location.origin) {
29481
- return rawUrl;
29482
- }
29483
- return null;
29484
- }
29485
- } catch {
29486
- return null;
29487
- }
29488
- const supabaseUrl = typeof globalThis.__VITE_SUPABASE_URL === "string" ? globalThis.__VITE_SUPABASE_URL : "";
29489
- if (supabaseUrl && rawUrl.includes(supabaseUrl)) {
29490
- const signedMatch = rawUrl.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
29491
- if (signedMatch) return `${supabaseUrl}/storage/v1/object/public/${signedMatch[1]}`;
29492
- if (rawUrl.includes("/storage/v1/object/public/")) return rawUrl;
29493
- }
29494
- const proxyBase = imageProxyUrl ? imageProxyUrl.replace(/\/image-proxy(?:\?.*)?$/, "") : exports.API_URL;
29495
- if (proxyBase) {
29496
- return `${proxyBase}/image-proxy?url=${encodeURIComponent(rawUrl)}`;
29497
- }
29498
- return rawUrl;
29499
- }
29500
- const CONCURRENCY = 6;
29501
- async function prefetchUrls(urls, signal) {
29502
- const unique = [...new Set(urls)];
29503
- if (unique.length === 0) return;
29504
- let i = 0;
29505
- const next = async () => {
29506
- while (i < unique.length) {
29507
- if (signal == null ? void 0 : signal.aborted) return;
29508
- const url = unique[i++];
29509
- try {
29510
- await fetch(url, { signal, mode: "cors", credentials: "omit" });
29511
- } catch {
29512
- }
29513
- }
29514
- };
29515
- const workers = Array.from({ length: Math.min(CONCURRENCY, unique.length) }, () => next());
29516
- await Promise.all(workers);
29517
- }
29518
- async function warmResolvedTemplateForPreview(config, options) {
29519
- const { signal, imageProxyUrl } = options ?? {};
29520
- await ensureFontsForResolvedConfig(config);
29521
- if (signal == null ? void 0 : signal.aborted) return;
29522
- const rawUrls = collectImageUrls(config);
29523
- const resolvedUrls = rawUrls.map((u) => normalizeAssetUrl(u, imageProxyUrl)).filter((u) => u !== null);
29524
- await prefetchUrls(resolvedUrls, signal);
29525
- }
29526
- async function warmTemplateFromForm(options) {
29527
- const { signal, imageProxyUrl, ...resolveOpts } = options;
29528
- const resolved = await resolveFromForm(resolveOpts);
29529
- if (signal == null ? void 0 : signal.aborted) return;
29530
- await warmResolvedTemplateForPreview(resolved.config, { signal, imageProxyUrl });
29531
- }
29532
29614
  function setAutoShrinkDebug(enabled) {
29533
29615
  if (typeof window !== "undefined") {
29534
29616
  window.__pixldocsDebugAutoShrink = !!enabled;
@@ -29595,4 +29677,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
29595
29677
  exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
29596
29678
  exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
29597
29679
  exports.warmTemplateFromForm = warmTemplateFromForm;
29598
- //# sourceMappingURL=index-C-zHLTyR.cjs.map
29680
+ //# sourceMappingURL=index-HqxKxsb3.cjs.map