@designcrowd/library.brand-page 5.18.6 → 5.18.7-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 (218) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DlYq1LAT.js → CreatedOnBrandCrowd-de-de-Cn8zNuGM.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BssRIQqn.js → CreatedOnBrandCrowd-en-us-CP8xAFc-.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-BGF9pptH.js → CreatedOnBrandCrowd-es-es-Ds8NijwP.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Caupqu2Z.js → CreatedOnBrandCrowd-fr-ca-BiVV_7lj.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Caupqu2Z.js → CreatedOnBrandCrowd-fr-fr-BiVV_7lj.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-s9q4uJ4u.js → CreatedOnBrandCrowd-pt-br-CjjrYojo.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-s9q4uJ4u.js → CreatedOnBrandCrowd-pt-pt-CjjrYojo.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-UnYnZbA7.js → CreatedOnDcom-de-de-ChR5jvlP.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-rDv6H12L.js → CreatedOnDcom-en-us-C4rO1BMA.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DKPLaT6o.js → CreatedOnDcom-es-es-DxGMS6JK.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DAv34bG7.js → CreatedOnDcom-fr-ca-n31u4t7u.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DsOHnS8q.js → CreatedOnDcom-fr-fr-gwdsX7YV.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DpjDBCLj.js → CreatedOnDcom-pt-br-iZjFp--s.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-rFG0unf1.js → CreatedOnDcom-pt-pt-B3Z3-RLC.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/mailing-list/MailingList.mixin.d.ts +4 -0
  20. package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +4 -1
  21. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.d.ts +4 -0
  22. package/lib/src/brand-page/components/payment/request-payment/RequestPayment.mixin.js +4 -0
  23. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.d.ts +4 -0
  24. package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.js +4 -1
  25. package/lib/src/brand-page/components/video/Video.mixin.d.ts +4 -0
  26. package/lib/src/brand-page/components/video/Video.mixin.js +4 -0
  27. package/lib/src/brand-page/constants/misc.constants.d.ts +26 -0
  28. package/lib/src/brand-page/constants/misc.constants.js +42 -0
  29. package/lib/src/brand-page/models/content.model.d.ts +2 -0
  30. package/lib/src/brand-page/utils/content-padding.util.d.ts +13 -0
  31. package/lib/src/brand-page/utils/content-padding.util.js +10 -0
  32. package/lib/src/brand-page/utils/index.d.ts +1 -0
  33. package/lib/src/brand-page/utils/index.js +1 -0
  34. package/lib/src/brand-page/utils/migration/migration-17.d.ts +2 -0
  35. package/lib/src/brand-page/utils/migration/migration-17.js +48 -0
  36. package/lib/src/brand-page/utils/migration.util.js +10 -0
  37. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +7 -2
  38. package/lib/src/brand-page-maker/components/admin-logo-settings/AdminLogoSettings.mixin.d.ts +2 -0
  39. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +19 -2
  40. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +8 -0
  41. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +7 -2
  42. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -0
  43. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +104 -2
  44. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +5 -0
  45. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +7 -2
  46. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +3 -0
  47. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.js +11 -0
  48. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +7 -2
  49. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +7 -2
  50. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +7 -2
  51. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +7 -2
  52. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +9 -0
  53. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +21 -1
  54. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +20 -0
  55. package/lib/src/brand-page-maker/composables/useEventTracker.js +17 -0
  56. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +17 -0
  57. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +25 -0
  58. package/lib/src/brand-page-maker/constants/config-settings-constant.js +38 -0
  59. package/lib/src/brand-page-maker/convertors/about-display-style.convertor.js +9 -1
  60. package/lib/src/brand-page-maker/convertors/banner-align-display-style.convertor.js +7 -1
  61. package/lib/src/brand-page-maker/convertors/contact-layout-style.convertor.js +6 -1
  62. package/lib/src/brand-page-maker/models/content-settings.js +36 -2
  63. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +12 -0
  64. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +41 -2
  65. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +307 -5
  66. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +86 -0
  67. package/lib/src/brand-page-maker/stores/history/history-store.js +501 -0
  68. package/lib/src/brand-page-maker/stores/logo-store/logo-store.d.ts +1 -0
  69. package/lib/src/brand-page-maker/stores/user/user-store.js +1 -0
  70. package/lib/src/bundled-translations-BUyt4scr.js +175 -0
  71. package/lib/src/bundled-translations.de-DE-DEw_OVai.js +175 -0
  72. package/lib/src/bundled-translations.es-ES-CFKWmkgY.js +175 -0
  73. package/lib/src/bundled-translations.fr-CA-DCl5LJS4.js +175 -0
  74. package/lib/src/bundled-translations.fr-FR-B5BjgllA.js +175 -0
  75. package/lib/src/bundled-translations.pt-BR-5PWpMPuM.js +175 -0
  76. package/lib/src/bundled-translations.pt-PT-B9_wN0HR.js +175 -0
  77. package/lib/src/bundles/bundled-translations.de-DE.json +5 -1
  78. package/lib/src/bundles/bundled-translations.es-ES.json +5 -1
  79. package/lib/src/bundles/bundled-translations.fr-CA.json +5 -1
  80. package/lib/src/bundles/bundled-translations.fr-FR.json +5 -1
  81. package/lib/src/bundles/bundled-translations.json +5 -1
  82. package/lib/src/bundles/bundled-translations.pt-BR.json +5 -1
  83. package/lib/src/bundles/bundled-translations.pt-PT.json +5 -1
  84. package/lib/src/constants/ui.d.ts +1 -0
  85. package/lib/src/css/library.brand-page-brandCrowd.min.css +128 -131
  86. package/lib/src/css/library.brand-page-brandPage.min.css +128 -132
  87. package/lib/src/css/library.brand-page-designCom.min.css +128 -131
  88. package/lib/src/css/library.brand-page-preview.min.css +128 -131
  89. package/lib/src/index.mjs +1 -1
  90. package/lib/src/{lib-ngdDRkSL.js → lib-CIj_C38s.js} +36378 -35591
  91. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +104 -2
  92. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +5 -0
  93. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.stories.js +1 -0
  94. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +37 -2
  95. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +28 -0
  96. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +19 -2
  97. package/lib/src/shared/content-settings/components/draggable-item-list/DraggableItemList.mixin.d.ts +3 -0
  98. package/lib/src/shared/content-settings/components/draggable-item-list/DraggableItemList.mixin.js +19 -2
  99. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +12 -0
  100. package/lib/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.js +18 -8
  101. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +67 -0
  102. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +6 -0
  103. package/lib/src/shared/content-settings/components/slider-field/SliderField.mixin.js +9 -0
  104. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +12 -0
  105. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +19 -0
  106. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.js +11 -0
  107. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +10 -2
  108. package/lib/src/shared/logo-settings/store/editable-logo-store.d.ts +5 -1
  109. package/lib/src/shared/logo-settings/store/editable-logo-store.js +31 -0
  110. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +40 -2
  111. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +24 -2
  112. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +23 -2
  113. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +16 -0
  114. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +23 -2
  115. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +16 -0
  116. package/lib/src/utils/minify-brand-page-metadata.js +11 -1
  117. package/package.json +1 -1
  118. package/src/brand-page/components/about-section/AboutSection.vue +10 -4
  119. package/src/brand-page/components/banner-section/BannerSection.vue +11 -4
  120. package/src/brand-page/components/booking-section/BookingSection.vue +11 -3
  121. package/src/brand-page/components/contact-form/ContactForm.mixin.ts +12 -6
  122. package/src/brand-page/components/contact-form/ContactForm.vue +105 -104
  123. package/src/brand-page/components/image-gallery/ImageGallery.mixin.ts +5 -0
  124. package/src/brand-page/components/image-gallery/ImageGallery.vue +1 -1
  125. package/src/brand-page/components/mailing-list/MailingList.mixin.ts +8 -1
  126. package/src/brand-page/components/mailing-list/MailingList.vue +1 -1
  127. package/src/brand-page/components/map/Map.vue +5 -1
  128. package/src/brand-page/components/payment/request-payment/RequestPayment.mixin.ts +5 -0
  129. package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
  130. package/src/brand-page/components/payment/support-me/SupportMe.mixin.ts +8 -1
  131. package/src/brand-page/components/payment/support-me/SupportMe.vue +1 -0
  132. package/src/brand-page/components/shop/Shop.vue +7 -2
  133. package/src/brand-page/components/social-links/SocialLinks.vue +7 -3
  134. package/src/brand-page/components/text/Text.vue +12 -5
  135. package/src/brand-page/components/text-media-section/TextMediaSection.vue +11 -3
  136. package/src/brand-page/components/video/Video.mixin.ts +5 -0
  137. package/src/brand-page/components/video/Video.vue +1 -1
  138. package/src/brand-page/constants/misc.constants.ts +48 -0
  139. package/src/brand-page/models/content.model.ts +2 -0
  140. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +9 -3
  141. package/src/brand-page/utils/content-padding.util.test.ts +29 -0
  142. package/src/brand-page/utils/content-padding.util.ts +13 -0
  143. package/src/brand-page/utils/index.ts +1 -0
  144. package/src/brand-page/utils/migration/migration-17.ts +65 -0
  145. package/src/brand-page/utils/migration.util.test.ts +195 -0
  146. package/src/brand-page/utils/migration.util.ts +11 -0
  147. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +9 -0
  148. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +2 -3
  149. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -0
  150. package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +5 -0
  151. package/src/brand-page-maker/components/cookie-collection/CookieSettings.vue +18 -17
  152. package/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.ts +13 -0
  153. package/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.vue +26 -3
  154. package/src/brand-page-maker/components/quick-link-bar/RedoButton.vue +66 -0
  155. package/src/brand-page-maker/components/quick-link-bar/UndoButton.vue +66 -0
  156. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.de-DE.json +6 -4
  157. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.es-ES.json +6 -4
  158. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.fr-CA.json +6 -4
  159. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.fr-FR.json +6 -4
  160. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.json +3 -1
  161. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.pt-BR.json +6 -4
  162. package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.pt-PT.json +6 -4
  163. package/src/brand-page-maker/components/quick-link-bar/undo-redo-buttons.test.ts +116 -0
  164. package/src/brand-page-maker/components/save-contact/SaveContact.vue +12 -19
  165. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +10 -0
  166. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +28 -1
  167. package/src/brand-page-maker/composables/useEventTracker.ts +29 -0
  168. package/src/brand-page-maker/composables/useUndoRedoFlag.ts +30 -0
  169. package/src/brand-page-maker/constants/config-settings-constant.ts +44 -0
  170. package/src/brand-page-maker/convertors/about-display-style.convertor.ts +10 -0
  171. package/src/brand-page-maker/convertors/banner-align-display-style.convertor.ts +8 -0
  172. package/src/brand-page-maker/convertors/contact-layout-style.convertor.test.ts +50 -0
  173. package/src/brand-page-maker/convertors/contact-layout-style.convertor.ts +8 -0
  174. package/src/brand-page-maker/models/content-settings.ts +38 -2
  175. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +334 -0
  176. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +371 -4
  177. package/src/brand-page-maker/stores/history/history-store.test.ts +1120 -0
  178. package/src/brand-page-maker/stores/history/history-store.ts +666 -0
  179. package/src/brand-page-maker/stores/logo-store/logo-store.ts +5 -0
  180. package/src/brand-page-maker/stores/user/user-store.ts +1 -0
  181. package/src/bundles/bundled-translations.de-DE.json +5 -1
  182. package/src/bundles/bundled-translations.es-ES.json +5 -1
  183. package/src/bundles/bundled-translations.fr-CA.json +5 -1
  184. package/src/bundles/bundled-translations.fr-FR.json +5 -1
  185. package/src/bundles/bundled-translations.json +5 -1
  186. package/src/bundles/bundled-translations.pt-BR.json +5 -1
  187. package/src/bundles/bundled-translations.pt-PT.json +5 -1
  188. package/src/constants/ui.ts +1 -0
  189. package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +5 -0
  190. package/src/shared/content-settings/components/content-settings/ContentSettings.stories.ts +1 -0
  191. package/src/shared/content-settings/components/content-settings/i18n/content-settings.de-DE.json +2 -0
  192. package/src/shared/content-settings/components/content-settings/i18n/content-settings.es-ES.json +2 -0
  193. package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-CA.json +2 -0
  194. package/src/shared/content-settings/components/content-settings/i18n/content-settings.fr-FR.json +2 -0
  195. package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +2 -0
  196. package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-BR.json +2 -0
  197. package/src/shared/content-settings/components/content-settings/i18n/content-settings.pt-PT.json +2 -0
  198. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.vue +1 -1
  199. package/src/shared/content-settings/components/draggable-item-list/DraggableItemList.mixin.ts +19 -2
  200. package/src/shared/content-settings/components/draggable-item-list/DraggableItemList.vue +1 -1
  201. package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.vue +2 -2
  202. package/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.ts +26 -10
  203. package/src/shared/content-settings/components/logo-selection-field/LogoSelection.vue +63 -48
  204. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +4 -0
  205. package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +7 -0
  206. package/src/shared/content-settings/components/slider-field/SliderField.mixin.ts +9 -0
  207. package/src/shared/content-settings/components/text-field/TextField.vue +1 -0
  208. package/src/shared/content-settings/components/text-image-field/TextImageField.vue +33 -4
  209. package/src/shared/logo-settings/components/EditLogoSettings.mixin.ts +11 -0
  210. package/src/shared/logo-settings/store/editable-logo-store.ts +42 -1
  211. package/src/utils/minify-brand-page-metadata.ts +11 -1
  212. package/lib/src/bundled-translations-B5Fkt3qd.js +0 -175
  213. package/lib/src/bundled-translations.de-DE-DpdVePM9.js +0 -175
  214. package/lib/src/bundled-translations.es-ES-BsIcrzZF.js +0 -175
  215. package/lib/src/bundled-translations.fr-CA-G1XCp2Qh.js +0 -175
  216. package/lib/src/bundled-translations.fr-FR-BdwLXOnB.js +0 -175
  217. package/lib/src/bundled-translations.pt-BR-Bvn5ePe7.js +0 -175
  218. package/lib/src/bundled-translations.pt-PT-CDtkb2k1.js +0 -175
