@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
@@ -1529,6 +1529,8 @@
1529
1529
  "iconFillColorTitle": "Color de relleno del icono",
1530
1530
  "iconBackgroundColorTitle": "Color de fondo del icono",
1531
1531
  "paddingTitle": "Relleno",
1532
+ "topPaddingTitle": "Relleno superior",
1533
+ "bottomPaddingTitle": "Relleno inferior",
1532
1534
  "dividerStyleTitle": "Estilo del divisor",
1533
1535
  "dividerColorTitle": "Color del separador",
1534
1536
  "addItemTitle": "Añadir artículo",
@@ -1529,6 +1529,8 @@
1529
1529
  "iconFillColorTitle": "Couleur de remplissage de l’icône",
1530
1530
  "iconBackgroundColorTitle": "Couleur d’arrière-plan de l’icône",
1531
1531
  "paddingTitle": "Marges intérieures",
1532
+ "topPaddingTitle": "Marge intérieure supérieure",
1533
+ "bottomPaddingTitle": "Marges intérieures inférieures",
1532
1534
  "dividerStyleTitle": "Style de séparateur",
1533
1535
  "dividerColorTitle": "Couleur du séparateur",
1534
1536
  "addItemTitle": "Ajouter un article",
@@ -1529,6 +1529,8 @@
1529
1529
  "iconFillColorTitle": "Couleur de remplissage de l’icône",
1530
1530
  "iconBackgroundColorTitle": "Couleur d’arrière-plan de l’icône",
1531
1531
  "paddingTitle": "Marges intérieures",
1532
+ "topPaddingTitle": "Marges intérieures supérieures",
1533
+ "bottomPaddingTitle": "Marges intérieures inférieures",
1532
1534
  "dividerStyleTitle": "Style de séparateur",
1533
1535
  "dividerColorTitle": "Couleur du séparateur",
1534
1536
  "addItemTitle": "Ajouter un article",
@@ -1529,6 +1529,8 @@
1529
1529
  "iconFillColorTitle": "Icon fill color",
1530
1530
  "iconBackgroundColorTitle": "Icon background color",
1531
1531
  "paddingTitle": "Padding",
1532
+ "topPaddingTitle": "Top padding",
1533
+ "bottomPaddingTitle": "Bottom padding",
1532
1534
  "dividerStyleTitle": "Divider Style",
1533
1535
  "dividerColorTitle": "Divider color",
1534
1536
  "addItemTitle": "Add Item",
@@ -1529,6 +1529,8 @@
1529
1529
  "iconFillColorTitle": "Cor de preenchimento do ícone",
1530
1530
  "iconBackgroundColorTitle": "Cor de fundo do ícone",
1531
1531
  "paddingTitle": "Preenchimento",
1532
+ "topPaddingTitle": "Preenchimento superior",
1533
+ "bottomPaddingTitle": "Preenchimento inferior",
1532
1534
  "dividerStyleTitle": "Estilo do separador",
1533
1535
  "dividerColorTitle": "Cor do separador",
1534
1536
  "addItemTitle": "Adicionar item",
@@ -1529,6 +1529,8 @@
1529
1529
  "iconFillColorTitle": "Cor de preenchimento do ícone",
1530
1530
  "iconBackgroundColorTitle": "Cor de fundo do ícone",
1531
1531
  "paddingTitle": "Preenchimento",
1532
+ "topPaddingTitle": "Preenchimento superior",
1533
+ "bottomPaddingTitle": "Preenchimento inferior",
1532
1534
  "dividerStyleTitle": "Estilo do separador",
1533
1535
  "dividerColorTitle": "Cor do separador",
1534
1536
  "addItemTitle": "Adicionar Item",
@@ -1,6 +1,57 @@
1
1
  import { ref } from 'vue';
2
2
  const HTML2CANVAS_CDN_URL = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
3
3
  let html2canvasLoadPromise = null;
