@designcrowd/library.brand-page 5.18.7 → 5.18.8-ast-padding-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CZmqvsOT.js → CreatedOnBrandCrowd-de-de-4K_VkRLQ.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-5nTR1eLT.js → CreatedOnBrandCrowd-en-us-n6Fd47e_.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-xLvoayrw.js → CreatedOnBrandCrowd-es-es-BcCANYX_.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CN5n8X8v.js → CreatedOnBrandCrowd-fr-ca-qusGdhEQ.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CN5n8X8v.js → CreatedOnBrandCrowd-fr-fr-qusGdhEQ.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CdkYC12E.js → CreatedOnBrandCrowd-pt-br-C9IBrRc_.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CdkYC12E.js → CreatedOnBrandCrowd-pt-pt-C9IBrRc_.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-B8rV2rD7.js → CreatedOnDcom-de-de-BY13DaXj.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-C4WaRIpF.js → CreatedOnDcom-en-us-BPc5Nacx.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BPHPey0b.js → CreatedOnDcom-es-es-KrsweAGY.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-xrMr4xlv.js → CreatedOnDcom-fr-ca-NaPJNFZH.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BEozL-o2.js → CreatedOnDcom-fr-fr-DjZKRox_.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-CoN-ZhjG.js → CreatedOnDcom-pt-br-xGSMGQZa.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-9vfUJ3ks.js → CreatedOnDcom-pt-pt-B7CcpJ4c.js} +1 -1
  15. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +4 -0
  16. package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +8 -4
  17. package/lib/src/brand-page/components/image-gallery/ImageGallery.mixin.d.ts +4 -0
  18. package/lib/src/brand-page/components/image-gallery/ImageGallery.mixin.js +4 -0
  19. package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +1 -1
  20. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.d.ts +4 -0
  21. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +4 -1
  22. package/lib/src/brand-page/components/media/Media.mixin.d.ts +2 -2
  23. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +4 -0
  24. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -0
  25. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.d.ts +4 -0
  26. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.js +4 -1
  27. package/lib/src/brand-page/components/testimonial-section/TestimonialSection.mixin.d.ts +4 -0
  28. package/lib/src/brand-page/components/testimonial-section/TestimonialSection.mixin.js +4 -1
  29. package/lib/src/brand-page/components/video/Video.mixin.d.ts +4 -0
  30. package/lib/src/brand-page/components/video/Video.mixin.js +4 -0
  31. package/lib/src/brand-page/constants/misc.constants.d.ts +35 -0
  32. package/lib/src/brand-page/constants/misc.constants.js +51 -0
  33. package/lib/src/brand-page/models/content.model.d.ts +2 -0
  34. package/lib/src/brand-page/utils/content-padding.util.d.ts +13 -0
  35. package/lib/src/brand-page/utils/content-padding.util.js +10 -0
  36. package/lib/src/brand-page/utils/index.d.ts +1 -0
  37. package/lib/src/brand-page/utils/index.js +1 -0
  38. package/lib/src/brand-page/utils/migration/migration-17.d.ts +2 -0
  39. package/lib/src/brand-page/utils/migration/migration-17.js +54 -0
  40. package/lib/src/brand-page/utils/migration.util.js +10 -0
  41. package/lib/src/brand-page-maker/components/admin-logo-settings/AdminLogoSettings.mixin.d.ts +2 -0
  42. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +12 -0
  43. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +14 -0
  44. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +16 -3
  45. package/lib/src/brand-page-maker/constants/config-settings-constant.js +39 -0
  46. package/lib/src/brand-page-maker/convertors/about-display-style.convertor.js +9 -1
  47. package/lib/src/brand-page-maker/convertors/banner-align-display-style.convertor.js +7 -1
  48. package/lib/src/brand-page-maker/convertors/contact-layout-style.convertor.js +6 -1
  49. package/lib/src/brand-page-maker/models/content-settings.js +38 -2
  50. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +12 -0
  51. package/lib/src/{bundled-translations-D1q3rSEz.js → bundled-translations-BUyt4scr.js} +9 -9
  52. package/lib/src/{bundled-translations.de-DE-BxqLLwbh.js → bundled-translations.de-DE-DEw_OVai.js} +1 -1
  53. package/lib/src/{bundled-translations.es-ES-uh249NlJ.js → bundled-translations.es-ES-CFKWmkgY.js} +17 -17
  54. package/lib/src/{bundled-translations.fr-CA-BEQmv2fX.js → bundled-translations.fr-CA-DCl5LJS4.js} +1 -1
  55. package/lib/src/{bundled-translations.fr-FR-BnNuCojH.js → bundled-translations.fr-FR-B5BjgllA.js} +1 -1
  56. package/lib/src/{bundled-translations.pt-BR-CJUOxVlq.js → bundled-translations.pt-BR-5PWpMPuM.js} +1 -1
  57. package/lib/src/{bundled-translations.pt-PT-Ds9cMEqK.js → bundled-translations.pt-PT-B9_wN0HR.js} +9 -9
  58. package/lib/src/bundles/bundled-translations.de-DE.json +2 -0
  59. package/lib/src/bundles/bundled-translations.es-ES.json +2 -0
  60. package/lib/src/bundles/bundled-translations.fr-CA.json +2 -0
  61. package/lib/src/bundles/bundled-translations.fr-FR.json +2 -0
  62. package/lib/src/bundles/bundled-translations.json +2 -0
  63. package/lib/src/bundles/bundled-translations.pt-BR.json +2 -0
  64. package/lib/src/bundles/bundled-translations.pt-PT.json +2 -0
  65. package/lib/src/composables/useSnapshot.js +233 -21
  66. package/lib/src/css/library.brand-page-brandCrowd.min.css +147 -161
  67. package/lib/src/css/library.brand-page-brandPage.min.css +147 -161
  68. package/lib/src/css/library.brand-page-designCom.min.css +147 -161
  69. package/lib/src/css/library.brand-page-preview.min.css +147 -161
  70. package/lib/src/index.mjs +1 -1
  71. package/lib/src/{lib-xHNclG2R.js → lib-ClPLZIYL.js} +21178 -20848
  72. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +14 -0
  73. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +14 -0
  74. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +12 -0
  75. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +12 -0
  76. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +12 -0
  77. package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.js +9 -0
  78. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +12 -0
  79. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +16 -0
  80. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +14 -0
  81. package/lib/src/utils/minify-brand-page-metadata.js +11 -1
  82. package/package.json +1 -1
  83. package/src/brand-page/components/about-section/AboutSection.vue +10 -4
  84. package/src/brand-page/components/banner-section/BannerSection.vue +11 -4
  85. package/src/brand-page/components/booking-section/BookingSection.vue +11 -3
  86. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +12 -6
  87. package/src/brand-page/components/contact-form/ContactForm.vue +105 -104
  88. package/src/brand-page/components/image-gallery/ImageGallery.mixin.ts +5 -0
  89. package/src/brand-page/components/image-gallery/ImageGallery.vue +1 -1
  90. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -1
  91. package/src/brand-page/components/mailing-list/MailingList.vue +1 -1
  92. package/src/brand-page/components/map/Map.vue +5 -1
  93. package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +5 -0
  94. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
  95. package/src/brand-page/components/payment/support-me/SupportMe.mixin.ts +8 -1
  96. package/src/brand-page/components/payment/support-me/SupportMe.vue +1 -0
  97. package/src/brand-page/components/shop/Shop.vue +7 -2
  98. package/src/brand-page/components/social-links/SocialLinks.vue +7 -3
  99. package/src/brand-page/components/testimonial-section/TestimonialSection.mixin.ts +7 -1
  100. package/src/brand-page/components/testimonial-section/TestimonialSection.vue +4 -6
  101. package/src/brand-page/components/text/Text.vue +12 -5
  102. package/src/brand-page/components/text-media-section/TextMediaSection.vue +11 -3
  103. package/src/brand-page/components/video/Video.mixin.ts +5 -0
  104. package/src/brand-page/components/video/Video.vue +1 -1
  105. package/src/brand-page/constants/misc.constants.ts +58 -0
  106. package/src/brand-page/models/content.model.ts +2 -0
  107. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +9 -3
  108. package/src/brand-page/utils/content-padding.util.test.ts +29 -0
  109. package/src/brand-page/utils/content-padding.util.ts +13 -0
  110. package/src/brand-page/utils/index.ts +1 -0
  111. package/src/brand-page/utils/migration/migration-17.ts +72 -0
  112. package/src/brand-page/utils/migration.util.test.ts +252 -0
  113. package/src/brand-page/utils/migration.util.ts +11 -0
  114. package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +3 -4
  115. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +8 -1
  116. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +19 -4
  117. package/src/brand-page-maker/constants/config-settings-constant.ts +45 -0
  118. package/src/brand-page-maker/convertors/about-display-style.convertor.ts +10 -0
  119. package/src/brand-page-maker/convertors/banner-align-display-style.convertor.ts +8 -0
  120. package/src/brand-page-maker/convertors/contact-layout-style.convertor.test.ts +50 -0
  121. package/src/brand-page-maker/convertors/contact-layout-style.convertor.ts +8 -0
  122. package/src/brand-page-maker/models/content-settings.ts +41 -2
  123. package/src/bundles/bundled-translations.de-DE.json +2 -0
  124. package/src/bundles/bundled-translations.es-ES.json +2 -0
  125. package/src/bundles/bundled-translations.fr-CA.json +2 -0
  126. package/src/bundles/bundled-translations.fr-FR.json +2 -0
  127. package/src/bundles/bundled-translations.json +2 -0
  128. package/src/bundles/bundled-translations.pt-BR.json +2 -0
  129. package/src/bundles/bundled-translations.pt-PT.json +2 -0
  130. package/src/composables/useSnapshot.ts +316 -31
  131. package/src/shared/content-settings/components/content-settings/i18n/content-settings.de-DE.json +2 -0
  132. package/src/shared/content-settings/components/content-settings/i18n/content-settings.es-ES.json +2 -0
  133. package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-CA.json +2 -0
  134. package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-FR.json +2 -0
  135. package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +2 -0
  136. package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-BR.json +2 -0
  137. package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-PT.json +2 -0
  138. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.vue +1 -1
  139. package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.vue +2 -2
  140. package/src/shared/content-settings/components/slider-field/SliderField.mixin.ts +9 -0
  141. package/src/utils/minify-brand-page-metadata.ts +11 -1
  142. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
  143. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
  144. package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
