@pixldocs/canvas-renderer 0.5.165 → 0.5.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,85 @@ Client-side template renderer for Pixldocs — render templates in any web app w
8
8
  npm install @pixldocs/canvas-renderer fabric react react-dom
9
9
  ```
10
10
 
11
- > **Private package**: Add `//registry.npmjs.org/:_authToken=${NPM_TOKEN}` to your `.npmrc` and configure `NPM_TOKEN` as a build secret.
11
+ > Public package no auth token or `.npmrc` needed.
12
+
13
+ ---
14
+
15
+ ## Quick Start: Build an App From a Workspace
16
+
17
+ Got a Pixldocs workspace full of published templates (e.g. social-media posts,
18
+ invitations, certificates)? You can spin up a standalone app that lists and
19
+ renders them with just the **workspace ID** + Pixldocs anon key — no per-template
20
+ wiring needed.
21
+
22
+ ```tsx
23
+ import {
24
+ listPublishedTemplates,
25
+ PixldocsRenderer,
26
+ } from '@pixldocs/canvas-renderer';
27
+
28
+ const SUPABASE_URL = 'https://ttvtjhxjxuxdeybcnjkd.supabase.co';
29
+ const SUPABASE_ANON_KEY = 'eyJ...'; // Pixldocs publishable key (safe in browser)
30
+ const WORKSPACE_ID = 'your-workspace-uuid';
31
+
32
+ // 1. List all published templates owned by a workspace
33
+ const templates = await listPublishedTemplates({
34
+ workspaceId: WORKSPACE_ID,
35
+ supabaseUrl: SUPABASE_URL,
36
+ supabaseAnonKey: SUPABASE_ANON_KEY,
37
+ // category: 'social-media', // optional filter
38
+ });
39
+
40
+ // templates: [{ id, name, thumbnail_url, category, price, ... }, ...]
41
+
42
+ // 2. Render a chosen template (form-bound or static)
43
+ const renderer = new PixldocsRenderer({
44
+ supabaseUrl: SUPABASE_URL,
45
+ supabaseAnonKey: SUPABASE_ANON_KEY,
46
+ });
47
+
48
+ const pages = await renderer.renderFromForm({
49
+ templateId: templates[0].id,
50
+ sectionState: { /* user inputs (optional for static templates) */ },
51
+ });
52
+ ```
53
+
54
+ ### Two ways to source templates
55
+
56
+ | Approach | Best for | API |
57
+ |----------|----------|-----|
58
+ | **By workspace** | Multi-template apps (template gallery, social-media post maker, certificate maker) — pick from a curated workspace | `listPublishedTemplates({ workspaceId })` |
59
+ | **By form schema** | Single-purpose apps where one form drives many template variants (e.g. BioMaker — one form, many biodata designs) | Resolve directly via `renderFromForm({ templateId, formSchemaId, sectionState })` |
60
+
61
+ Both work with the same anon key and the same renderer — choose whichever
62
+ matches your product shape.
63
+
64
+ ---
65
+
66
+ ## Templates Catalog API
67
+
68
+ ### `listPublishedTemplates(options)`
69
+
70
+ Returns every published template belonging to a workspace. Uses public RLS
71
+ (no auth needed beyond the anon key).
72
+
73
+ ```ts
74
+ const templates = await listPublishedTemplates({
75
+ workspaceId: 'uuid',
76
+ supabaseUrl: '...',
77
+ supabaseAnonKey: '...',
78
+ category: 'social-media', // optional
79
+ limit: 200, // optional (default 200)
80
+ offset: 0, // optional pagination
81
+ });
82
+ ```
83
+
84
+ Each item: `{ id, name, description, category, thumbnail_url, preview_images, price, download_count, workspace_id, sort_order, created_at, updated_at }`.
85
+
86
+ ### `getPublishedTemplate({ templateId, supabaseUrl, supabaseAnonKey })`
87
+
88
+ Fetch a single published template's catalog row (without the heavy `config`
89
+ JSON — use the renderer for that).
12
90
 
13
91
  ---
14
92
 
