@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.
@@ -277,6 +277,172 @@ function findCommonAncestorGroup(selectedIds, pageChildren) {
277
277
  }
278
278
  return null;
279
279
  }
280
+ let activeThemeColors = {};
281
+ function setMarkdownThemeColors(c) {
282
+ activeThemeColors = { ...c };
283
+ }
284
+ function resolveColorToken(token, theme) {
285
+ const raw = token.trim();
286
+ const t = raw.toLowerCase();
287
+ if (t === "primary") return theme.primary;
288
+ if (t === "secondary") return theme.secondary;
289
+ if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
290
+ if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) return raw;
291
+ if (/^[a-z]+$/i.test(raw)) return raw;
292
+ return void 0;
293
+ }
294
+ function mergeStyle(a, b) {
295
+ return { ...a, ...b };
296
+ }
297
+ function tokenize(input, theme) {
298
+ const runs = [];
299
+ const stack = [];
300
+ let buf = "";
301
+ const activeStyle = () => {
302
+ let s = {};
303
+ for (const e of stack) s = mergeStyle(s, e.style);
304
+ return s;
305
+ };
306
+ const flush = () => {
307
+ if (buf.length === 0) return;
308
+ runs.push({ text: buf, style: activeStyle() });
309
+ buf = "";
310
+ };
311
+ let i = 0;
312
+ const n = input.length;
313
+ const peek = (s, at = i) => input.startsWith(s, at);
314
+ const findUnescaped = (needle, from) => {
315
+ let p = from;
316
+ while (p < n) {
317
+ if (input[p] === "\\" && p + 1 < n) {
318
+ p += 2;
319
+ continue;
320
+ }
321
+ if (input.startsWith(needle, p)) return p;
322
+ p++;
323
+ }
324
+ return -1;
325
+ };
326
+ const tryOpenBracket = () => {
327
+ if (input[i] !== "[") return -1;
328
+ const m = /^\[(c|bg)=([^\]]+)\]/.exec(input.slice(i));
329
+ if (!m) return -1;
330
+ const kind = m[1];
331
+ const tokenRaw = m[2];
332
+ const closer = kind === "c" ? "[/c]" : "[/bg]";
333
+ if (findUnescaped(closer, i + m[0].length) === -1) return -1;
334
+ const color = resolveColorToken(tokenRaw, theme);
335
+ const style = {};
336
+ if (color) {
337
+ if (kind === "c") style.fill = color;
338
+ else style.textBackgroundColor = color;
339
+ }
340
+ flush();
341
+ stack.push({ kind, style, closer });
342
+ return i + m[0].length;
343
+ };
344
+ const tryCloseBracket = () => {
345
+ for (let s = stack.length - 1; s >= 0; s--) {
346
+ const top = stack[s];
347
+ if (top.kind !== "c" && top.kind !== "bg") continue;
348
+ if (peek(top.closer)) {
349
+ if (s !== stack.length - 1) stack.length = s + 1;
350
+ flush();
351
+ stack.pop();
352
+ return i + top.closer.length;
353
+ }
354
+ break;
355
+ }
356
+ return -1;
357
+ };
358
+ const toggle = (delim, kind, style) => {
359
+ if (!peek(delim)) return null;
360
+ const topIdx = stack.findIndex((e) => e.kind === kind);
361
+ if (topIdx >= 0) {
362
+ if (topIdx !== stack.length - 1) return null;
363
+ flush();
364
+ stack.length = topIdx;
365
+ return i + delim.length;
366
+ }
367
+ if (findUnescaped(delim, i + delim.length) === -1) return null;
368
+ flush();
369
+ stack.push({ kind, style, closer: delim });
370
+ return i + delim.length;
371
+ };
372
+ while (i < n) {
373
+ const ch = input[i];
374
+ if (ch === "\\" && i + 1 < n) {
375
+ buf += input[i + 1];
376
+ i += 2;
377
+ continue;
378
+ }
379
+ if (ch === "[") {
380
+ const closed = tryCloseBracket();
381
+ if (closed > 0) {
382
+ i = closed;
383
+ continue;
384
+ }
385
+ const opened = tryOpenBracket();
386
+ if (opened > 0) {
387
+ i = opened;
388
+ continue;
389
+ }
390
+ }
391
+ let next;
392
+ if ((next = toggle("**", "bold", { fontWeight: 700 })) !== null) {
393
+ i = next;
394
+ continue;
395
+ }
396
+ if ((next = toggle("__", "under", { underline: true })) !== null) {
397
+ i = next;
398
+ continue;
399
+ }
400
+ if ((next = toggle("~~", "strike", { linethrough: true })) !== null) {
401
+ i = next;
402
+ continue;
403
+ }
404
+ if ((next = toggle("==", "highlight", { textBackgroundColor: theme.secondary || "#ffe066" })) !== null) {
405
+ i = next;
406
+ continue;
407
+ }
408
+ if ((next = toggle("*", "italic", { fontStyle: "italic" })) !== null) {
409
+ i = next;
410
+ continue;
411
+ }
412
+ buf += ch;
413
+ i++;
414
+ }
415
+ flush();
416
+ return runs;
417
+ }
418
+ function parseTextMarkdown(input, themeColors) {
419
+ const theme = activeThemeColors;
420
+ const runs = tokenize(input ?? "", theme);
421
+ let plain = "";
422
+ const styles = {};
423
+ let lineIdx = 0;
424
+ let charIdx = 0;
425
+ let hasFormatting = false;
426
+ for (const run of runs) {
427
+ const styleHasContent = Object.keys(run.style).length > 0;
428
+ if (styleHasContent) hasFormatting = true;
429
+ for (const ch of run.text) {
430
+ if (ch === "\n") {
431
+ plain += "\n";
432
+ lineIdx++;
433
+ charIdx = 0;
434
+ continue;
435
+ }
436
+ plain += ch;
437
+ if (styleHasContent) {
438
+ if (!styles[lineIdx]) styles[lineIdx] = {};
439
+ styles[lineIdx][charIdx] = { ...run.style };
440
+ }
441
+ charIdx++;
442
+ }
443
+ }
444
+ return { plainText: plain, styles, hasFormatting };
445
+ }
280
446
  const heightCache = /* @__PURE__ */ new Map();