4
+ // Capture is sized to the rendered content (scrollHeight), not the clipped
5
+ // viewport box. An empty page yields a short capture; a long page captures its
6
+ // full height up to the cap.
7
+ const MIN_SNAPSHOT_HEIGHT = 200;
8
+ // The snapshot is consumed by the AI pipeline's OpenAI vision model, which
9
+ // normalises the SHORT side to ~768px and tiles the long side — so token cost
10
+ // is driven by page HEIGHT, not absolute resolution. This cap bounds that cost
11
+ // AND, more importantly, the html2canvas render work (CPU/memory) — taller pages
12
+ // are truncated. The render is the dominant cost on a capture, so we keep the
13
+ // cap conservative, and lower still on mobile where memory/CPU are tighter.
14
+ const MAX_SNAPSHOT_HEIGHT_DESKTOP = 4096;
15
+ const MAX_SNAPSHOT_HEIGHT_MOBILE = 2560;
16
+ // OpenAI's vision short-side target. Rendering at this width avoids the
17
+ // down-then-up resample blur you'd get from scaling smaller (OpenAI upscales
18
+ // the short side back to 768 regardless) while keeping the payload tight.
19
+ const SNAPSHOT_TARGET_WIDTH = 768;
20
+ /**
21
+ * Extracts the YouTube video id from an embed URL, or null for non-YouTube
22
+ * iframes. Handles youtube.com/embed/<id> and youtube-nocookie.com variants.
23
+ */
24
+ function getYouTubeId(src) {
25
+ const match = src.match(/(?:youtube(?:-nocookie)?\.com\/embed\/)([A-Za-z0-9_-]{6,})/);
26
+ return match ? match[1] : null;
27
+ }
28
+ /**
29
+ * Fetches a YouTube thumbnail and returns it as a data URI, or null on failure.
30
+ * Inlining as a data URI avoids cross-origin canvas tainting in html2canvas.
31
+ * Tries maxres first, falls back to hq (maxres 404s on some videos).
32
+ */
33
+ async function fetchYouTubeThumbnail(id) {
34
+ for (const quality of ['maxresdefault', 'hqdefault']) {
35
+ try {
36
+ const response = await fetch(`https://img.youtube.com/vi/${id}/${quality}.jpg`);
37
+ if (!response.ok)
38
+ continue;
39
+ const blob = await response.blob();
40
+ // YouTube returns a tiny placeholder for missing qualities; skip those.
41
+ if (blob.size < 2000)
42
+ continue;
43
+ return await new Promise((resolve) => {
44
+ const reader = new FileReader();
45
+ reader.onload = () => resolve(reader.result);
46
+ reader.readAsDataURL(blob);
47
+ });
48
+ }
49
+ catch {
50
+ /* try next quality */
51
+ }
52
+ }
53
+ return null;
54
+ }
4
55
  /**
5
56
  * Dynamically loads html2canvas from CDN if not already loaded
6
57
  */