@@ -0,0 +1,666 @@
1
+ import { defineStore } from 'pinia';
2
+ import { computed, ref, shallowRef, toRaw, watch } from 'vue';
3
+ import type { BrandPageModel } from '@brand-page';
4
+ import { useBrandPageMakerStore } from '../brandpage-maker/brandpage-maker-store';
5
+ import { useBrandPageContainerStore } from '../brandpage-container';
6
+ import { useAiWebsiteBuilderThreadStore } from '../ai-thread/ai-website-builder-thread-store';
7
+ import { useEventTracker } from '../../composables/useEventTracker';
8
+ import { usePageNavigationStore } from '@shared/page-navigation/store/page-navigation-store';
9
+ import { useUiEditableLogoStore } from '@shared/logo-settings/store/editable-logo-store';
10
+
11
+ /**
12
+ * A single history entry. `snapshot` is JSON-stringified up-front so Pinia
13
+ * treats it as opaque (no per-field reactivity) and `sizeBytes` is exact.
14
+ * Sidecar state not in `brandPageData` rides along on this entry —
15
+ * `activePageId` for page-tab restore, `logoEdits` for pre-purchase logo
16
+ * form state.
17
+ */
18
+ export interface HistoryEntry {
19
+ snapshot: string;
20
+ timestamp: string;
21
+ source: 'manual_edit' | 'template_switch' | 'ai_changeset' | 'rich_text';
22
+ actor: 'user' | 'ai_builder';
23
+ label?: string;
24
+ sizeBytes: number;
25
+ /** Active page id at capture time. Empty before first router push. */
26
+ activePageId: string;
27
+ /** JSON-stringified `editableLogoStore.logoEdits`. Undefined if logo tab never opened. */
28
+ logoEdits?: string;
29
+ }
30
+
31
+ // Monotonic burst — first mutation starts the timer, subsequent mutations
32
+ // don't reset it; max-wait ceiling forces a commit during continuous typing.
33
+ const CAPTURE_DEBOUNCE_MS = 1700;
34
+ const CAPTURE_MAX_WAIT_MS = 10000;
35
+
36
+ // Merge bursts flush quickly — sync hydration writes land in <50ms,
37
+ // user typing takes >150ms, so a short debounce keeps user input out
38
+ // of the merge.
39
+ const MERGE_DEBOUNCE_MS = 50;
40
+
41
+ // FR-12 / FR-13 / FR-13c. 25 MB per-entry accommodates a high-res BYO logo
42
+ // rendered into 4 viewport sections + per-page logo blocks. Oversize
43
+ // entries are skipped with telemetry; 100 MB total evicts oldest
44
+ // non-baseline entries.
45
+ const MAX_ENTRIES = 40;
46
+ const MAX_MEMORY_BYTES = 100 * 1024 * 1024;
47
+ const MAX_ENTRY_BYTES = 25 * 1024 * 1024;
48
+
49
+ // Window after baseline during which captured mutations REPLACE the
50
+ // baseline instead of pushing — covers async post-init cascades
51
+ // (template load, router afterEach, layout settle).
52
+ const BASELINE_SETTLE_MS = 2000;
53
+
54
+ export const useHistoryStore = defineStore('history', () => {
55
+ const eventTracker = useEventTracker();
56
+
57
+ // --- State ---
58
+
59
+ // shallowRef keeps the array reactive but prevents Pinia from proxying
60
+ // every entry — combined with stringified snapshots, entries are
61
+ // fully opaque to reactivity traversal.
62
+ const entries = shallowRef<HistoryEntry[]>([]);
63
+ const currentIndex = ref(-1); // -1 means "no baseline yet"
64
+ const isApplyingSnapshot = ref(false);
65
+ // Bumped once per undo/redo apply so editors can detect an external re-seed
66
+ // without depending on the synchronous isApplyingSnapshot window (already
67
+ // closed by the time their async model.value watcher runs).
68
+ const snapshotAppliedToken = ref(0);
69
+ const baselineCaptured = ref(false);
70
+ const watcherInstalled = ref(false);
71
+ let baselineCapturedAt = 0;
72
+
73
+ // --- Getters ---
74
+
75
+ const canUndo = computed(() => currentIndex.value > 0);
76
+ const canRedo = computed(
77
+ () =>
78
+ currentIndex.value >= 0 && currentIndex.value < entries.value.length - 1,
79
+ );
80
+
81
+ // --- Internals ---
82
+
83
+ // Pre-purchase only (matches the save-path strip in
84
+ // `brand-page-container-store.ts:805`). Each rendered base64 is ~2-4 MB
85
+ // across 6-8 placements, so unstripped snapshots bump the 25 MB cap.
86
+ // `_syncLogoSettingsFromSnapshot` repopulates on undo.
87
+ function stripLogoBase64IfPrePurchase(data: {
88
+ pages?: { contents?: { logoImageBase64Url?: string }[] }[];
89
+ pageHeaderDesktop?: { logoImageBase64Url?: string };
90
+ pageHeaderMobile?: { logoImageBase64Url?: string };
91
+ pageFooterDesktop?: { logoImageBase64Url?: string };
92
+ pageFooterMobile?: { logoImageBase64Url?: string };
93
+ }): void {
94
+ try {
95
+ if (!useBrandPageContainerStore().isPrePurchase) return;
96
+ } catch {
97
+ return;
98
+ }
99
+
100
+ if (data.pageHeaderDesktop) {
101
+ data.pageHeaderDesktop.logoImageBase64Url = undefined;
102
+ }
103
+
104
+ if (data.pageHeaderMobile) {
105
+ data.pageHeaderMobile.logoImageBase64Url = undefined;
106
+ }
107
+
108
+ if (data.pageFooterDesktop) {
109
+ data.pageFooterDesktop.logoImageBase64Url = undefined;
110
+ }
111
+
112
+ if (data.pageFooterMobile) {
113
+ data.pageFooterMobile.logoImageBase64Url = undefined;
114
+ }
115
+
116
+ if (Array.isArray(data.pages)) {
117
+ for (const page of data.pages) {
118
+ if (!Array.isArray(page?.contents)) continue;
119
+
120
+ for (const content of page.contents) {
121
+ if (
122
+ content &&
123
+ typeof content === 'object' &&
124
+ 'logoImageBase64Url' in content
125
+ ) {
126
+ content.logoImageBase64Url = undefined;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ function serializeCurrentSnapshot(): string | null {
134
+ try {
135
+ const cloned = JSON.parse(
136
+ JSON.stringify(toRaw(useBrandPageMakerStore().brandPageData)),
137
+ );
138
+
139
+ stripLogoBase64IfPrePurchase(cloned);
140
+
141
+ return JSON.stringify(cloned);
142
+ } catch (error) {
143
+ console.error('history-store: failed to serialize snapshot', error);
144
+
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function getCurrentActivePageId(): string {
150
+ return usePageNavigationStore().activePageId ?? '';
151
+ }
152
+
153
+ function serializeCurrentLogoEdits(): string | undefined {
154
+ try {
155
+ const editableLogoStore = useUiEditableLogoStore();
156
+
157
+ return JSON.stringify(toRaw(editableLogoStore.logoEdits));
158
+ } catch (error) {
159
+ console.error('history-store: failed to serialize logoEdits', error);
160
+
161
+ return undefined;
162
+ }
163
+ }
164
+
165
+ // Two-phase merge flag. Hydration write sites call `mergeNextMutation()`
166
+ // immediately before their imperative write, which sets `mergeArmed`.
167
+ // When the watcher's `scheduleAutoCapture` arms a new idle timer, it
168
+ // transfers `mergeArmed` into `pendingTimerIsMerge`. The flush then
169
+ // consumes `pendingTimerIsMerge` and merges into the current head
170
+ // instead of pushing.
171
+ //
172
+ // The microtask cleanup on `mergeArmed` prevents stale flags surviving
173
+ // when the user navigates away (e.g. sidebar tab switch) before the
174
+ // flagged write actually happens — a previously-set arm is dropped
175
+ // synchronously after the current tick if no watcher fired.
176
+ const mergeArmed = ref(false);
177
+ let pendingTimerIsMerge = false;
178
+
179
+ // Returns 'noop' when no-change vs current head, 'oversized' when over
180
+ // MAX_ENTRY_BYTES, else 'committed'. Truncates the redo tail, caps at
181
+ // MAX_ENTRIES, evicts oldest non-baseline to stay under MAX_MEMORY_BYTES.
182
+ // Baseline (index 0) is never evicted.
183
+ function commitEntry(args: {
184
+ snapshot: string;
185
+ source: HistoryEntry['source'];
186
+ actor: HistoryEntry['actor'];
187
+ label?: string;
188
+ activePageId: string;
189
+ logoEdits?: string;
190
+ }): 'committed' | 'oversized' | 'noop' {
191
+ const current = entries.value[currentIndex.value];
192
+
193
+ // Compare both snapshot AND logoEdits sidecar — pre-purchase logo
194
+ // edits don't change brandPageData (base64 is stripped) so the
195
+ // sidecar is the only signal that distinguishes them.
196
+ if (
197
+ current &&
198
+ current.snapshot === args.snapshot &&
199
+ current.logoEdits === args.logoEdits
200
+ ) {
201
+ return 'noop';
202
+ }
203
+
204
+ if (args.snapshot.length > MAX_ENTRY_BYTES) {
205
+ eventTracker.sendHistoryOversizedEvent({
206
+ attempted_bytes: args.snapshot.length,
207
+ source: args.source,
208
+ });
209
+
210
+ return 'oversized';
211
+ }
212
+
213
+ const entry: HistoryEntry = {
214
+ snapshot: args.snapshot,
215
+ timestamp: new Date().toISOString(),
216
+ source: args.source,
217
+ actor: args.actor,
218
+ label: args.label,
219
+ sizeBytes: args.snapshot.length,
220
+ activePageId: args.activePageId,
221
+ logoEdits: args.logoEdits,
222
+ };
223
+
224
+ // FR-11: truncate redo tail, then append.
225
+ const next = entries.value.slice(0, currentIndex.value + 1);
226
+
227
+ next.push(entry);
228
+
229
+ // FR-12: 40-entry cap. Drop oldest non-baseline (index 1) if we'd exceed.
230
+ let didEvict = false;
231
+
232
+ while (next.length > MAX_ENTRIES && next.length > 1) {
233
+ next.splice(1, 1);
234
+ didEvict = true;
235
+ }
236
+
237
+ // FR-13: keep total serialized size under 100 MB. Drop oldest
238
+ // non-baseline repeatedly until under budget. Baseline alone can exceed
239
+ // the budget on a very large page — in that pathological case we stop
240
+ // evicting at length 2 (baseline + newest) rather than dropping the
241
+ // baseline.
242
+ let totalBytes = next.reduce((sum, e) => sum + e.sizeBytes, 0);
243
+
244
+ while (totalBytes > MAX_MEMORY_BYTES && next.length > 2) {
245
+ totalBytes -= next[1].sizeBytes;
246
+ next.splice(1, 1);
247
+ didEvict = true;
248
+ }
249
+
250
+ entries.value = next;
251
+ currentIndex.value = next.length - 1;
252
+
253
+ if (didEvict) {
254
+ eventTracker.sendHistoryCappedEvent({
255
+ entries_count: next.length,
256
+ size_bytes: totalBytes,
257
+ });
258
+ }
259
+
260
+ return 'committed';
261
+ }
262
+
263
+ // Hoisted so force-commit paths (template switch, AI apply) can cancel
264
+ // any pending auto-capture that would otherwise duplicate their entry.
265
+ let idleTimerId: ReturnType<typeof setTimeout> | null = null;
266
+ let maxWaitTimerId: ReturnType<typeof setTimeout> | null = null;
267
+
268
+ function cancelPendingAutoCapture(): void {
269
+ if (idleTimerId !== null) {
270
+ clearTimeout(idleTimerId);
271
+ idleTimerId = null;
272
+ }
273
+
274
+ if (maxWaitTimerId !== null) {
275
+ clearTimeout(maxWaitTimerId);
276
+ maxWaitTimerId = null;
277
+ }
278
+ }
279
+
280
+ function installWatcher(): void {
281
+ if (watcherInstalled.value) return;
282
+
283
+ const flush = () => {
284
+ cancelPendingAutoCapture();
285
+
286
+ if (isApplyingSnapshot.value) return;
287
+
288
+ if (!baselineCaptured.value) return;
289
+
290
+ const snapshot = serializeCurrentSnapshot();
291
+
292
+ if (!snapshot) return;
293
+
294
+ const activePageId = getCurrentActivePageId();
295
+ const logoEdits = serializeCurrentLogoEdits();
296
+
297
+ // Settle window: fold post-init reactive cascades back into the
298
+ // baseline so undo doesn't light up before the user has done
299
+ // anything.
300
+ const inSettleWindow =
301
+ currentIndex.value === 0 &&
302
+ Date.now() - baselineCapturedAt < BASELINE_SETTLE_MS;
303
+
304
+ // Merge: the timer that fired this flush was armed during a
305
+ // hydration burst — replace the current head in place instead of
306
+ // pushing. Consumed once.
307
+ const shouldMerge = pendingTimerIsMerge;
308
+
309
+ pendingTimerIsMerge = false;
310
+
311
+ if (inSettleWindow) {
312
+ entries.value = [
313
+ {
314
+ ...entries.value[0],
315
+ snapshot,
316
+ timestamp: new Date().toISOString(),
317
+ sizeBytes: snapshot.length,
318
+ activePageId,
319
+ logoEdits,
320
+ },
321
+ ];
322
+
323
+ return;
324
+ }
325
+
326
+ if (shouldMerge && entries.value[currentIndex.value]) {
327
+ const next = entries.value.slice();
328
+
329
+ next[currentIndex.value] = {
330
+ ...next[currentIndex.value],
331
+ snapshot,
332
+ timestamp: new Date().toISOString(),
333
+ sizeBytes: snapshot.length,
334
+ activePageId,
335
+ logoEdits,
336
+ };
337
+ entries.value = next;
338
+
339
+ return;
340
+ }
341
+
342
+ commitEntry({
343
+ snapshot,
344
+ source: 'manual_edit',
345
+ actor: 'user',
346
+ activePageId,
347
+ logoEdits,
348
+ });
349
+ };
350
+
351
+ // Monotonic — first mutation arms; subsequent ones inside the window
352
+ // don't reset. Gated on isApplyingSnapshot so undo/redo don't leave
353
+ // a stale timer ticking. If a hydration site armed `mergeArmed`
354
+ // immediately before the mutation that triggered this run, transfer
355
+ // the intent into `pendingTimerIsMerge` so the resulting flush
356
+ // merges instead of commits.
357
+ const scheduleAutoCapture = (): void => {
358
+ if (isApplyingSnapshot.value) return;
359
+
360
+ if (idleTimerId !== null) return;
361
+
362
+ if (mergeArmed.value) {
363
+ pendingTimerIsMerge = true;
364
+ mergeArmed.value = false;
365
+ }
366
+
367
+ const idle = pendingTimerIsMerge
368
+ ? MERGE_DEBOUNCE_MS
369
+ : CAPTURE_DEBOUNCE_MS;
370
+
371
+ idleTimerId = setTimeout(flush, idle);
372
+ maxWaitTimerId = setTimeout(flush, CAPTURE_MAX_WAIT_MS);
373
+ };
374
+
375
+ watch(() => useBrandPageMakerStore().brandPageData, scheduleAutoCapture, {
376
+ deep: true,
377
+ });
378
+
379
+ watcherInstalled.value = true;
380
+ }
381
+
382
+ // --- Actions ---
383
+
384
+ // Called once per session by BrandPageMaker.mixin.ts after
385
+ // setIsBrandPageLoaded(true). Installs the watcher.
386
+ function captureBaseline(): void {
387
+ if (baselineCaptured.value) return;
388
+
389
+ installWatcher();
390
+
391
+ const snapshot = serializeCurrentSnapshot();
392
+
393
+ if (!snapshot) return;
394
+
395
+ entries.value = [
396
+ {
397
+ snapshot,
398
+ timestamp: new Date().toISOString(),
399
+ source: 'manual_edit',
400
+ actor: 'user',
401
+ label: 'baseline',
402
+ sizeBytes: snapshot.length,
403
+ activePageId: getCurrentActivePageId(),
404
+ logoEdits: serializeCurrentLogoEdits(),
405
+ },
406
+ ];
407
+ currentIndex.value = 0;
408
+ baselineCaptured.value = true;
409
+ baselineCapturedAt = Date.now();
410
+ }
411
+
412
+ // Force-commit for non-watcher call sites (template switch, AI apply).
413
+ function capture(args: {
414
+ source: HistoryEntry['source'];
415
+ actor: HistoryEntry['actor'];
416
+ label?: string;
417
+ }): 'committed' | 'oversized' | 'noop' | 'skipped' {
418
+ if (isApplyingSnapshot.value) return 'skipped';
419
+
420
+ if (!baselineCaptured.value) return 'skipped';
421
+
422
+ const snapshot = serializeCurrentSnapshot();
423
+
424
+ if (!snapshot) return 'skipped';
425
+
426
+ return commitEntry({
427
+ snapshot,
428
+ activePageId: getCurrentActivePageId(),
429
+ logoEdits: serializeCurrentLogoEdits(),
430
+ ...args,
431
+ });
432
+ }
433
+
434
+ // Cancel pending auto-capture so the forced entry doesn't duplicate
435
+ // with the watcher's eventual flush.
436
+ function captureTemplateSwitch(
437
+ label?: string,
438
+ ): 'committed' | 'oversized' | 'noop' | 'skipped' {
439
+ cancelPendingAutoCapture();
440
+
441
+ return capture({ source: 'template_switch', actor: 'user', label });
442
+ }
443
+
444
+ function captureAiChangeset(
445
+ label?: string,
446
+ ): 'committed' | 'oversized' | 'noop' | 'skipped' {
447
+ cancelPendingAutoCapture();
448
+
449
+ return capture({ source: 'ai_changeset', actor: 'ai_builder', label });
450
+ }
451
+
452
+ // Active-page resolution priority:
453
+ // 1. Diff `cameFromIndex` against target — pages present in target
454
+ // but absent from source are being restored/added; navigate there
455
+ // (undo-of-delete, redo-of-add).
456
+ // 2. Walk back from targetIndex for the closest entry whose
457
+ // activePageId still exists (undo-of-add and non-page edits).
458
+ async function applySnapshotString(
459
+ snapshotStr: string,
460
+ targetIndex: number,
461
+ cameFromIndex: number,
462
+ ): Promise<void> {
463
+ const brandPageMakerStore = useBrandPageMakerStore();
464
+
465
+ let snapshot: BrandPageModel;
466
+
467
+ try {
468
+ snapshot = JSON.parse(snapshotStr) as BrandPageModel;
469
+ } catch (error) {
470
+ console.error('history-store: failed to parse snapshot', error);
471
+
472
+ return;
473
+ }
474
+
475
+ const restoredPageIds = new Set<string>();
476
+
477
+ snapshot.pages?.forEach((p) => {
478
+ if (p?.id) restoredPageIds.add(p.id);
479
+ });
480
+
481
+ let resolvedActivePageId: string | undefined;
482
+
483
+ // Step 1: diff cameFromIndex → first page absent from source wins.
484
+ const sourceEntry = entries.value[cameFromIndex];
485
+
486
+ if (sourceEntry) {
487
+ const sourcePageIds = new Set<string>();
488
+
489
+ try {
490
+ const sourceSnap = JSON.parse(sourceEntry.snapshot) as BrandPageModel;
491
+
492
+ sourceSnap.pages?.forEach((p) => {
493
+ if (p?.id) sourcePageIds.add(p.id);
494
+ });
495
+
496
+ for (const p of snapshot.pages ?? []) {
497
+ if (p?.id && !sourcePageIds.has(p.id)) {
498
+ resolvedActivePageId = p.id;
499
+ break;
500
+ }
501
+ }
502
+ } catch {
503
+ // Source snapshot unparseable — fall through to walk-back.
504
+ }
505
+ }
506
+
507
+ // Step 2: walk-back fallback for non-page-structural edits.
508
+ if (!resolvedActivePageId) {
509
+ for (let i = targetIndex; i >= 0; i--) {
510
+ const candidate = entries.value[i]?.activePageId;
511
+
512
+ if (candidate && restoredPageIds.has(candidate)) {
513
+ resolvedActivePageId = candidate;
514
+ break;
515
+ }
516
+ }
517
+ }
518
+
519
+ const targetEntry = entries.value[targetIndex];
520
+
521
+ isApplyingSnapshot.value = true;
522
+ snapshotAppliedToken.value += 1;
523
+ // Clear any unconsumed merge intent — a stale flag would fold the
524
+ // post-apply tick into the restored entry and erase the redo.
525
+ mergeArmed.value = false;
526
+ pendingTimerIsMerge = false;
527
+ cancelPendingAutoCapture();
528
+
529
+ try {
530
+ await brandPageMakerStore.applyHistorySnapshot(
531
+ snapshot,
532
+ resolvedActivePageId,
533
+ targetEntry?.logoEdits,
534
+ );
535
+ } finally {
536
+ isApplyingSnapshot.value = false;
537
+ }
538
+ }
539
+
540
+ async function undo(): Promise<void> {
541
+ if (!canUndo.value) return;
542
+
543
+ const aiThreadStore = useAiWebsiteBuilderThreadStore();
544
+
545
+ if (aiThreadStore.isProcessing) return;
546
+
547
+ // Actor of the entry being undone (the one we're leaving).
548
+ const undoneActor = entries.value[currentIndex.value]?.actor;
549
+ const cameFromIndex = currentIndex.value;
550
+
551
+ currentIndex.value -= 1;
552
+ const target = entries.value[currentIndex.value];
553
+
554
+ if (target) {
555
+ await applySnapshotString(
556
+ target.snapshot,
557
+ currentIndex.value,
558
+ cameFromIndex,
559
+ );
560
+ }
561
+
562
+ eventTracker.sendUndoEvent({
563
+ entries_count: entries.value.length,
564
+ actor: undoneActor,
565
+ });
566
+ }
567
+
568
+ async function redo(): Promise<void> {
569
+ if (!canRedo.value) return;
570
+
571
+ const aiThreadStore = useAiWebsiteBuilderThreadStore();
572
+
573
+ if (aiThreadStore.isProcessing) return;
574
+
575
+ const cameFromIndex = currentIndex.value;
576
+
577
+ currentIndex.value += 1;
578
+ const target = entries.value[currentIndex.value];
579
+
580
+ if (target) {
581
+ await applySnapshotString(
582
+ target.snapshot,
583
+ currentIndex.value,
584
+ cameFromIndex,
585
+ );
586
+ }
587
+
588
+ eventTracker.sendRedoEvent({
589
+ entries_count: entries.value.length,
590
+ actor: target?.actor,
591
+ });
592
+ }
593
+
594
+ // PRD §3: undo history resets on save/publish. Called by
595
+ // brand-page-container-store.saveBrandPage. No-ops when no baseline
596
+ // exists — unflagged sessions never installed the watcher.
597
+ function reset(): void {
598
+ if (!baselineCaptured.value) return;
599
+
600
+ cancelPendingAutoCapture();
601
+ mergeArmed.value = false;
602
+ pendingTimerIsMerge = false;
603
+
604
+ entries.value = [];
605
+ currentIndex.value = -1;
606
+ baselineCaptured.value = false;
607
+ baselineCapturedAt = 0;
608
+
609
+ captureBaseline();
610
+ }
611
+
612
+ // Hydration write sites call this immediately before their imperative
613
+ // mutation. The arm is auto-cleared on the next Vue tick — `nextTick`
614
+ // runs AFTER all queued reactive effects (including the watcher), so
615
+ // if the hydration write triggered `scheduleAutoCapture`, the arm has
616
+ // already been transferred into `pendingTimerIsMerge` before this
617
+ // cleanup fires. If no mutation followed (e.g. the user navigated
618
+ // away before the flagged write), the cleanup drops the stale arm so
619
+ // it can't be consumed by an unrelated future mutation.
620
+ function mergeNextMutation(): void {
621
+ mergeArmed.value = true;
622
+ // setTimeout(0) is a macrotask — runs AFTER the synchronous write
623
+ // and AFTER Vue's microtask flush of the watcher's reactive effect.
624
+ // If `scheduleAutoCapture` fired in between (the imperative write
625
+ // triggered the watcher), the arm has already been transferred into
626
+ // `pendingTimerIsMerge` before this cleanup runs.
627
+ setTimeout(() => {
628
+ mergeArmed.value = false;
629
+ }, 0);
630
+ }
631
+
632
+ // --- Instrumentation (NFR-04a) ---
633
+
634
+ function getCurrentSizeBytes(): number {
635
+ return entries.value.reduce((sum, e) => sum + e.sizeBytes, 0);
636
+ }
637
+
638
+ function getCurrentEntryCount(): number {
639
+ return entries.value.length;
640
+ }
641
+
642
+ return {
643
+ // State
644
+ entries,
645
+ currentIndex,
646
+ isApplyingSnapshot,
647
+ snapshotAppliedToken,
648
+ baselineCaptured,
649
+
650
+ // Getters
651
+ canUndo,
652
+ canRedo,
653
+
654
+ // Actions
655
+ captureBaseline,
656
+ capture,
657
+ captureTemplateSwitch,
658
+ captureAiChangeset,
659
+ mergeNextMutation,
660
+ undo,
661
+ redo,
662
+ reset,
663
+ getCurrentSizeBytes,
664
+ getCurrentEntryCount,
665
+ };
666
+ });
@@ -14,6 +14,11 @@ export interface LogoDraftAsset {
14
14
  tick: number;
15
15
  isByo: boolean;
16
16
  logoEditUrl?: string;
17
+ // The `logoTemplateAsset` active when this draft was created. Stamped at
18
+ // upload/setState time so undo/redo can restore the right template token
19
+ // + isByo flag from a snapshot's `logoDraftToken` alone — without us
20
+ // bloating each history entry with a copy of the asset.
21
+ templateAsset?: LogoTemplateAsset;
17
22
  }
18
23
 
19
24
  export interface LogoStoreState {
@@ -33,6 +33,7 @@ export const useBrandPageMakerUserStore = defineStore('brandPageMakerUser', {
33
33
  canUseGettyVideos: false,
34
34
  canUseGoogleAdsense: false,
35
35
  googleAdsenseEnabled: false,
36
+ canUseUndoRedo: false,
36
37
  },
37
38
  emailAddress: '',
38
39
  hasPremiumSubscription: false,
@@ -1126,7 +1126,9 @@
1126
1126
  "quickLinkBar": {
1127
1127
  "myWebPageLabel": "Meine Webseite:",
1128
1128
  "copyButton": "Kopieren",
1129
- "copiedButton": "Kopiert!"
1129
+ "copiedButton": "Kopiert!",
1130
+ "undoLabel": "Rückgängig machen",
1131
+ "redoLabel": "Wiederholen"
1130
1132
  },
1131
1133
  "saveContactSettings": {
1132
1134
  "saveContentInfo": "Lassen Sie Besucher Ihre Kontaktinformationen speichern, wenn sie Ihre digitale Visitenkarte ansehen.",
@@ -1527,6 +1529,8 @@
1527
1529
  "iconFillColorTitle": "Farbe der Symbolfüllung",
1528
1530
  "iconBackgroundColorTitle": "Symbolhintergrundfarbe",
1529
1531
  "paddingTitle": "Polsterung",
1532
+ "topPaddingTitle": "Polsterung oben",
1533
+ "bottomPaddingTitle": "Polsterung unten",
1530
1534
  "dividerStyleTitle": "Trennlinienstil",
1531
1535
  "dividerColorTitle": "Trennlinienfarbe",
1532
1536
  "addItemTitle": "Artikel hinzufügen",