281
447
  const CACHE_TTL = 5e3;
282
448
  const WIDTH_BUCKET_PX = 8;
@@ -333,10 +499,21 @@ function getTextboxWidthFitMetrics(textbox, targetWidth) {
333
499
  fitsWidth: maxLineWidth <= targetWidth + 1
334
500
  };
335
501
  }
502
+ function getRenderedTextForMeasurement(element) {
503
+ const raw = element.text ?? "";
504
+ if (!raw) return "";
505
+ if (element.formattingEnabled !== true) return raw;
506
+ try {
507
+ const { plainText } = parseTextMarkdown(raw);
508
+ return plainText || "";
509
+ } catch {
510
+ return raw;
511
+ }
512
+ }
336
513
  function getCacheKey(element) {
337
514
  const widthPx = typeof element.width === "number" ? element.width : 200;
338
515
  return JSON.stringify({
339
- text: element.text,
516
+ text: getRenderedTextForMeasurement(element),
340
517
  widthBucket: bucketWidth(widthPx),
341
518
  fontSize: element.fontSize,
342
519
  fontFamily: element.fontFamily,
@@ -360,7 +537,8 @@ function measureTextHeight(element) {
360
537
  if (element.type !== "text") {
361
538
  return element.height || 20;
362
539
  }
363
- const textToMeasure = element.text || " ";
540
+ const rendered = getRenderedTextForMeasurement(element);
541
+ const textToMeasure = rendered || " ";
364
542
  const cacheKey = getCacheKey(element);
365
543
  const cached = heightCache.get(cacheKey);
366
544
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
@@ -9294,225 +9472,59 @@ function buildRoundedRectPath$1(width, height, radii) {
9294
9472
  `L ${bl} ${height}`,
9295
9473
  bl > 0 ? `A ${bl} ${bl} 0 0 1 0 ${height - bl}` : `L 0 ${height}`,
9296
9474
  `L 0 ${tl}`,
9297
- tl > 0 ? `A ${tl} ${tl} 0 0 1 ${tl} 0` : `L 0 0`,
9298
- "Z"
9299
- ].join(" ");
9300
- }
9301
- function getTrianglePoints(x, y, width, height) {
9302
- return [
9303
- { x: x + width / 2, y },
9304
- { x: x + width, y: y + height },
9305
- { x, y: y + height }
9306
- ];
9307
- }
9308
- function buildRoundedTrianglePath(w, h, rTop, rBR, rBL) {
9309
- const A = { x: w / 2, y: 0 };
9310
- const B = { x: w, y: h };
9311
- const C = { x: 0, y: h };
9312
- const maxR = Math.min(w, h) / 2;
9313
- const rt = Math.min(Math.max(0, rTop), maxR);
9314
- const rbr = Math.min(Math.max(0, rBR), maxR);
9315
- const rbl = Math.min(Math.max(0, rBL), maxR);
9316
- const unitVec = (p1, p2) => {
9317
- const dx = p2.x - p1.x;
9318
- const dy = p2.y - p1.y;
9319
- const len = Math.sqrt(dx * dx + dy * dy);
9320
- return len > 0 ? { x: dx / len, y: dy / len } : { x: 0, y: 0 };
9321
- };
9322
- const cornerSegment = (prev, curr, next, r) => {
9323
- if (r <= 0) {
9324
- return `L ${curr.x} ${curr.y}`;
9325
- }
9326
- const uPrev = unitVec(curr, prev);
9327
- const uNext = unitVec(curr, next);
9328
- const startX2 = curr.x + uPrev.x * r;
9329
- const startY2 = curr.y + uPrev.y * r;
9330
- const endX = curr.x + uNext.x * r;
9331
- const endY = curr.y + uNext.y * r;
9332
- return `L ${startX2} ${startY2} Q ${curr.x} ${curr.y} ${endX} ${endY}`;
9333
- };
9334
- const uCA = unitVec(C, A);
9335
- const startX = C.x + uCA.x * (rbl > 0 ? rbl : 0);
9336
- const startY = C.y + uCA.y * (rbl > 0 ? rbl : 0);
9337
- const actualStartX = rbl > 0 ? startX : C.x;
9338
- const actualStartY = rbl > 0 ? startY : C.y;
9339
- const parts = [
9340
- `M ${actualStartX} ${actualStartY}`,
9341
- cornerSegment(C, A, B, rt),
9342
- // top corner
9343
- cornerSegment(A, B, C, rbr),
9344
- // bottom-right corner
9345
- cornerSegment(B, C, A, rbl),
9346
- // bottom-left corner
9347
- "Z"
9348
- ];
9349
- return parts.join(" ");
9350
- }
9351
- let activeThemeColors = {};
9352
- function setMarkdownThemeColors(c) {
9353
- activeThemeColors = { ...c };
9354
- }
9355
- function resolveColorToken(token, theme) {
9356
- const raw = token.trim();
9357
- const t = raw.toLowerCase();
9358
- if (t === "primary") return theme.primary;
9359
- if (t === "secondary") return theme.secondary;
9360
- if (/^#([0-9a-f]{3,8})$/i.test(raw)) return raw;
9361
- if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) return raw;
9362
- if (/^[a-z]+$/i.test(raw)) return raw;
9363
- return void 0;
9364
- }
9365
- function mergeStyle(a, b) {
9366
- return { ...a, ...b };
9367
- }
9368
- function tokenize(input, theme) {
9369
- const runs = [];
9370
- const stack = [];
9371
- let buf = "";
9372
- const activeStyle = () => {
9373
- let s = {};
9374
- for (const e of stack) s = mergeStyle(s, e.style);
9375
- return s;
9376
- };
9377
- const flush = () => {
9378
- if (buf.length === 0) return;
9379
- runs.push({ text: buf, style: activeStyle() });
9380
- buf = "";
9381
- };
9382
- let i = 0;
9383
- const n = input.length;
9384
- const peek = (s, at = i) => input.startsWith(s, at);
9385
- const findUnescaped = (needle, from) => {
9386
- let p = from;
9387
- while (p < n) {
9388
- if (input[p] === "\\" && p + 1 < n) {
9389
- p += 2;
9390
- continue;
9391
- }
9392
- if (input.startsWith(needle, p)) return p;
9393
- p++;
9394
- }
9395
- return -1;
9396
- };
9397
- const tryOpenBracket = () => {
9398
- if (input[i] !== "[") return -1;
9399
- const m = /^\[(c|bg)=([^\]]+)\]/.exec(input.slice(i));
9400
- if (!m) return -1;
9401
- const kind = m[1];
9402
- const tokenRaw = m[2];
9403
- const closer = kind === "c" ? "[/c]" : "[/bg]";
9404
- if (findUnescaped(closer, i + m[0].length) === -1) return -1;
9405
- const color = resolveColorToken(tokenRaw, theme);
9406
- const style = {};
9407
- if (color) {
9408
- if (kind === "c") style.fill = color;
9409
- else style.textBackgroundColor = color;
9410
- }
9411
- flush();
9412
- stack.push({ kind, style, closer });
9413
- return i + m[0].length;
9414
- };
9415
- const tryCloseBracket = () => {
9416
- for (let s = stack.length - 1; s >= 0; s--) {
9417
- const top = stack[s];
9418
- if (top.kind !== "c" && top.kind !== "bg") continue;
9419
- if (peek(top.closer)) {
9420
- if (s !== stack.length - 1) stack.length = s + 1;
9421
- flush();
9422
- stack.pop();
9423
- return i + top.closer.length;
9424
- }
9425
- break;
9426
- }
9427
- return -1;
9428
- };
9429
- const toggle = (delim, kind, style) => {
9430
- if (!peek(delim)) return null;
9431
- const topIdx = stack.findIndex((e) => e.kind === kind);
9432
- if (topIdx >= 0) {
9433
- if (topIdx !== stack.length - 1) return null;
9434
- flush();
9435
- stack.length = topIdx;
9436
- return i + delim.length;
9437
- }
9438
- if (findUnescaped(delim, i + delim.length) === -1) return null;
9439
- flush();
9440
- stack.push({ kind, style, closer: delim });
9441
- return i + delim.length;
9442
- };
9443
- while (i < n) {
9444
- const ch = input[i];
9445
- if (ch === "\\" && i + 1 < n) {
9446
- buf += input[i + 1];
9447
- i += 2;
9448
- continue;
9449
- }
9450
- if (ch === "[") {
9451
- const closed = tryCloseBracket();
9452
- if (closed > 0) {
9453
- i = closed;
9454
- continue;
9455
- }
9456
- const opened = tryOpenBracket();
9457
- if (opened > 0) {
9458
- i = opened;
9459
- continue;
9460
- }
9461
- }
9462
- let next;
9463
- if ((next = toggle("**", "bold", { fontWeight: 700 })) !== null) {
9464
- i = next;
9465
- continue;
9466
- }
9467
- if ((next = toggle("__", "under", { underline: true })) !== null) {
9468
- i = next;
9469
- continue;
9470
- }
9471
- if ((next = toggle("~~", "strike", { linethrough: true })) !== null) {
9472
- i = next;
9473
- continue;
9474
- }
9475
- if ((next = toggle("==", "highlight", { textBackgroundColor: theme.secondary || "#ffe066" })) !== null) {
9476
- i = next;
9477
- continue;
9478
- }
9479
- if ((next = toggle("*", "italic", { fontStyle: "italic" })) !== null) {
9480
- i = next;
9481
- continue;
9482
- }
9483
- buf += ch;
9484
- i++;
9485
- }
9486
- flush();
9487
- return runs;
9488
- }
9489
- function parseTextMarkdown(input, themeColors) {
9490
- const theme = activeThemeColors;
9491
- const runs = tokenize(input ?? "", theme);
9492
- let plain = "";
9493
- const styles = {};
9494
- let lineIdx = 0;
9495
- let charIdx = 0;
9496
- let hasFormatting = false;
9497
- for (const run of runs) {
9498
- const styleHasContent = Object.keys(run.style).length > 0;
9499
- if (styleHasContent) hasFormatting = true;
9500
- for (const ch of run.text) {
9501
- if (ch === "\n") {
9502
- plain += "\n";
9503
- lineIdx++;
9504
- charIdx = 0;
9505
- continue;
9506
- }
9507
- plain += ch;
9508
- if (styleHasContent) {
9509
- if (!styles[lineIdx]) styles[lineIdx] = {};
9510
- styles[lineIdx][charIdx] = { ...run.style };
9511
- }
9512
- charIdx++;
9475
+ tl > 0 ? `A ${tl} ${tl} 0 0 1 ${tl} 0` : `L 0 0`,
9476
+ "Z"
9477
+ ].join(" ");
9478
+ }
9479
+ function getTrianglePoints(x, y, width, height) {
9480
+ return [
9481
+ { x: x + width / 2, y },
9482
+ { x: x + width, y: y + height },
9483
+ { x, y: y + height }
9484
+ ];
9485
+ }
9486
+ function buildRoundedTrianglePath(w, h, rTop, rBR, rBL) {
9487
+ const A = { x: w / 2, y: 0 };
9488
+ const B = { x: w, y: h };
9489
+ const C = { x: 0, y: h };
9490
+ const maxR = Math.min(w, h) / 2;
9491
+ const rt = Math.min(Math.max(0, rTop), maxR);
9492
+ const rbr = Math.min(Math.max(0, rBR), maxR);
9493
+ const rbl = Math.min(Math.max(0, rBL), maxR);
9494
+ const unitVec = (p1, p2) => {
9495
+ const dx = p2.x - p1.x;
9496
+ const dy = p2.y - p1.y;
9497
+ const len = Math.sqrt(dx * dx + dy * dy);
9498
+ return len > 0 ? { x: dx / len, y: dy / len } : { x: 0, y: 0 };
9499
+ };
9500
+ const cornerSegment = (prev, curr, next, r) => {
9501
+ if (r <= 0) {
9502
+ return `L ${curr.x} ${curr.y}`;
9513
9503
  }
9514
- }
9515
- return { plainText: plain, styles, hasFormatting };
9504
+ const uPrev = unitVec(curr, prev);
9505
+ const uNext = unitVec(curr, next);
9506
+ const startX2 = curr.x + uPrev.x * r;
9507
+ const startY2 = curr.y + uPrev.y * r;
9508
+ const endX = curr.x + uNext.x * r;
9509
+ const endY = curr.y + uNext.y * r;
9510
+ return `L ${startX2} ${startY2} Q ${curr.x} ${curr.y} ${endX} ${endY}`;
9511
+ };
9512
+ const uCA = unitVec(C, A);
9513
+ const startX = C.x + uCA.x * (rbl > 0 ? rbl : 0);
9514
+ const startY = C.y + uCA.y * (rbl > 0 ? rbl : 0);
9515
+ const actualStartX = rbl > 0 ? startX : C.x;
9516
+ const actualStartY = rbl > 0 ? startY : C.y;
9517
+ const parts = [
9518
+ `M ${actualStartX} ${actualStartY}`,
9519
+ cornerSegment(C, A, B, rt),
9520
+ // top corner
9521
+ cornerSegment(A, B, C, rbr),
9522
+ // bottom-right corner
9523
+ cornerSegment(B, C, A, rbl),
9524
+ // bottom-left corner
9525
+ "Z"
9526
+ ];
9527
+ return parts.join(" ");
9516
9528
  }
9517
9529
  function angleToCoords(angleDeg) {
9518
9530
  const rad = angleDeg * Math.PI / 180;
@@ -21115,9 +21127,14 @@ function setInTree(nodes, elementId, targetProperty, value) {
21115
21127
  }
21116
21128
  if (targetProperty === "text" && node.type === "text") {
21117
21129
  const overflowPolicy = String(node.overflowPolicy ?? "grow-and-push");
21118
- if (overflowPolicy !== "auto-shrink") {
21119
- delete node.height;
21130
+ if (overflowPolicy === "auto-shrink") {
21131
+ const explicitH = typeof node.height === "number" ? node.height : 0;
21132
+ const existingMin = Math.max(0, Number(node.minBoxHeight) || 0);
21133
+ if (explicitH > 0 && existingMin <= 0) {
21134
+ node.minBoxHeight = explicitH;
21135
+ }
21120
21136
  }
21137
+ delete node.height;
21121
21138
  }
21122
21139
  return true;
21123
21140
  }
@@ -25218,6 +25235,89 @@ const previewBlur = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineP
25218
25235
  injectPreviewBlur,
25219
25236
  resolveBlurElementExactIdsFromFlatFormKeys
25220
25237
  }, Symbol.toStringTag, { value: "Module" }));