@@ -5,6 +5,68 @@ const HTML2CANVAS_CDN_URL =
5
5
 
6
6
  let html2canvasLoadPromise: Promise<void> | null = null;
7
7
 
8
+ // Capture is sized to the rendered content (scrollHeight), not the clipped
9
+ // viewport box. An empty page yields a short capture; a long page captures its
10
+ // full height up to the cap.
11
+ const MIN_SNAPSHOT_HEIGHT = 200;
12
+ // The snapshot is consumed by the AI pipeline's OpenAI vision model, which
13
+ // normalises the SHORT side to ~768px and tiles the long side — so token cost
14
+ // is driven by page HEIGHT, not absolute resolution. This cap bounds that cost
15
+ // AND, more importantly, the html2canvas render work (CPU/memory) — taller pages
16
+ // are truncated. The render is the dominant cost on a capture, so we keep the
17
+ // cap conservative, and lower still on mobile where memory/CPU are tighter.
18
+ const MAX_SNAPSHOT_HEIGHT_DESKTOP = 4096;
19
+ const MAX_SNAPSHOT_HEIGHT_MOBILE = 2560;
20
+ // OpenAI's vision short-side target. Rendering at this width avoids the
21
+ // down-then-up resample blur you'd get from scaling smaller (OpenAI upscales
22
+ // the short side back to 768 regardless) while keeping the payload tight.
23
+ const SNAPSHOT_TARGET_WIDTH = 768;
24
+
25
+ /**
26
+ * Extracts the YouTube video id from an embed URL, or null for non-YouTube
27
+ * iframes. Handles youtube.com/embed/<id> and youtube-nocookie.com variants.
28
+ */
29
+ function getYouTubeId(src: string): string | null {
30
+ const match = src.match(
31
+ /(?:youtube(?:-nocookie)?\.com\/embed\/)([A-Za-z0-9_-]{6,})/,
32
+ );
33
+
34
+ return match ? match[1] : null;
35
+ }
36
+
37
+ /**
38
+ * Fetches a YouTube thumbnail and returns it as a data URI, or null on failure.
39
+ * Inlining as a data URI avoids cross-origin canvas tainting in html2canvas.
40
+ * Tries maxres first, falls back to hq (maxres 404s on some videos).
41
+ */
42
+ async function fetchYouTubeThumbnail(id: string): Promise<string | null> {
43
+ for (const quality of ['maxresdefault', 'hqdefault']) {
44
+ try {
45
+ const response = await fetch(
46
+ `https://img.youtube.com/vi/${id}/${quality}.jpg`,
47
+ );
48
+
49
+ if (!response.ok) continue;
50
+
51
+ const blob = await response.blob();
52
+
53
+ // YouTube returns a tiny placeholder for missing qualities; skip those.
54
+ if (blob.size < 2000) continue;
55
+
56
+ return await new Promise<string>((resolve) => {
57
+ const reader = new FileReader();
58
+
59
+ reader.onload = () => resolve(reader.result as string);
60
+ reader.readAsDataURL(blob);
61
+ });
62
+ } catch {
63
+ /* try next quality */
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
8
70
  /**
9
71
  * Dynamically loads html2canvas from CDN if not already loaded
10
72
  */
@@ -67,22 +129,74 @@ export function useSnapshot() {
67
129
  throw new Error('html2canvas failed to load');
68
130
  }
69
131
 
70
- // Pre-fetch blob URLs from shape <object> elements so they can be
71
- // rendered as <img> in the html2canvas clone (html2canvas cannot
72
- // traverse the isolated DOM inside <object> elements).
73
- const shapeDataUris = new Map<string, string>();
74
- const shapeObjects = element.querySelectorAll('.c-shape-object object');
75
-
76
- for (const obj of Array.from(shapeObjects)) {
77
- const dataUrl = obj.getAttribute('data');
78
-
79
- if (dataUrl && dataUrl.startsWith('blob:')) {
132
+ // Wait for fonts and in-flight images to finish before capturing —
133
+ // otherwise html2canvas snapshots a half-rendered page (missing images,
134
+ // fallback fonts) and the measured content height is wrong.
135
+ await document.fonts?.ready;
136
+ await Promise.all(
137
+ Array.from(element.querySelectorAll('img'))
138
+ .filter((img) => !img.complete)
139
+ .map(
140
+ (img) =>
141
+ new Promise<void>((resolve) => {
142
+ img.onload = img.onerror = () => resolve();
143
+ }),
144
+ ),
145
+ );
146
+
147
+ // The maker renders the full website inside `.c-preview-container`
148
+ // (position: fixed + transform: scale). The page background — solid
149
+ // colour, gradient, or image — lives on `.brand-page-root-background`,
150
+ // which sits OUTSIDE the scrolling content. Capturing the content alone
151
+ // drops that background, so transparent sections fall back to white and
152
+ // their (often light) text becomes invisible. Capture the whole preview
153
+ // (`[data-test-tag="editor-preview"]`) so the background layer is
154
+ // included, and size the canvas to the real page length, which only the
155
+ // inner scroll container `.brand-page__inner` reports via scrollHeight.
156
+ const target =
157
+ element.querySelector<HTMLElement>(
158
+ '[data-test-tag="editor-preview"]',
159
+ ) ?? element;
160
+ const scrollEl =
161
+ element.querySelector<HTMLElement>('.brand-page__inner') ?? target;
162
+
163
+ // Keep the render light on mobile (tighter CPU/memory) by truncating
164
+ // taller there than on desktop.
165
+ const isMobileViewport =
166
+ typeof window !== 'undefined' &&
167
+ window.matchMedia?.('(max-width: 768px)').matches;
168
+ const maxHeight = isMobileViewport
169
+ ? MAX_SNAPSHOT_HEIGHT_MOBILE
170
+ : MAX_SNAPSHOT_HEIGHT_DESKTOP;
171
+
172
+ const captureHeight = Math.min(
173
+ Math.max(scrollEl.scrollHeight, MIN_SNAPSHOT_HEIGHT),
174
+ maxHeight,
175
+ );
176
+
177
+ // Render at OpenAI's short-side target width (never upscaling). Token cost
178
+ // is set by height/aspect ratio, not by scaling down the width, so we keep
179
+ // the width crisp and let MAX_SNAPSHOT_HEIGHT bound the cost.
180
+ const captureWidth = target.clientWidth || target.offsetWidth || 1;
181
+ const scale = Math.min(1, SNAPSHOT_TARGET_WIDTH / captureWidth);
182
+
183
+ // Pre-fetch blob-backed SVGs from content shape <object> elements so they
184
+ // can be rendered as <img> in the clone (html2canvas can't reach into the
185
+ // isolated DOM inside <object>). Background-shape blob <img>s are NOT
186
+ // converted: html2canvas renders them no differently with or without the
187
+ // conversion, so it'd just be wasted fetches.
188
+ const blobSvgUris = new Map<string, string>();
189
+ const blobSources = Array.from(
190
+ element.querySelectorAll<HTMLObjectElement>('.c-shape-object object'),
191
+ ).map((o) => o.getAttribute('data'));
192
+
193
+ for (const src of blobSources) {
194
+ if (src && src.startsWith('blob:') && !blobSvgUris.has(src)) {
80
195
  try {
81
- const response = await fetch(dataUrl);
82
- const svgText = await response.text();
196
+ const svgText = await (await fetch(src)).text();
83
197
 
84
- shapeDataUris.set(
85
- dataUrl,
198
+ blobSvgUris.set(
199
+ src,
86
200
  `data:image/svg+xml,${encodeURIComponent(svgText)}`,
87
201
  );
88
202
  } catch {
@@ -91,34 +205,205 @@ export function useSnapshot() {
91
205
  }
92
206
  }
93
207
 
94
- const canvas = await window.html2canvas(element, {
95
- allowTaint: true,
208
+ // html2canvas ignores `object-fit`, so it stretches images to their box
209
+ // instead of cropping/containing them — section images (cover) come out
210
+ // distorted. Record the rendered box + fit of every object-fit image from
211
+ // the live (laid-out) DOM, keyed by src, so the clone can swap them for a
212
+ // background-size div, which html2canvas renders correctly.
213
+ interface ObjectFitInfo {
214
+ fit: string;
215
+ position: string;
216
+ width: number;
217
+ height: number;
218
+ }
219
+
220
+ const objectFitImages = new Map<string, ObjectFitInfo>();
221
+
222
+ for (const img of Array.from(
223
+ element.querySelectorAll<HTMLImageElement>('img'),
224
+ )) {
225
+ const { objectFit, objectPosition } = getComputedStyle(img);
226
+
227
+ if (objectFit !== 'cover' && objectFit !== 'contain') continue;
228
+
229
+ const rect = img.getBoundingClientRect();
230
+ const src = img.currentSrc || img.src;
231
+
232
+ if (src && rect.width > 2 && rect.height > 2) {
233
+ objectFitImages.set(src, {
234
+ fit: objectFit,
235
+ position: objectPosition || 'center',
236
+ width: Math.round(rect.width),
237
+ height: Math.round(rect.height),
238
+ });
239
+ }
240
+ }
241
+
242
+ // html2canvas can't render <iframe> embeds (e.g. YouTube videos) — they
243
+ // come out blank. Pre-fetch each embed's thumbnail as a data URI, keyed by
244
+ // video id, so the clone can swap the iframe for a poster image.
245
+ interface VideoThumb {
246
+ dataUri: string;
247
+ width: number;
248
+ height: number;
249
+ }
250
+
251
+ const videoThumbs = new Map<string, VideoThumb>();
252
+
253
+ for (const iframe of Array.from(
254
+ element.querySelectorAll<HTMLIFrameElement>('iframe'),
255
+ )) {
256
+ const id = getYouTubeId(iframe.src);
257
+
258
+ if (!id || videoThumbs.has(id)) continue;
259
+
260
+ const dataUri = await fetchYouTubeThumbnail(id);
261
+
262
+ if (!dataUri) continue;
263
+
264
+ const rect = iframe.getBoundingClientRect();
265
+
266
+ videoThumbs.set(id, {
267
+ dataUri,
268
+ width: Math.round(rect.width) || iframe.clientWidth,
269
+ height: Math.round(rect.height) || iframe.clientHeight,
270
+ });
271
+ }
272
+
273
+ const canvas = await window.html2canvas(target, {
274
+ // Must stay false: with allowTaint, html2canvas draws cross-origin
275
+ // media that lacks CORS (e.g. a Getty background <video>), which taints
276
+ // the canvas and makes toDataURL throw — failing the whole snapshot.
277
+ // With it off, such content is skipped (rendered blank) instead, so the
278
+ // snapshot always exports. CORS-enabled images (Unsplash, user uploads)
279
+ // still render via useCORS.
280
+ allowTaint: false,
96
281
  useCORS: true,
97
- scale: 0.8, // Reduce quality to decrease file size
282
+ scale, // fit the full page into the vision model's image budget
98
283
  logging: false,
99
284
  backgroundColor: '#ffffff',
100
- onclone: (_doc: Document, clonedElement: HTMLElement) => {
101
- const clonedObjects = clonedElement.querySelectorAll(
102
- '.c-shape-object object',
103
- );
285
+ // Capture the full page, not just the scroll viewport.
286
+ height: captureHeight,
287
+ windowHeight: captureHeight,
288
+ scrollX: 0,
289
+ scrollY: 0,
290
+ x: 0,
291
+ y: 0,
292
+ onclone: (_doc: Document) => {
293
+ const setStyle = (
294
+ selector: string,
295
+ styles: Partial<CSSStyleDeclaration>,
296
+ ) => {
297
+ const el = _doc.querySelector<HTMLElement>(selector);
298
+
299
+ if (el) Object.assign(el.style, styles);
300
+ };
301
+
302
+ // Neutralise the fixed-position + scale wrapper and the scroll clip so
303
+ // the clone lays out at full content height; otherwise html2canvas
304
+ // honours the viewport-height box and the page is cut off.
305
+ setStyle('.c-preview-container', {
306
+ position: 'static',
307
+ transform: 'none',
308
+ height: 'auto',
309
+ overflow: 'visible',
310
+ });
311
+ setStyle('[data-test-tag="editor-preview"]', {
312
+ position: 'relative',
313
+ height: `${captureHeight}px`,
314
+ overflow: 'visible',
315
+ });
316
+ // Stretch the (normally fixed, viewport-height) background and shape
317
+ // layers to cover the full page so transparent sections render their
318
+ // intended background instead of the white fallback.
319
+ setStyle('.brand-page-root-background', {
320
+ position: 'absolute',
321
+ top: '0',
322
+ left: '0',
323
+ width: '100%',
324
+ height: '100%',
325
+ });
326
+ setStyle('.brand-page__background-shapes-layer', {
327
+ position: 'absolute',
328
+ top: '0',
329
+ left: '0',
330
+ width: '100%',
331
+ height: '100%',
332
+ });
333
+ setStyle('.brand-page-root', {
334
+ position: 'relative',
335
+ height: 'auto',
336
+ });
337
+ setStyle('.brand-page', { height: 'auto' });
338
+ setStyle('.brand-page__inner', {
339
+ height: 'auto',
340
+ overflow: 'visible',
341
+ });
104
342
 
105
- clonedObjects.forEach((obj) => {
106
- const dataUrl = obj.getAttribute('data');
343
+ // Swap content shape <object>s for <img> using the pre-fetched data
344
+ // URIs (html2canvas can't render <object> SVGs directly).
345
+ _doc
346
+ .querySelectorAll<HTMLObjectElement>('.c-shape-object object')
347
+ .forEach((obj) => {
348
+ const dataUrl = obj.getAttribute('data');
107
349
 
108
- if (dataUrl && shapeDataUris.has(dataUrl)) {
109
- const img = _doc.createElement('img');
350
+ if (dataUrl && blobSvgUris.has(dataUrl)) {
351
+ const img = _doc.createElement('img');
110
352
 
111
- img.src = shapeDataUris.get(dataUrl)!;
353
+ img.src = blobSvgUris.get(dataUrl)!;
112
354
 
113
- const style = (obj as HTMLElement).getAttribute('style');
355
+ const style = obj.getAttribute('style');
114
356
 
115
- if (style) {
116
- img.setAttribute('style', style);
117
- }
357
+ if (style) img.setAttribute('style', style);
118
358
 
119
- obj.parentNode?.replaceChild(img, obj);
359
+ obj.parentNode?.replaceChild(img, obj);
360
+ }
361
+ });
362
+
363
+ // Replace object-fit images with a background-size div so they
364
+ // render cropped/contained instead of stretched.
365
+ _doc.querySelectorAll<HTMLImageElement>('img').forEach((img) => {
366
+ const src = img.currentSrc || img.src;
367
+ const fitInfo = objectFitImages.get(src);
368
+
369
+ if (fitInfo) {
370
+ const div = _doc.createElement('div');
371
+
372
+ div.className = img.className;
373
+ div.style.cssText = img.getAttribute('style') || '';
374
+ div.style.backgroundImage = `url("${img.src}")`;
375
+ div.style.backgroundSize = fitInfo.fit;
376
+ div.style.backgroundPosition = fitInfo.position;
377
+ div.style.backgroundRepeat = 'no-repeat';
378
+ div.style.width = `${fitInfo.width}px`;
379
+ div.style.height = `${fitInfo.height}px`;
380
+
381
+ img.parentNode?.replaceChild(div, img);
120
382
  }
121
383
  });
384
+
385
+ // Replace video iframes (blank in html2canvas) with their poster
386
+ // thumbnail rendered as a background-size div.
387
+ _doc
388
+ .querySelectorAll<HTMLIFrameElement>('iframe')
389
+ .forEach((iframe) => {
390
+ const id = getYouTubeId(iframe.src);
391
+ const thumb = id ? videoThumbs.get(id) : undefined;
392
+
393
+ if (!thumb) return;
394
+
395
+ const div = _doc.createElement('div');
396
+
397
+ div.style.cssText = iframe.getAttribute('style') || '';
398
+ div.style.width = `${thumb.width}px`;
399
+ div.style.height = `${thumb.height}px`;
400
+ div.style.backgroundImage = `url("${thumb.dataUri}")`;
401
+ div.style.backgroundSize = 'cover';
402
+ div.style.backgroundPosition = 'center';
403
+ div.style.backgroundColor = '#000000';
404
+
405
+ iframe.parentNode?.replaceChild(div, iframe);
406
+ });
122
407
  },
123
408
  });
124
409
 
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Farbe der Symbolfüllung",
114
114
  "iconBackgroundColorTitle": "Symbolhintergrundfarbe",
115
115
  "paddingTitle": "Polsterung",
116
+ "topPaddingTitle": "Polsterung oben",
117
+ "bottomPaddingTitle": "Polsterung unten",
116
118
  "dividerStyleTitle": "Trennlinienstil",
117
119
  "dividerColorTitle": "Trennlinienfarbe",
118
120
  "addItemTitle": "Artikel hinzufügen",
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Color de relleno del icono",
114
114
  "iconBackgroundColorTitle": "Color de fondo del icono",
115
115
  "paddingTitle": "Relleno",
116
+ "topPaddingTitle": "Relleno superior",
117
+ "bottomPaddingTitle": "Relleno inferior",
116
118
  "dividerStyleTitle": "Estilo del divisor",
117
119
  "dividerColorTitle": "Color del separador",
118
120
  "addItemTitle": "Añadir artículo",
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Couleur de remplissage de l’icône",
114
114
  "iconBackgroundColorTitle": "Couleur d’arrière-plan de l’icône",
115
115
  "paddingTitle": "Marges intérieures",
116
+ "topPaddingTitle": "Marge intérieure supérieure",
117
+ "bottomPaddingTitle": "Marges intérieures inférieures",
116
118
  "dividerStyleTitle": "Style de séparateur",
117
119
  "dividerColorTitle": "Couleur du séparateur",
118
120
  "addItemTitle": "Ajouter un article",
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Couleur de remplissage de l’icône",
114
114
  "iconBackgroundColorTitle": "Couleur d’arrière-plan de l’icône",
115
115
  "paddingTitle": "Marges intérieures",
116
+ "topPaddingTitle": "Marges intérieures supérieures",
117
+ "bottomPaddingTitle": "Marges intérieures inférieures",
116
118
  "dividerStyleTitle": "Style de séparateur",
117
119
  "dividerColorTitle": "Couleur du séparateur",
118
120
  "addItemTitle": "Ajouter un article",
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Icon fill color",
114
114
  "iconBackgroundColorTitle": "Icon background color",
115
115
  "paddingTitle": "Padding",
116
+ "topPaddingTitle": "Top padding",
117
+ "bottomPaddingTitle": "Bottom padding",
116
118
  "dividerStyleTitle": "Divider Style",
117
119
  "dividerColorTitle": "Divider color",
118
120
  "addItemTitle": "Add Item",
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Cor de preenchimento do ícone",
114
114
  "iconBackgroundColorTitle": "Cor de fundo do ícone",
115
115
  "paddingTitle": "Preenchimento",
116
+ "topPaddingTitle": "Preenchimento superior",
117
+ "bottomPaddingTitle": "Preenchimento inferior",
116
118
  "dividerStyleTitle": "Estilo do separador",
117
119
  "dividerColorTitle": "Cor do separador",
118
120
  "addItemTitle": "Adicionar item",
@@ -113,6 +113,8 @@
113
113
  "iconFillColorTitle": "Cor de preenchimento do ícone",
114
114
  "iconBackgroundColorTitle": "Cor de fundo do ícone",
115
115
  "paddingTitle": "Preenchimento",
116
+ "topPaddingTitle": "Preenchimento superior",
117
+ "bottomPaddingTitle": "Preenchimento inferior",
116
118
  "dividerStyleTitle": "Estilo do separador",
117
119
  "dividerColorTitle": "Cor do separador",
118
120
  "addItemTitle": "Adicionar Item",
@@ -56,7 +56,7 @@
56
56
  >
57
57
  <li
58
58
  v-for="(field, index) in fields"
59
- :key="`content-settings-item-field-${id}-${index}`"
59
+ :key="`content-settings-item-field-${id}-${field.name}`"
60
60
  class="tw-cursor-pointer"
61
61
  :class="{
62
62
  'tw-mb-3': index !== fields.length - 1,
@@ -25,8 +25,8 @@
25
25
  >
26
26
  <ul>
27
27
  <li
28
- v-for="(field, fieldIndex) in group.fields"
29
- :key="`content-settings-item-group-field-${id}-${group.id}-${fieldIndex}`"
28
+ v-for="field in group.fields"
29
+ :key="`content-settings-item-group-field-${id}-${group.id}-${field.name}`"
30
30
  >
31
31
  <ContentSettingsItemField
32
32
  v-if="
@@ -45,6 +45,15 @@ export default defineComponent({
45
45
  sliderValue: value,
46
46
  };
47
47
  },
48
+ watch: {
49
+ // The value can change outside the slider (e.g. a layout switch
50
+ // re-applying default padding, or undo) — keep the display in sync.
51
+ value(newValue: string | undefined) {
52
+ this.sliderValue = this.valueConvertor
53
+ ? this.valueConvertor.convert(newValue as any)
54
+ : newValue;
55
+ },
56
+ },
48
57
  computed: {
49
58
  innerMinLength(): number {
50
59
  return this.minLength ?? 0;
@@ -205,7 +205,17 @@ const minifyContent = (
205
205
  editor: Editor,
206
206
  assets?: AssetEntry[],
207
207
  ): MinifiedContent => {
208
- const base: MinifiedContent = { id: content.id, type: content.type };
208
+ const base: MinifiedContent = {
209
+ id: content.id,
210
+ type: content.type,
211
+ // Padding Control (BP-5687) — shared across every block, persist when set.
212
+ ...(typeof content.topPadding === 'number' && {
213
+ topPadding: content.topPadding,
214
+ }),
215
+ ...(typeof content.bottomPadding === 'number' && {
216
+ bottomPadding: content.bottomPadding,
217
+ }),
218
+ };
209
219
  const c = content as any;
210
220
 
211
221
  // Helper: include imageUrl and its asset key if available
@@ -1,13 +0,0 @@
1
- import type { BrandPageModel } from '../../brand-page/models';
2
- import { BaseMetadataBuilder, type BuilderResult, type MetadataBuilderRequest } from '../models';
3
- import type { Logger } from '../logger';
4
- export interface ReorderPagesPayload {
5
- pageOrder: string[];
6
- }
7
- export declare class ReorderPagesBuilder extends BaseMetadataBuilder<ReorderPagesPayload> {
8
- constructor(request: MetadataBuilderRequest<ReorderPagesPayload>, logger?: Logger);
9
- validateAsync(): Promise<{
10
- errors: string[];
11
- }>;
12
- buildAsync(model: BrandPageModel): Promise<BuilderResult>;
13
- }
@@ -1,49 +0,0 @@
1
- import { BaseMetadataBuilder, } from '../models';
2
- export class ReorderPagesBuilder extends BaseMetadataBuilder {
3
- constructor(request, logger) {
4
- super(request, logger);
5
- }
6
- async validateAsync() {
7
- const errors = [];
8
- const payload = this.request.payload;
9
- if (!payload?.pageOrder?.length) {
10
- errors.push('page_order is required for reorder_pages');
11
- }
12
- return { errors };
13
- }
14
- async buildAsync(model) {
15
- this.log('Build called');
16
- const validation = await this.validateAsync();
17
- if (validation.errors.length) {
18
- return { request: this.request, model, errors: validation.errors };
19
- }
20
- const { pageOrder } = this.request.payload;
21
- // Build pageId → routeId lookup from nav.routes
22
- const pageIdToRouteId = new Map();
23
- for (const route of model.nav.routes) {
24
- pageIdToRouteId.set(route.pageId, route.id);
25
- }
26
- // Build new nav.orders from the flat page_order (array of pageIds)
27
- const newOrders = [];
28
- const seenRouteIds = new Set();
29
- for (const pageId of pageOrder) {
30
- const routeId = pageIdToRouteId.get(pageId);
31
- if (!routeId)
32
- continue;
33
- newOrders.push({ routeId, subRoutes: [] });
34
- seenRouteIds.add(routeId);
35
- }
36
- // Safety: append any routes not mentioned (don't lose pages)
37
- for (const route of model.nav.routes) {
38
- const routeId = pageIdToRouteId.get(route.pageId);
39
- if (routeId && !seenRouteIds.has(routeId)) {
40
- newOrders.push({ routeId, subRoutes: [] });
41
- }
42
- }
43
- model.nav.orders = newOrders;
44
- this.log({
45
- message: `Reordered pages: ${pageOrder.join(', ')}`,
46
- });
47
- return { request: this.request, model };
48
- }
49
- }
@@ -1,80 +0,0 @@
1
- import type { BrandPageModel, NavOrder } from '../../brand-page/models';
2
- import {
3
- BaseMetadataBuilder,
4
- type BuilderResult,
5
- type MetadataBuilderRequest,
6
- } from '../models';
7
- import type { Logger } from '../logger';
8
-
9
- export interface ReorderPagesPayload {
10
- pageOrder: string[];
11
- }
12
-
13
- export class ReorderPagesBuilder extends BaseMetadataBuilder<ReorderPagesPayload> {
14
- constructor(
15
- request: MetadataBuilderRequest<ReorderPagesPayload>,
16
- logger?: Logger,
17
- ) {
18
- super(request, logger);
19
- }
20
-
21
- async validateAsync(): Promise<{ errors: string[] }> {
22
- const errors: string[] = [];
23
- const payload = this.request.payload;
24
-
25
- if (!payload?.pageOrder?.length) {
26
- errors.push('page_order is required for reorder_pages');
27
- }
28
-
29
- return { errors };
30
- }
31
-
32
- async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
33
- this.log('Build called');
34
-
35
- const validation = await this.validateAsync();
36
-
37
- if (validation.errors.length) {
38
- return { request: this.request, model, errors: validation.errors };
39
- }
40
-
41
- const { pageOrder } = this.request.payload!;
42
-
43
- // Build pageId → routeId lookup from nav.routes
44
- const pageIdToRouteId = new Map<string, string>();
45
-
46
- for (const route of model.nav.routes) {
47
- pageIdToRouteId.set(route.pageId, route.id);
48
- }
49
-
50
- // Build new nav.orders from the flat page_order (array of pageIds)
51
- const newOrders: NavOrder[] = [];
52
- const seenRouteIds = new Set<string>();
53
-
54
- for (const pageId of pageOrder) {
55
- const routeId = pageIdToRouteId.get(pageId);
56
-
57
- if (!routeId) continue;
58
-
59
- newOrders.push({ routeId, subRoutes: [] });
60
- seenRouteIds.add(routeId);
61
- }
62
-
63
- // Safety: append any routes not mentioned (don't lose pages)
64
- for (const route of model.nav.routes) {
65
- const routeId = pageIdToRouteId.get(route.pageId);
66
-
67
- if (routeId && !seenRouteIds.has(routeId)) {
68
- newOrders.push({ routeId, subRoutes: [] });
69
- }
70
- }
71
-
72
- model.nav.orders = newOrders;
73
-
74
- this.log({
75
- message: `Reordered pages: ${pageOrder.join(', ')}`,
76
- });
77
-
78
- return { request: this.request, model };
79
- }
80
- }