@@ -348,11 +348,12 @@ function getCacheKey(element) {
348
348
  scaleY: element.scaleY,
349
349
  splitByGrapheme: element.splitByGrapheme,
350
350
  overflowPolicy: element.overflowPolicy,
351
- height: element.overflowPolicy === "auto-shrink" ? element.height : void 0
351
+ height: element.overflowPolicy === "auto-shrink" ? element.height : void 0,
352
+ minBoxHeight: element.minBoxHeight,
353
+ verticalAlign: element.verticalAlign
352
354
  });
353
355
  }
354
356
  function measureTextHeight(element) {
355
- var _a;
356
357
  if (element.type !== "text") {
357
358
  return element.height || 20;
358
359
  }
@@ -368,8 +369,8 @@ function measureTextHeight(element) {
368
369
  let fontSize = element.fontSize || 16;
369
370
  const overflowPolicy = element.overflowPolicy || "grow-and-push";
370
371
  if (overflowPolicy === "auto-shrink") {
371
- const baseHeight = element.height;
372
- const explicitLineCount = Math.max(1, textToMeasure.split("\n").length);
372
+ const minBoxH = Math.max(0, Number(element.minBoxHeight) || 0);
373
+ const baseHeight = typeof element.height === "number" ? Math.max(element.height, minBoxH) : minBoxH > 0 ? minBoxH : element.height;
373
374
  while (fontSize > 1) {
374
375
  const testTb = new fabric__namespace.Textbox(textToMeasure, {
375
376
  width: measureWidth,
@@ -383,11 +384,9 @@ function measureTextHeight(element) {
383
384
  });
384
385
  testTb.initDimensions();
385
386
  const textHeight = testTb.height || 0;
386
- const renderedLineCount = ((_a = testTb.textLines) == null ? void 0 : _a.length) || 1;
387
- const hasNoImplicitWrap = renderedLineCount <= explicitLineCount;
388
387
  const fitsHeight = !baseHeight || textHeight <= baseHeight;
389
388
  const { fitsWidth } = getTextboxWidthFitMetrics(testTb, measureWidth);
390
- if (hasNoImplicitWrap && fitsHeight && fitsWidth) break;
389
+ if (fitsHeight && fitsWidth) break;
391
390
  fontSize--;
392
391
  }
393
392
  const finalTb = new fabric__namespace.Textbox(textToMeasure, {
@@ -402,7 +401,7 @@ function measureTextHeight(element) {
402
401
  });
403
402
  finalTb.initDimensions();
404
403
  const measuredH = (finalTb.height || element.height || 20) * (element.scaleY || 1);
405
- const cappedH = typeof baseHeight === "number" ? Math.min(baseHeight * (element.scaleY || 1), measuredH) : measuredH;
404
+ const cappedH = typeof baseHeight === "number" ? Math.max(baseHeight * (element.scaleY || 1), measuredH) : measuredH;
406
405
  heightCache.set(cacheKey, { height: cappedH, timestamp: Date.now() });
407
406
  return cappedH;
408
407
  }
@@ -5550,6 +5549,54 @@ function calculateScaleSnapGuides(scalingObj, corner, canvas, canvasWidth, canva
5550
5549
  return true;
5551
5550
  });
5552
5551
  }
5552
+ const TextboxProto = fabric__namespace.Textbox.prototype;
5553
+ if (!TextboxProto.__pixldocsOrigCalcTextHeight) {
5554
+ TextboxProto.__pixldocsOrigCalcTextHeight = TextboxProto.calcTextHeight;
5555
+ TextboxProto.calcTextHeight = function() {
5556
+ const orig = TextboxProto.__pixldocsOrigCalcTextHeight.call(this);
5557
+ this._contentHeight = orig;
5558
+ const min = this.minBoxHeight || 0;
5559
+ return min > orig ? min : orig;
5560
+ };
5561
+ }
5562
+ if (!TextboxProto.__pixldocsOrigGetTopOffset) {
5563
+ TextboxProto.__pixldocsOrigGetTopOffset = TextboxProto._getTopOffset;
5564
+ TextboxProto._getTopOffset = function() {
5565
+ const baseOffset = TextboxProto.__pixldocsOrigGetTopOffset.call(this);
5566
+ const valign = this.verticalAlign || "top";
5567
+ if (valign === "top") return baseOffset;
5568
+ const content = typeof this._contentHeight === "number" ? this._contentHeight : TextboxProto.__pixldocsOrigCalcTextHeight.call(this);
5569
+ const padding = (this.height || 0) - content;
5570
+ if (padding <= 0) return baseOffset;
5571
+ if (valign === "middle") return baseOffset + padding / 2;
5572
+ if (valign === "bottom") return baseOffset + padding;
5573
+ return baseOffset;
5574
+ };
5575
+ }
5576
+ if (TextboxProto._getSVGLeftTopOffsets && !TextboxProto.__pixldocsOrigGetSVGLeftTopOffsets) {
5577
+ TextboxProto.__pixldocsOrigGetSVGLeftTopOffsets = TextboxProto._getSVGLeftTopOffsets;
5578
+ TextboxProto._getSVGLeftTopOffsets = function() {
5579
+ const base = TextboxProto.__pixldocsOrigGetSVGLeftTopOffsets.call(this);
5580
+ const valign = this.verticalAlign || "top";
5581
+ if (valign === "top") return base;
5582
+ const content = typeof this._contentHeight === "number" ? this._contentHeight : TextboxProto.__pixldocsOrigCalcTextHeight.call(this);
5583
+ const padding = (this.height || 0) - content;
5584
+ if (padding <= 0) return base;
5585
+ const extra = valign === "middle" ? padding / 2 : padding;
5586
+ return { ...base, textTop: base.textTop + extra };
5587
+ };
5588
+ }
5589
+ const stateProps = fabric__namespace.Textbox.prototype.stateProperties;
5590
+ if (Array.isArray(stateProps)) {
5591
+ if (!stateProps.includes("minBoxHeight")) stateProps.push("minBoxHeight");
5592
+ if (!stateProps.includes("verticalAlign")) stateProps.push("verticalAlign");
5593
+ }
5594
+ const cacheProps = fabric__namespace.Textbox.prototype.cacheProperties;
5595
+ if (Array.isArray(cacheProps)) {
5596
+ if (!cacheProps.includes("minBoxHeight")) cacheProps.push("minBoxHeight");
5597
+ if (!cacheProps.includes("verticalAlign")) cacheProps.push("verticalAlign");
5598
+ }
5599
+ TextboxProto.__pixldocsTextboxExtended = true;
5553
5600
  const PD_BG_KEY = "__pdBg";
5554
5601
  const PATCHED_KEY = "__pdBgPatched";
5555
5602
  function extractTextBgConfig(element) {
@@ -6369,7 +6416,7 @@ function createText(element) {
6369
6416
  iterationSamples.push(lastIter);
6370
6417
  }
6371
6418
  }
6372
- if (hasNoImplicitWrap && fitsHeight && fitsWidth) {
6419
+ if (fitsHeight && fitsWidth) {
6373
6420
  breakReason = "fits";
6374
6421
  break;
6375
6422
  }
@@ -6462,7 +6509,15 @@ function createText(element) {
6462
6509
  // formatting tokens (**, __, [c=...], etc). Disable inline edit and steer
6463
6510
  // users to the right-panel text field which exposes the raw markdown.
6464
6511
  editable: !formattingEnabled,
6465
- ...formattingEnabled ? { styles: parsedStyles } : element.styles ? { styles: element.styles } : {}
6512
+ ...formattingEnabled ? { styles: parsedStyles } : element.styles ? { styles: element.styles } : {},
6513
+ // Vertical sizing extensions (see fabricTextboxExtensions.ts). Apply for
6514
+ // every overflow policy — in auto-shrink mode `minBoxHeight` acts as a
6515
+ // visual floor so the box renders at the user's chosen height even after
6516
+ // the font shrinks to fit. PageCanvas's auto-shrink loop already uses the
6517
+ // same value as a fit-target, so the rendered box and the shrink target
6518
+ // stay in sync (parity with the Use page / EC2 renderer).
6519
+ ...(element.minBoxHeight ?? 0) > 0 ? { minBoxHeight: element.minBoxHeight } : {},
6520
+ verticalAlign: element.verticalAlign || "top"
6466
6521
  });
6467
6522
  textbox.__formattingEnabled = formattingEnabled;
6468
6523
  textbox.initDimensions();
@@ -8057,6 +8112,22 @@ const PageCanvas = react.forwardRef(
8057
8112
  obj.dirty = true;
8058
8113
  }
8059
8114
  }
8115
+ if (obj instanceof fabric__namespace.Textbox) {
8116
+ const sy = obj.scaleY ?? 1;
8117
+ if (Math.abs(sy - 1) > 1e-3) {
8118
+ const center = obj.getCenterPoint();
8119
+ const newMinH = Math.max(0, (obj.height ?? 0) * Math.abs(sy));
8120
+ obj.minBoxHeight = newMinH;
8121
+ obj.scaleY = 1;
8122
+ try {
8123
+ obj.initDimensions();
8124
+ } catch {
8125
+ }
8126
+ obj.setPositionByOrigin(center, "center", "center");
8127
+ obj.setCoords();
8128
+ obj.dirty = true;
8129
+ }
8130
+ }
8060
8131
  if (obj.__lockScaleDuringCrop || obj.__cropDrag) {
8061
8132
  obj.set({ scaleX: 1, scaleY: 1 });
8062
8133
  obj.setCoords();
@@ -8633,6 +8704,12 @@ const PageCanvas = react.forwardRef(
8633
8704
  scaleY: finalScaleY,
8634
8705
  transformMatrix: finalAbsoluteMatrix
8635
8706
  };
8707
+ if (obj instanceof fabric__namespace.Textbox) {
8708
+ const baked = obj.minBoxHeight;
8709
+ if (typeof baked === "number" && baked > 0) {
8710
+ elementUpdate.minBoxHeight = baked;
8711
+ }
8712
+ }
8636
8713
  if (sourceElement && sourceElement.opacity !== void 0) {
8637
8714
  elementUpdate.opacity = sourceElement.opacity;
8638
8715
  }
@@ -9344,7 +9421,9 @@ const PageCanvas = react.forwardRef(
9344
9421
  const resolvedSizeForCompare = (pageChildren == null ? void 0 : pageChildren.length) ? getNodeBounds(element, pageChildren) : { width: typeof element.width === "number" ? element.width : 0, height: typeof element.height === "number" ? element.height : 0 };
9345
9422
  const fabricText = existingObj.text ?? "";
9346
9423
  const storeText = element.text ?? "";
9347
- const otherPropsChanged = Math.abs((existingObj.width ?? 0) - resolvedSizeForCompare.width) > 0.1 || Math.abs((existingObj.height ?? 0) - resolvedSizeForCompare.height) > 0.1 || Math.abs((existingObj.angle ?? 0) - (element.angle ?? 0)) > 0.1 || Math.abs((existingObj.scaleX ?? 1) - (element.scaleX ?? 1)) > 0.01 || Math.abs((existingObj.scaleY ?? 1) - (element.scaleY ?? 1)) > 0.01 || (existingObj.flipX ?? false) !== (element.flipX ?? false) || (existingObj.flipY ?? false) !== (element.flipY ?? false) || fabricText !== storeText || existingObj.fill !== (element.fill ?? "") || existingObj.stroke !== (element.stroke ?? "") || Math.abs((existingObj.strokeWidth ?? 0) - (element.strokeWidth ?? 0)) > 0.01 || Math.abs((existingObj.opacity ?? 1) - (element.opacity ?? 1)) > 0.01 || (existingObj.fontSize ?? 0) !== (element.fontSize ?? 0) || (existingObj.fontFamily ?? "") !== (element.fontFamily ?? "") || // Detect text background + shadow changes so panel edits flow into Fabric.
9424
+ const otherPropsChanged = Math.abs((existingObj.width ?? 0) - resolvedSizeForCompare.width) > 0.1 || Math.abs((existingObj.height ?? 0) - resolvedSizeForCompare.height) > 0.1 || Math.abs((existingObj.angle ?? 0) - (element.angle ?? 0)) > 0.1 || Math.abs((existingObj.scaleX ?? 1) - (element.scaleX ?? 1)) > 0.01 || Math.abs((existingObj.scaleY ?? 1) - (element.scaleY ?? 1)) > 0.01 || (existingObj.flipX ?? false) !== (element.flipX ?? false) || (existingObj.flipY ?? false) !== (element.flipY ?? false) || fabricText !== storeText || existingObj.fill !== (element.fill ?? "") || existingObj.stroke !== (element.stroke ?? "") || Math.abs((existingObj.strokeWidth ?? 0) - (element.strokeWidth ?? 0)) > 0.01 || Math.abs((existingObj.opacity ?? 1) - (element.opacity ?? 1)) > 0.01 || (existingObj.fontSize ?? 0) !== (element.fontSize ?? 0) || (existingObj.fontFamily ?? "") !== (element.fontFamily ?? "") || // Vertical alignment & min box height: panel-driven changes must trigger a re-apply
9425
+ // so _getTopOffset (verticalAlign) and calcTextHeight (minBoxHeight) repaint correctly.
9426
+ (existingObj.verticalAlign ?? "top") !== (element.verticalAlign ?? "top") || Math.abs((existingObj.minBoxHeight ?? 0) - (element.minBoxHeight ?? 0)) > 0.1 || // Detect text background + shadow changes so panel edits flow into Fabric.
9348
9427
  JSON.stringify({
9349
9428
  c: element.textBgColor ?? null,
9350
9429
  p: element.textBgPadding ?? 0,
@@ -9799,7 +9878,7 @@ const PageCanvas = react.forwardRef(
9799
9878
  });
9800
9879
  }, [selectedIds, isActive, ready, elements]);
9801
9880
  const updateFabricObject = (obj, element, skipPositionUpdate = false) => {
9802
- var _a, _b, _c;
9881
+ var _a, _b;
9803
9882
  const fc = fabricRef.current;
9804
9883
  if (fc && isTransforming(fc)) {
9805
9884
  return;
@@ -10168,7 +10247,8 @@ const PageCanvas = react.forwardRef(
10168
10247
  const fixedWidth = Math.max(storedWidth, 1);
10169
10248
  const splitByGrapheme = overflowPolicy === "auto-shrink" ? false : element.splitByGrapheme ?? element.wordWrap === "break-word";
10170
10249
  if (overflowPolicy === "auto-shrink") {
10171
- const explicitLineCount = Math.max(1, text.split("\n").length);
10250
+ const minBoxHForShrink = Math.max(0, Number(element.minBoxHeight) || 0);
10251
+ const heightBound = Math.max(rH || 0, minBoxHForShrink);
10172
10252
  while (fontSize > 1) {
10173
10253
  const testTextbox = new fabric__namespace.Textbox(text, {
10174
10254
  width: fixedWidth,
@@ -10178,15 +10258,13 @@ const PageCanvas = react.forwardRef(
10178
10258
  fontStyle: element.fontStyle || "normal",
10179
10259
  lineHeight: element.lineHeight || 1.2,
10180
10260
  charSpacing: element.charSpacing || 0,
10181
- splitByGrapheme: false
10261
+ splitByGrapheme: element.splitByGrapheme ?? false
10182
10262
  });
10183
10263
  testTextbox.initDimensions();
10184
10264
  const textHeight = testTextbox.height || 0;
10185
- const renderedLineCount = ((_b = testTextbox.textLines) == null ? void 0 : _b.length) || 1;
10186
- const hasNoImplicitWrap = renderedLineCount <= explicitLineCount;
10187
- const fitsHeight = rH <= 0 || textHeight <= rH;
10265
+ const fitsHeight = heightBound <= 0 || textHeight <= heightBound;
10188
10266
  const { fitsWidth } = getTextboxWidthFitMetrics(testTextbox, fixedWidth);
10189
- if (hasNoImplicitWrap && fitsHeight && fitsWidth) {
10267
+ if (fitsHeight && fitsWidth) {
10190
10268
  break;
10191
10269
  }
10192
10270
  fontSize--;
@@ -10257,6 +10335,10 @@ const PageCanvas = react.forwardRef(
10257
10335
  splitByGrapheme,
10258
10336
  text
10259
10337
  });
10338
+ const valign = element.verticalAlign || "top";
10339
+ const minBoxH = Math.max(0, Number(element.minBoxHeight) || 0);
10340
+ obj.verticalAlign = valign;
10341
+ obj.minBoxHeight = minBoxH;
10260
10342
  if (element.formattingEnabled === true) {
10261
10343
  obj.styles = parsedStyles || {};
10262
10344
  } else {
@@ -10279,7 +10361,7 @@ const PageCanvas = react.forwardRef(
10279
10361
  } catch {
10280
10362
  }
10281
10363
  obj.dirty = true;
10282
- (_c = obj.setCoords) == null ? void 0 : _c.call(obj);
10364
+ (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
10283
10365
  obj.__lastTextBgShadowJson = JSON.stringify({
10284
10366
  c: element.textBgColor ?? null,
10285
10367
  p: element.textBgPadding ?? 0,
@@ -16036,9 +16118,9 @@ function captureFabricCanvasSvgForPdf(fabricInstance, canvasWidth, canvasHeight)
16036
16118
  }
16037
16119
  return svgString;
16038
16120
  }
16039
- const resolvedPackageVersion = "0.5.165";
16121
+ const resolvedPackageVersion = "0.5.168";
16040
16122
  const PACKAGE_VERSION = resolvedPackageVersion;
16041
- const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.165";
16123
+ const DEPLOYMENT_VERSION_MARKER = "__PIXLDOCS_CANVAS_RENDERER_VERSION__:0.5.168";
16042
16124
  const roundParityValue = (value) => {
16043
16125
  if (typeof value !== "number") return value;
16044
16126
  return Number.isFinite(value) ? Number(value.toFixed(3)) : value;
@@ -16164,6 +16246,68 @@ function installUnderlineFix(fab) {
16164
16246
  __underlineFixInstalled = true;
16165
16247
  console.log(`[canvas-renderer] underline-fix monkey patch installed (v${PACKAGE_VERSION})`);
16166
16248
  }
16249
+ let __textboxBoxExtensionsInstalled = false;
16250
+ function installTextboxBoxExtensions(fab) {
16251
+ var _a;
16252
+ if (__textboxBoxExtensionsInstalled) return;
16253
+ const TextboxProto2 = (_a = fab.Textbox) == null ? void 0 : _a.prototype;
16254
+ if (!TextboxProto2) return;
16255
+ if (TextboxProto2.__pixldocsTextboxExtended) {
16256
+ __textboxBoxExtensionsInstalled = true;
16257
+ return;
16258
+ }
16259
+ if (typeof TextboxProto2.calcTextHeight === "function") {
16260
+ const origCalc = TextboxProto2.calcTextHeight;
16261
+ TextboxProto2.__pixldocsOrigCalcTextHeight = origCalc;
16262
+ TextboxProto2.calcTextHeight = function() {
16263
+ const orig = origCalc.call(this);
16264
+ this._contentHeight = orig;
16265
+ const min = this.minBoxHeight || 0;
16266
+ return min > orig ? min : orig;
16267
+ };
16268
+ }
16269
+ if (typeof TextboxProto2._getTopOffset === "function") {
16270
+ const origTop = TextboxProto2._getTopOffset;
16271
+ TextboxProto2.__pixldocsOrigGetTopOffset = origTop;
16272
+ TextboxProto2._getTopOffset = function() {
16273
+ const baseOffset = origTop.call(this);
16274
+ const valign = this.verticalAlign || "top";
16275
+ if (valign === "top") return baseOffset;
16276
+ const content = typeof this._contentHeight === "number" ? this._contentHeight : TextboxProto2.__pixldocsOrigCalcTextHeight ? TextboxProto2.__pixldocsOrigCalcTextHeight.call(this) : 0;
16277
+ const padding = (this.height || 0) - content;
16278
+ if (padding <= 0) return baseOffset;
16279
+ if (valign === "middle") return baseOffset + padding / 2;
16280
+ if (valign === "bottom") return baseOffset + padding;
16281
+ return baseOffset;
16282
+ };
16283
+ }
16284
+ if (typeof TextboxProto2._getSVGLeftTopOffsets === "function") {
16285
+ const origSvgOffsets = TextboxProto2._getSVGLeftTopOffsets;
16286
+ TextboxProto2.__pixldocsOrigGetSVGLeftTopOffsets = origSvgOffsets;
16287
+ TextboxProto2._getSVGLeftTopOffsets = function() {
16288
+ const base = origSvgOffsets.call(this);
16289
+ const valign = this.verticalAlign || "top";
16290
+ if (valign === "top") return base;
16291
+ const content = typeof this._contentHeight === "number" ? this._contentHeight : TextboxProto2.__pixldocsOrigCalcTextHeight ? TextboxProto2.__pixldocsOrigCalcTextHeight.call(this) : 0;
16292
+ const padding = (this.height || 0) - content;
16293
+ if (padding <= 0) return base;
16294
+ const extra = valign === "middle" ? padding / 2 : padding;
16295
+ return { ...base, textTop: base.textTop + extra };
16296
+ };
16297
+ }
16298
+ const stateProps2 = TextboxProto2.stateProperties;
16299
+ if (Array.isArray(stateProps2)) {
16300
+ if (!stateProps2.includes("minBoxHeight")) stateProps2.push("minBoxHeight");
16301
+ if (!stateProps2.includes("verticalAlign")) stateProps2.push("verticalAlign");
16302
+ }
16303
+ const cacheProps2 = TextboxProto2.cacheProperties;
16304
+ if (Array.isArray(cacheProps2)) {
16305
+ if (!cacheProps2.includes("minBoxHeight")) cacheProps2.push("minBoxHeight");
16306
+ if (!cacheProps2.includes("verticalAlign")) cacheProps2.push("verticalAlign");
16307
+ }
16308
+ TextboxProto2.__pixldocsTextboxExtended = true;
16309
+ __textboxBoxExtensionsInstalled = true;
16310
+ }
16167
16311
  function configHasAutoShrinkText(config) {
16168
16312
  var _a;
16169
16313
  if (!((_a = config == null ? void 0 : config.pages) == null ? void 0 : _a.length)) return false;
@@ -16186,6 +16330,7 @@ class PixldocsRenderer {
16186
16330
  this.config = config;
16187
16331
  this.installRuntimeGlobals();
16188
16332
  installUnderlineFix(fabric__namespace);
16333
+ installTextboxBoxExtensions(fabric__namespace);
16189
16334
  try {
16190
16335
  console.log(`[canvas-renderer] PixldocsRenderer v${PACKAGE_VERSION} initialized`);
16191
16336
  } catch {
@@ -16473,7 +16618,7 @@ class PixldocsRenderer {
16473
16618
  await this.waitForCanvasScene(container, cloned, i);
16474
16619
  }
16475
16620
  console.log(`[canvas-renderer][pdf-unified] mounted ${cloned.pages.length} page(s), handing off to client exportMultiPagePdf`);
16476
- const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-aWseupbk.cjs"));
16621
+ const { exportMultiPagePdf, preparePagesForExport } = await Promise.resolve().then(() => require("./vectorPdfExport-Bw2WQNoU.cjs"));
16477
16622
  const prepared = preparePagesForExport(
16478
16623
  cloned.pages,
16479
16624
  canvasWidth,
@@ -18575,7 +18720,7 @@ async function prepareLiveCanvasSvgForPdf(rawSvg, pageWidth, pageHeight, pageKey
18575
18720
  if (options == null ? void 0 : options.stripPageBackground) stripRootPageBackgroundFromSvg(svgToDraw);
18576
18721
  sanitizeSvgTreeForPdf(svgToDraw);
18577
18722
  try {
18578
- const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-aWseupbk.cjs"));
18723
+ const { bakeTextAnchorPositionsFromLiveSvg, logTextMeasurementDiagnostic } = await Promise.resolve().then(() => require("./vectorPdfExport-Bw2WQNoU.cjs"));
18579
18724
  try {
18580
18725
  await logTextMeasurementDiagnostic(svgToDraw);
18581
18726
  } catch {
@@ -18781,6 +18926,56 @@ async function assemblePdfFromSvgs(svgResults, options = {}) {
18781
18926
  pages: svgResults.map((p) => ({ width: p.width, height: p.height }))
18782
18927
  };
18783
18928
  }
18929
+ const SELECT_COLUMNS = "id,name,description,category,thumbnail_url,preview_images,price,download_count,workspace_id,sort_order,created_at,updated_at";
18930
+ async function listPublishedTemplates(options) {
18931
+ const { workspaceId, supabaseUrl, supabaseAnonKey, category, limit = 200, offset = 0 } = options;
18932
+ if (!workspaceId) throw new Error("listPublishedTemplates: workspaceId is required");
18933
+ if (!supabaseUrl || !supabaseAnonKey) {
18934
+ throw new Error("listPublishedTemplates: supabaseUrl and supabaseAnonKey are required");
18935
+ }
18936
+ const params = new URLSearchParams({
18937
+ select: SELECT_COLUMNS,
18938
+ workspace_id: `eq.${workspaceId}`,
18939
+ status: "eq.published",
18940
+ order: "sort_order.asc,updated_at.desc",
18941
+ limit: String(limit),
18942
+ offset: String(offset)
18943
+ });
18944
+ if (category) params.set("category", `eq.${category}`);
18945
+ const url = `${supabaseUrl.replace(/\/$/, "")}/rest/v1/templates?${params.toString()}`;
18946
+ const res = await fetch(url, {
18947
+ headers: {
18948
+ apikey: supabaseAnonKey,
18949
+ Authorization: `Bearer ${supabaseAnonKey}`,
18950
+ Accept: "application/json"
18951
+ }
18952
+ });
18953
+ if (!res.ok) {
18954
+ const text = await res.text().catch(() => "");
18955
+ throw new Error(`listPublishedTemplates failed: ${res.status} ${text}`);
18956
+ }
18957
+ return await res.json();
18958
+ }
18959
+ async function getPublishedTemplate(options) {
18960
+ const { templateId, supabaseUrl, supabaseAnonKey } = options;
18961
+ const params = new URLSearchParams({
18962
+ select: SELECT_COLUMNS,
18963
+ id: `eq.${templateId}`,
18964
+ status: "eq.published",
18965
+ limit: "1"
18966
+ });
18967
+ const url = `${supabaseUrl.replace(/\/$/, "")}/rest/v1/templates?${params.toString()}`;
18968
+ const res = await fetch(url, {
18969
+ headers: {
18970
+ apikey: supabaseAnonKey,
18971
+ Authorization: `Bearer ${supabaseAnonKey}`,
18972
+ Accept: "application/json"
18973
+ }
18974
+ });
18975
+ if (!res.ok) return null;
18976
+ const rows = await res.json();
18977
+ return rows[0] ?? null;
18978
+ }
18784
18979
  function collectImageUrls(config) {
18785
18980
  const urls = [];
18786
18981
  const walk = (nodes) => {
@@ -18901,6 +19096,7 @@ exports.getCanvasForPage = getCanvasForPage;
18901
19096
  exports.getEmbeddedJsPDFFontName = getEmbeddedJsPDFFontName;
18902
19097
  exports.getImageProxyFetchOptions = getImageProxyFetchOptions;
18903
19098
  exports.getProxiedImageUrl = getProxiedImageUrl;
19099
+ exports.getPublishedTemplate = getPublishedTemplate;
18904
19100
  exports.getRoundedRectRadii = getRoundedRectRadii;
18905
19101
  exports.getTrianglePoints = getTrianglePoints;
18906
19102
  exports.hasEdgeFade = hasEdgeFade;
@@ -18909,6 +19105,7 @@ exports.isElement = isElement;
18909
19105
  exports.isFontAvailable = isFontAvailable;
18910
19106
  exports.isGroup = isGroup;
18911
19107
  exports.isPrivateUrl = isPrivateUrl;
19108
+ exports.listPublishedTemplates = listPublishedTemplates;
18912
19109
  exports.loadGoogleFontCSS = loadGoogleFontCSS;
18913
19110
  exports.normalizeFontFamily = normalizeFontFamily;
18914
19111
  exports.normalizeShapeType = normalizeShapeType;
@@ -18922,4 +19119,4 @@ exports.setAutoShrinkDebug = setAutoShrinkDebug;
18922
19119
  exports.setBundledAssetPrefixes = setBundledAssetPrefixes;
18923
19120
  exports.warmResolvedTemplateForPreview = warmResolvedTemplateForPreview;
18924
19121
  exports.warmTemplateFromForm = warmTemplateFromForm;
18925
- //# sourceMappingURL=index-B8sm4VZ8.cjs.map
19122
+ //# sourceMappingURL=index-DgX2Y94P.cjs.map