25238
+ function collectImageUrls(config) {
25239
+ const urls = [];
25240
+ const walk = (nodes) => {
25241
+ for (const node of nodes) {
25242
+ if (!node || node.visible === false) continue;
25243
+ const src = typeof node.src === "string" ? node.src.trim() : "";
25244
+ const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
25245
+ if (node.type === "image") {
25246
+ const url = src || imageUrl;
25247
+ if (url) urls.push(url);
25248
+ }
25249
+ if (Array.isArray(node.children) && node.children.length > 0) {
25250
+ walk(node.children);
25251
+ }
25252
+ }
25253
+ };
25254
+ for (const page of config.pages || []) {
25255
+ walk(page.children || []);
25256
+ }
25257
+ return urls;
25258
+ }
25259
+ function normalizeAssetUrl(rawUrl, imageProxyUrl) {
25260
+ if (!rawUrl) return null;
25261
+ if (rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) return null;
25262
+ if (rawUrl.startsWith("/") && !rawUrl.startsWith("//")) {
25263
+ if (typeof window !== "undefined") return new URL(rawUrl, window.location.origin).toString();
25264
+ return null;
25265
+ }
25266
+ try {
25267
+ const h = new URL(rawUrl).hostname.toLowerCase();
25268
+ if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(h)) {
25269
+ if (typeof window !== "undefined" && new URL(rawUrl).origin === window.location.origin) {
25270
+ return rawUrl;
25271
+ }
25272
+ return null;
25273
+ }
25274
+ } catch {
25275
+ return null;
25276
+ }
25277
+ const supabaseUrl = typeof globalThis.__VITE_SUPABASE_URL === "string" ? globalThis.__VITE_SUPABASE_URL : "";
25278
+ if (supabaseUrl && rawUrl.includes(supabaseUrl)) {
25279
+ const signedMatch = rawUrl.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
25280
+ if (signedMatch) return `${supabaseUrl}/storage/v1/object/public/${signedMatch[1]}`;
25281
+ if (rawUrl.includes("/storage/v1/object/public/")) return rawUrl;
25282
+ }
25283
+ const proxyBase = imageProxyUrl ? imageProxyUrl.replace(/\/image-proxy(?:\?.*)?$/, "") : API_URL;
25284
+ if (proxyBase) {
25285
+ return `${proxyBase}/image-proxy?url=${encodeURIComponent(rawUrl)}`;
25286
+ }
25287
+ return rawUrl;
25288
+ }
25289
+ const CONCURRENCY = 6;
25290
+ async function prefetchUrls(urls, signal) {
25291
+ const unique = [...new Set(urls)];
25292
+ if (unique.length === 0) return;
25293
+ let i = 0;
25294
+ const next = async () => {
25295
+ while (i < unique.length) {
25296
+ if (signal == null ? void 0 : signal.aborted) return;
25297
+ const url = unique[i++];
25298
+ try {
25299
+ await fetch(url, { signal, mode: "cors", credentials: "omit" });
25300
+ } catch {
25301
+ }
25302
+ }
25303
+ };
25304
+ const workers = Array.from({ length: Math.min(CONCURRENCY, unique.length) }, () => next());
25305
+ await Promise.all(workers);
25306
+ }
25307
+ async function warmResolvedTemplateForPreview(config, options) {
25308
+ const { signal, imageProxyUrl } = options ?? {};
25309
+ await ensureFontsForResolvedConfig(config);
25310
+ if (signal == null ? void 0 : signal.aborted) return;
25311
+ const rawUrls = collectImageUrls(config);
25312
+ const resolvedUrls = rawUrls.map((u) => normalizeAssetUrl(u, imageProxyUrl)).filter((u) => u !== null);
25313
+ await prefetchUrls(resolvedUrls, signal);
25314
+ }
25315
+ async function warmTemplateFromForm(options) {
25316
+ const { signal, imageProxyUrl, ...resolveOpts } = options;
25317
+ const resolved = await resolveFromForm(resolveOpts);
25318
+ if (signal == null ? void 0 : signal.aborted) return;
25319
+ await warmResolvedTemplateForPreview(resolved.config, { signal, imageProxyUrl });
25320
+ }
25221
25321
  const PREVIEW_DEBUG_PREFIX = "[canvas-renderer][preview-debug]";