@@ -47,44 +98,205 @@ export function useSnapshot() {
47
98
  if (typeof window.html2canvas === 'undefined') {
48
99
  throw new Error('html2canvas failed to load');
49
100
  }
50
- // Pre-fetch blob URLs from shape <object> elements so they can be
51
- // rendered as <img> in the html2canvas clone (html2canvas cannot
52
- // traverse the isolated DOM inside <object> elements).
53
- const shapeDataUris = new Map();
54
- const shapeObjects = element.querySelectorAll('.c-shape-object object');
55
- for (const obj of Array.from(shapeObjects)) {
56
- const dataUrl = obj.getAttribute('data');
57
- if (dataUrl && dataUrl.startsWith('blob:')) {
101
+ // Wait for fonts and in-flight images to finish before capturing —
102
+ // otherwise html2canvas snapshots a half-rendered page (missing images,
103
+ // fallback fonts) and the measured content height is wrong.
104
+ await document.fonts?.ready;
105
+ await Promise.all(Array.from(element.querySelectorAll('img'))
106
+ .filter((img) => !img.complete)
107
+ .map((img) => new Promise((resolve) => {
108
+ img.onload = img.onerror = () => resolve();
109
+ })));
110
+ // The maker renders the full website inside `.c-preview-container`
111
+ // (position: fixed + transform: scale). The page background — solid
112
+ // colour, gradient, or image — lives on `.brand-page-root-background`,
113
+ // which sits OUTSIDE the scrolling content. Capturing the content alone
114
+ // drops that background, so transparent sections fall back to white and
115
+ // their (often light) text becomes invisible. Capture the whole preview
116
+ // (`[data-test-tag="editor-preview"]`) so the background layer is
117
+ // included, and size the canvas to the real page length, which only the
118
+ // inner scroll container `.brand-page__inner` reports via scrollHeight.
119
+ const target = element.querySelector('[data-test-tag="editor-preview"]') ?? element;
120
+ const scrollEl = element.querySelector('.brand-page__inner') ?? target;
121
+ // Keep the render light on mobile (tighter CPU/memory) by truncating
122
+ // taller there than on desktop.
123
+ const isMobileViewport = typeof window !== 'undefined' &&
124
+ window.matchMedia?.('(max-width: 768px)').matches;
125
+ const maxHeight = isMobileViewport
126
+ ? MAX_SNAPSHOT_HEIGHT_MOBILE
127
+ : MAX_SNAPSHOT_HEIGHT_DESKTOP;
128
+ const captureHeight = Math.min(Math.max(scrollEl.scrollHeight, MIN_SNAPSHOT_HEIGHT), maxHeight);
129
+ // Render at OpenAI's short-side target width (never upscaling). Token cost
130
+ // is set by height/aspect ratio, not by scaling down the width, so we keep
131
+ // the width crisp and let MAX_SNAPSHOT_HEIGHT bound the cost.
132
+ const captureWidth = target.clientWidth || target.offsetWidth || 1;
133
+ const scale = Math.min(1, SNAPSHOT_TARGET_WIDTH / captureWidth);
134
+ // Pre-fetch blob-backed SVGs from content shape <object> elements so they
135
+ // can be rendered as <img> in the clone (html2canvas can't reach into the
136
+ // isolated DOM inside <object>). Background-shape blob <img>s are NOT
137
+ // converted: html2canvas renders them no differently with or without the
138
+ // conversion, so it'd just be wasted fetches.
139
+ const blobSvgUris = new Map();
140
+ const blobSources = Array.from(element.querySelectorAll('.c-shape-object object')).map((o) => o.getAttribute('data'));
141
+ for (const src of blobSources) {
142
+ if (src && src.startsWith('blob:') && !blobSvgUris.has(src)) {
58
143
  try {
59
- const response = await fetch(dataUrl);
60
- const svgText = await response.text();
61
- shapeDataUris.set(dataUrl, `data:image/svg+xml,${encodeURIComponent(svgText)}`);
144
+ const svgText = await (await fetch(src)).text();
145
+ blobSvgUris.set(src, `data:image/svg+xml,${encodeURIComponent(svgText)}`);
62
146
  }
63
147
  catch {
64
148
  /* skip – shape will render as blank, same as before */
65
149
  }
66
150
  }
67
151
  }
68
- const canvas = await window.html2canvas(element, {
69
- allowTaint: true,
152
+ const objectFitImages = new Map();
153
+ for (const img of Array.from(element.querySelectorAll('img'))) {
154
+ const { objectFit, objectPosition } = getComputedStyle(img);
155
+ if (objectFit !== 'cover' && objectFit !== 'contain')
156
+ continue;
157
+ const rect = img.getBoundingClientRect();
158
+ const src = img.currentSrc || img.src;
159
+ if (src && rect.width > 2 && rect.height > 2) {
160
+ objectFitImages.set(src, {
161
+ fit: objectFit,
162
+ position: objectPosition || 'center',
163
+ width: Math.round(rect.width),
164
+ height: Math.round(rect.height),
165
+ });
166
+ }
167
+ }
168
+ const videoThumbs = new Map();
169
+ for (const iframe of Array.from(element.querySelectorAll('iframe'))) {
170
+ const id = getYouTubeId(iframe.src);
171
+ if (!id || videoThumbs.has(id))
172
+ continue;
173
+ const dataUri = await fetchYouTubeThumbnail(id);
174
+ if (!dataUri)
175
+ continue;
176
+ const rect = iframe.getBoundingClientRect();
177
+ videoThumbs.set(id, {
178
+ dataUri,
179
+ width: Math.round(rect.width) || iframe.clientWidth,
180
+ height: Math.round(rect.height) || iframe.clientHeight,
181
+ });
182
+ }
183
+ const canvas = await window.html2canvas(target, {
184
+ // Must stay false: with allowTaint, html2canvas draws cross-origin
185
+ // media that lacks CORS (e.g. a Getty background <video>), which taints
186
+ // the canvas and makes toDataURL throw — failing the whole snapshot.
187
+ // With it off, such content is skipped (rendered blank) instead, so the
188
+ // snapshot always exports. CORS-enabled images (Unsplash, user uploads)
189
+ // still render via useCORS.
190
+ allowTaint: false,
70
191
  useCORS: true,
71
- scale: 0.8, // Reduce quality to decrease file size
192
+ scale, // fit the full page into the vision model's image budget
72
193
  logging: false,
73
194
  backgroundColor: '#ffffff',
74
- onclone: (_doc, clonedElement) => {
75
- const clonedObjects = clonedElement.querySelectorAll('.c-shape-object object');
76
- clonedObjects.forEach((obj) => {
195
+ // Capture the full page, not just the scroll viewport.
196
+ height: captureHeight,
197
+ windowHeight: captureHeight,
198
+ scrollX: 0,
199
+ scrollY: 0,
200
+ x: 0,
201
+ y: 0,
202
+ onclone: (_doc) => {
203
+ const setStyle = (selector, styles) => {
204
+ const el = _doc.querySelector(selector);
205
+ if (el)
206
+ Object.assign(el.style, styles);
207
+ };
208
+ // Neutralise the fixed-position + scale wrapper and the scroll clip so
209
+ // the clone lays out at full content height; otherwise html2canvas
210
+ // honours the viewport-height box and the page is cut off.
211
+ setStyle('.c-preview-container', {
212
+ position: 'static',
213
+ transform: 'none',
214
+ height: 'auto',
215
+ overflow: 'visible',
216
+ });
217
+ setStyle('[data-test-tag="editor-preview"]', {
218
+ position: 'relative',
219
+ height: `${captureHeight}px`,
220
+ overflow: 'visible',
221
+ });
222
+ // Stretch the (normally fixed, viewport-height) background and shape
223
+ // layers to cover the full page so transparent sections render their
224
+ // intended background instead of the white fallback.
225
+ setStyle('.brand-page-root-background', {
226
+ position: 'absolute',
227
+ top: '0',
228
+ left: '0',
229
+ width: '100%',
230
+ height: '100%',
231
+ });
232
+ setStyle('.brand-page__background-shapes-layer', {
233
+ position: 'absolute',
234
+ top: '0',
235
+ left: '0',
236
+ width: '100%',
237
+ height: '100%',
238
+ });
239
+ setStyle('.brand-page-root', {
240
+ position: 'relative',
241
+ height: 'auto',
242
+ });
243
+ setStyle('.brand-page', { height: 'auto' });
244
+ setStyle('.brand-page__inner', {
245
+ height: 'auto',
246
+ overflow: 'visible',
247
+ });
248
+ // Swap content shape <object>s for <img> using the pre-fetched data
249
+ // URIs (html2canvas can't render <object> SVGs directly).
250
+ _doc
251
+ .querySelectorAll('.c-shape-object object')
252
+ .forEach((obj) => {
77
253
  const dataUrl = obj.getAttribute('data');
78
- if (dataUrl && shapeDataUris.has(dataUrl)) {
254
+ if (dataUrl && blobSvgUris.has(dataUrl)) {
79
255
  const img = _doc.createElement('img');
80
- img.src = shapeDataUris.get(dataUrl);
256
+ img.src = blobSvgUris.get(dataUrl);
81
257
  const style = obj.getAttribute('style');
82
- if (style) {
258
+ if (style)
83
259
  img.setAttribute('style', style);
84
- }
85
260
  obj.parentNode?.replaceChild(img, obj);
86
261
  }
87
262
  });
263
+ // Replace object-fit images with a background-size div so they
264
+ // render cropped/contained instead of stretched.
265
+ _doc.querySelectorAll('img').forEach((img) => {
266
+ const src = img.currentSrc || img.src;
267
+ const fitInfo = objectFitImages.get(src);
268
+ if (fitInfo) {
269
+ const div = _doc.createElement('div');
270
+ div.className = img.className;
271
+ div.style.cssText = img.getAttribute('style') || '';
272
+ div.style.backgroundImage = `url("${img.src}")`;
273
+ div.style.backgroundSize = fitInfo.fit;
274
+ div.style.backgroundPosition = fitInfo.position;
275
+ div.style.backgroundRepeat = 'no-repeat';
276
+ div.style.width = `${fitInfo.width}px`;
277
+ div.style.height = `${fitInfo.height}px`;
278
+ img.parentNode?.replaceChild(div, img);
279
+ }
280
+ });
281
+ // Replace video iframes (blank in html2canvas) with their poster
282
+ // thumbnail rendered as a background-size div.
283
+ _doc
284
+ .querySelectorAll('iframe')
285
+ .forEach((iframe) => {
286
+ const id = getYouTubeId(iframe.src);
287
+ const thumb = id ? videoThumbs.get(id) : undefined;
288
+ if (!thumb)
289
+ return;
290
+ const div = _doc.createElement('div');
291
+ div.style.cssText = iframe.getAttribute('style') || '';
292
+ div.style.width = `${thumb.width}px`;
293
+ div.style.height = `${thumb.height}px`;
294
+ div.style.backgroundImage = `url("${thumb.dataUri}")`;
295
+ div.style.backgroundSize = 'cover';
296
+ div.style.backgroundPosition = 'center';
297
+ div.style.backgroundColor = '#000000';
298
+ iframe.parentNode?.replaceChild(div, iframe);
299
+ });
88
300
  },
89
301
  });
90
302
  const base64Image = canvas.toDataURL('image/jpeg', 0.8); // Use JPEG with 80% quality for smaller size