25222
25322
  function computeFontSignature(config) {
25223
25323
  var _a2;
@@ -25233,6 +25333,39 @@ function computeFontSignature(config) {
25233
25333
  for (const page of config.pages) walk(page.children || []);
25234
25334
  return Array.from(fams).sort().join("|");
25235
25335
  }
25336
+ function computeImageSignature(config) {
25337
+ var _a2;
25338
+ if (!((_a2 = config == null ? void 0 : config.pages) == null ? void 0 : _a2.length)) return "";
25339
+ try {
25340
+ const urls = collectImageUrls(config);
25341
+ return urls.length === 0 ? "" : urls.slice().sort().join("|");
25342
+ } catch {
25343
+ return "";
25344
+ }
25345
+ }
25346
+ function preloadImageUrl(url, proxyBase) {
25347
+ return new Promise((resolve) => {
25348
+ if (!url || url.startsWith("data:") || url.startsWith("blob:")) return resolve();
25349
+ let done = false;
25350
+ const finish = () => {
25351
+ if (!done) {
25352
+ done = true;
25353
+ resolve();
25354
+ }
25355
+ };
25356
+ const isHttp = /^https?:/i.test(url);
25357
+ const target = isHttp && proxyBase && !url.includes("/image-proxy?") ? `${proxyBase.replace(/\/+$/, "")}?url=${encodeURIComponent(url)}` : url;
25358
+ const img = new Image();
25359
+ try {
25360
+ img.crossOrigin = "anonymous";
25361
+ } catch {
25362
+ }
25363
+ img.onload = finish;
25364
+ img.onerror = finish;
25365
+ img.src = target;
25366
+ setTimeout(finish, 6e3);
25367
+ });
25368
+ }
25236
25369
  function countUnderlinedNodes(config) {
25237
25370
  var _a2;
25238
25371
  if (!((_a2 = config == null ? void 0 : config.pages) == null ? void 0 : _a2.length)) return 0;
@@ -25395,6 +25528,38 @@ function PixldocsPreview(props) {
25395
25528
  const config = isResolveMode ? resolvedConfig : props.config;
25396
25529
  const previewKey = useMemo(() => `${pageIndex}`, [pageIndex]);
25397
25530
  const fontSignature = useMemo(() => computeFontSignature(config), [config]);
25531
+ const imageSignature = useMemo(() => computeImageSignature(config), [config]);
25532
+ const [imagesReady, setImagesReady] = useState(true);
25533
+ const firstImageRef = useMemo(() => ({ first: true }), []);
25534
+ useEffect(() => {
25535
+ if (!config) {
25536
+ setImagesReady(true);
25537
+ return;
25538
+ }
25539
+ if (firstImageRef.first) {
25540
+ firstImageRef.first = false;
25541
+ setImagesReady(true);
25542
+ return;
25543
+ }
25544
+ let urls = [];
25545
+ try {
25546
+ urls = collectImageUrls(config);
25547
+ } catch {
25548
+ urls = [];
25549
+ }
25550
+ if (urls.length === 0) {
25551
+ setImagesReady(true);
25552
+ return;
25553
+ }
25554
+ setImagesReady(false);
25555
+ let cancelled = false;
25556
+ Promise.all(urls.map((u) => preloadImageUrl(u, imageProxyUrl))).then(() => {
25557
+ if (!cancelled) setImagesReady(true);
25558
+ });
25559
+ return () => {
25560
+ cancelled = true;
25561
+ };
25562
+ }, [imageSignature, imageProxyUrl]);
25398
25563
  useEffect(() => {
25399
25564
  if (isResolveMode) return;
25400
25565
  if (!config) {
@@ -25477,7 +25642,7 @@ function PixldocsPreview(props) {
25477
25642
  /* @__PURE__ */ jsxs(
25478
25643
  "div",
25479
25644
  {
25480
- style: hasOverlays ? { visibility: canvasSettled ? "visible" : "hidden", position: "relative", width: canvasW * zoom, height: canvasH * zoom } : { visibility: canvasSettled ? "visible" : "hidden" },
25645
+ style: hasOverlays ? { visibility: canvasSettled && imagesReady ? "visible" : "hidden", position: "relative", width: canvasW * zoom, height: canvasH * zoom } : { visibility: canvasSettled && imagesReady ? "visible" : "hidden" },
25481
25646
  children: [
25482
25647
  /* @__PURE__ */ jsx(
25483
25648
  PreviewCanvas,
@@ -25523,7 +25688,7 @@ function PixldocsPreview(props) {
25523
25688
  ]
25524
25689
  }
25525
25690
  ),
25526
- !canvasSettled && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: loadingFallback ?? /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
25691
+ (!canvasSettled || !imagesReady) && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", minHeight: 200 }, children: loadingFallback ?? /* @__PURE__ */ jsx("div", { style: { color: "#888", fontSize: 14 }, children: "Loading preview..." }) })
25527
25692
  ] });
25528
25693
  }
25529
25694
  function normalizeSvgDimensions(svg, targetWidth, targetHeight) {
@@ -26044,9 +26209,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
26044
26209
  }
26045
26210
  return svgString;
26046
26211
  }
26047
- const resolvedPackageVersion = "0.5.457";
26212
+ const resolvedPackageVersion = "0.5.459";
26048
26213
  const PACKAGE_VERSION = resolvedPackageVersion;
26049
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.457";
26214
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.459";
26050
26215
  const roundParityValue = (value) => {
26051
26216
  if (typeof value !== "number") return value;
26052
26217
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -26860,7 +27025,7 @@ class PixldocsRenderer {
26860
27025
  await this.waitForCanvasScene(container, cloned, i);
26861
27026
  }
26862
27027
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
26863
- const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-DHunvQTF.js");
27028
+ const { exportMultiPagePdf, preparePagesForExport } = await import("./vectorPdfExport-De64EUqT.js");
26864
27029
  const prepared = preparePagesForExport(
26865
27030
  cloned.pages,
26866
27031
  canvasWidth,
@@ -29180,7 +29345,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
29180
29345
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
29181
29346
  sanitizeSvgTreeForPdf(svgToDraw);
29182
29347
  try {
29183
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-DHunvQTF.js");
29348
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await import("./vectorPdfExport-De64EUqT.js");
29184
29349
  try {
29185
29350
  await logTextMeasurementDiagnostic(svgToDraw);
29186
29351
  } catch {
@@ -29428,89 +29593,6 @@ async function getPublishedTemplate(options) {
29428
29593
  const rows = await res.json();
29429
29594
  return rows[0] ?? null;
29430
29595
  }
29431
- function collectImageUrls(config) {
29432
- const urls = [];
29433
- const walk = (nodes) => {
29434
- for (const node of nodes) {
29435
- if (!node || node.visible === false) continue;
29436
- const src = typeof node.src === "string" ? node.src.trim() : "";
29437
- const imageUrl = typeof node.imageUrl === "string" ? node.imageUrl.trim() : "";
29438
- if (node.type === "image") {
29439
- const url = src || imageUrl;
29440
- if (url) urls.push(url);
29441
- }
29442
- if (Array.isArray(node.children) && node.children.length > 0) {
29443
- walk(node.children);
29444
- }
29445
- }
29446
- };
29447
- for (const page of config.pages || []) {
29448
- walk(page.children || []);
29449
- }
29450
- return urls;
29451
- }
29452
- function normalizeAssetUrl(rawUrl, imageProxyUrl) {
29453
- if (!rawUrl) return null;
29454
- if (rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) return null;
29455
- if (rawUrl.startsWith("/") && !rawUrl.startsWith("//")) {
29456
- if (typeof window !== "undefined") return new URL(rawUrl, window.location.origin).toString();
29457
- return null;
29458
- }
29459
- try {
29460
- const h = new URL(rawUrl).hostname.toLowerCase();
29461
- if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.endsWith(".local") || /^(10\.|192\.168\.|169\.254\.)/.test(h)) {
29462
- if (typeof window !== "undefined" && new URL(rawUrl).origin === window.location.origin) {
29463
- return rawUrl;
29464
- }
29465
- return null;
29466
- }
29467
- } catch {
29468
- return null;
29469
- }
29470
- const supabaseUrl = typeof globalThis.__VITE_SUPABASE_URL === "string" ? globalThis.__VITE_SUPABASE_URL : "";
29471
- if (supabaseUrl && rawUrl.includes(supabaseUrl)) {
29472
- const signedMatch = rawUrl.match(/\/storage\/v1\/object\/sign\/([^?]+)/);
29473
- if (signedMatch) return `${supabaseUrl}/storage/v1/object/public/${signedMatch[1]}`;
29474
- if (rawUrl.includes("/storage/v1/object/public/")) return rawUrl;
29475
- }
29476
- const proxyBase = imageProxyUrl ? imageProxyUrl.replace(/\/image-proxy(?:\?.*)?$/, "") : API_URL;
29477
- if (proxyBase) {
29478
- return `${proxyBase}/image-proxy?url=${encodeURIComponent(rawUrl)}`;
29479
- }
29480
- return rawUrl;
29481
- }
29482
- const CONCURRENCY = 6;
29483
- async function prefetchUrls(urls, signal) {
29484
- const unique = [...new Set(urls)];
29485
- if (unique.length === 0) return;
29486
- let i = 0;
29487
- const next = async () => {
29488
- while (i < unique.length) {
29489
- if (signal == null ? void 0 : signal.aborted) return;
29490
- const url = unique[i++];
29491
- try {
29492
- await fetch(url, { signal, mode: "cors", credentials: "omit" });
29493
- } catch {
29494
- }
29495
- }
29496
- };
29497
- const workers = Array.from({ length: Math.min(CONCURRENCY, unique.length) }, () => next());
29498
- await Promise.all(workers);
29499
- }
29500
- async function warmResolvedTemplateForPreview(config, options) {
29501
- const { signal, imageProxyUrl } = options ?? {};
29502
- await ensureFontsForResolvedConfig(config);
29503
- if (signal == null ? void 0 : signal.aborted) return;
29504
- const rawUrls = collectImageUrls(config);
29505
- const resolvedUrls = rawUrls.map((u) => normalizeAssetUrl(u, imageProxyUrl)).filter((u) => u !== null);
29506
- await prefetchUrls(resolvedUrls, signal);
29507
- }
29508
- async function warmTemplateFromForm(options) {
29509
- const { signal, imageProxyUrl, ...resolveOpts } = options;
29510
- const resolved = await resolveFromForm(resolveOpts);
29511
- if (signal == null ? void 0 : signal.aborted) return;
29512
- await warmResolvedTemplateForPreview(resolved.config, { signal, imageProxyUrl });
29513
- }
29514
29596
  function setAutoShrinkDebug(enabled) {
29515
29597
  if (typeof window !== "undefined") {
29516
29598
  window.__pixldocsDebugAutoShrink = !!enabled;
@@ -29580,4 +29662,4 @@ export {
29580
29662
  buildTeaserBlurFlatKeys as y,
29581
29663
  collectFontDescriptorsFromConfig as z
29582
29664
  };
29583
- //# sourceMappingURL=index-BWYQI5Fp.js.map
29665
+ //# sourceMappingURL=index-BzvyPXLB.js.map