@dev.smartpricing/message-composer-layer 1.0.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 (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. package/package.json +64 -0
@@ -0,0 +1,123 @@
1
+ <script lang="ts" setup>
2
+ import type { UnsubscribeLinkBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { DEFAULT_UNSUBSCRIBE_LINK_SETTINGS } from '@dev.smartpricing/message-composer-utils/types'
4
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
5
+ import { computed } from 'vue'
6
+ import { useAppContextStore } from '@/stores/appContext'
7
+ import ColorInput from '~/components/Common/ColorInput.vue'
8
+ import FontFamilySelect from '~/components/Common/FontFamilySelect.vue'
9
+ import SectionHeader from '~/components/Common/SectionHeader.vue'
10
+
11
+ const props = defineProps<{
12
+ block: UnsubscribeLinkBlock
13
+ }>()
14
+
15
+ const emailEditorStore = useEmailEditorStore()
16
+ const appContext = useAppContextStore()
17
+
18
+ const content = computed({
19
+ get: () => props.block.settings.content,
20
+ set: (value) => {
21
+ emailEditorStore.updateBlock(props.block.id, {
22
+ settings: { ...props.block.settings, content: value },
23
+ })
24
+ },
25
+ })
26
+
27
+ function resetTextStyle() {
28
+ emailEditorStore.updateBlock(props.block.id, {
29
+ settings: {
30
+ ...props.block.settings,
31
+ fontFamily: DEFAULT_UNSUBSCRIBE_LINK_SETTINGS.fontFamily,
32
+ fontSize: DEFAULT_UNSUBSCRIBE_LINK_SETTINGS.fontSize,
33
+ },
34
+ })
35
+ }
36
+
37
+ function resetColors() {
38
+ emailEditorStore.updateBlock(props.block.id, {
39
+ settings: {
40
+ ...props.block.settings,
41
+ textColor: DEFAULT_UNSUBSCRIBE_LINK_SETTINGS.textColor,
42
+ backgroundColor: DEFAULT_UNSUBSCRIBE_LINK_SETTINGS.backgroundColor,
43
+ },
44
+ })
45
+ }
46
+
47
+ defineOptions({
48
+ name: 'UnsubscribeLinkSettings',
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <div class="space-y-6 p-4" :data-testid="unsubscribeLinkSettingsTestIds.container">
54
+ <!-- Content Editor -->
55
+ <div class="space-y-3">
56
+ <p class="paragraph-md-bold">
57
+ {{ $t('editor.email.blocks.heading.settings.content') }}
58
+ </p>
59
+ <RichTextEditor
60
+ v-model:text="content"
61
+ preset="email"
62
+ bold
63
+ italic
64
+ underline
65
+ link
66
+ dynamic-values
67
+ :data-testid="unsubscribeLinkSettingsTestIds.contentEditor"
68
+ />
69
+ </div>
70
+
71
+ <!-- Text Style Section -->
72
+ <div class="space-y-3">
73
+ <SectionHeader
74
+ :title="$t('editor.email.blocks.heading.settings.textStyle')"
75
+ show-reset
76
+ @reset="resetTextStyle"
77
+ />
78
+
79
+ <div class="grid grid-cols-2 gap-3">
80
+ <FontFamilySelect
81
+ v-model="block.settings.fontFamily"
82
+ :label="$t('editor.email.blocks.heading.settings.font')"
83
+ :default-value="emailEditorStore.resolvedSettings.fontFamily"
84
+ :data-testid="unsubscribeLinkSettingsTestIds.fontFamilySelect"
85
+ />
86
+ <UFormField
87
+ :label="$t('editor.email.blocks.heading.settings.size')"
88
+ :data-testid="unsubscribeLinkSettingsTestIds.fontSizeField"
89
+ >
90
+ <UFieldGroup class="w-full">
91
+ <UInputNumber
92
+ v-model="block.settings.fontSize"
93
+ :data-testid="unsubscribeLinkSettingsTestIds.fontSizeInput"
94
+ />
95
+ <UBadge color="neutral" variant="outline">px</UBadge>
96
+ </UFieldGroup>
97
+ </UFormField>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Color Section -->
102
+ <div class="space-y-3">
103
+ <SectionHeader
104
+ :title="$t('editor.email.blocks.heading.settings.color')"
105
+ show-reset
106
+ @reset="resetColors"
107
+ />
108
+
109
+ <ColorInput
110
+ v-model="block.settings.textColor"
111
+ :label="$t('editor.email.blocks.heading.settings.text')"
112
+ :default-value="emailEditorStore.resolvedSettings.textColor"
113
+ :data-testid="unsubscribeLinkSettingsTestIds.textColorInput"
114
+ />
115
+
116
+ <ColorInput
117
+ v-model="block.settings.backgroundColor"
118
+ :label="$t('editor.email.blocks.heading.settings.backgroundBlock')"
119
+ :data-testid="unsubscribeLinkSettingsTestIds.backgroundColorInput"
120
+ />
121
+ </div>
122
+ </div>
123
+ </template>
@@ -0,0 +1,143 @@
1
+ <script lang="ts" setup>
2
+ import { watch, ref, nextTick } from 'vue'
3
+ import { useDragAndDrop } from '@formkit/drag-and-drop/vue'
4
+ import IndexBlock from './Blocks/IndexBlock.vue'
5
+ import type { EmailBlock } from '@dev.smartpricing/message-composer-utils/types'
6
+
7
+ const emailEditorStore = useEmailEditorStore()
8
+
9
+ const props = defineProps<{
10
+ version: 'mobile' | 'desktop'
11
+ }>()
12
+
13
+ const emit = defineEmits<{
14
+ 'block-added': [payload: { type: string; adding_mode: string }]
15
+ 'block-deleted': [payload: { type: string }]
16
+ 'block-duplicated': [payload: { type: string }]
17
+ 'block-sorted': [payload: { type: string; before_sort_position: number; new_position: number }]
18
+ }>()
19
+
20
+ // Flag to prevent circular updates
21
+ const isSyncing = ref(false)
22
+
23
+ function handleBlockClick(blockId: string) {
24
+ emailEditorStore.selectBlock(blockId)
25
+ }
26
+
27
+ function handleDuplicate(blockId: string) {
28
+ const block = emailEditorStore.blocks.find((b) => b.id === blockId)
29
+ const blockType = block?.type ?? 'unknown'
30
+ emailEditorStore.duplicateBlock(blockId)
31
+ emit('block-duplicated', { type: blockType })
32
+ }
33
+
34
+ function handleDelete(blockId: string) {
35
+ const block = emailEditorStore.blocks.find((b) => b.id === blockId)
36
+ const blockType = block?.type ?? 'unknown'
37
+ emailEditorStore.removeBlock(blockId)
38
+ emit('block-deleted', { type: blockType })
39
+ }
40
+
41
+ // Setup drag-and-drop for canvas
42
+ const [canvasRef, canvasValues] = useDragAndDrop(emailEditorStore.blocks, {
43
+ group: 'mail-builder',
44
+ dragEffectAllowed: 'move',
45
+ dragDropEffect: 'move',
46
+ sortable: true,
47
+ onSort(data) {
48
+ const draggedBlock = data.draggedNodes[0]?.data?.value as EmailBlock | undefined
49
+ if (draggedBlock) {
50
+ emit('block-sorted', {
51
+ type: draggedBlock.type,
52
+ before_sort_position: data.previousPosition,
53
+ new_position: data.position,
54
+ })
55
+ }
56
+ },
57
+ onTransfer(data) {
58
+ const transferredBlock = data.draggedNodes[0]?.data?.value as EmailBlock | undefined
59
+ if (transferredBlock) {
60
+ emit('block-added', { type: transferredBlock.type, adding_mode: 'Drag' })
61
+ }
62
+ },
63
+ })
64
+
65
+ // Sync canvasValues back to store when drag-and-drop changes them
66
+ watch(
67
+ canvasValues,
68
+ (newBlocks) => {
69
+ if (isSyncing.value) return
70
+ isSyncing.value = true
71
+ emailEditorStore.setBlocks(newBlocks)
72
+ nextTick(() => {
73
+ isSyncing.value = false
74
+ })
75
+ },
76
+ { deep: true },
77
+ )
78
+
79
+ // Sync store blocks to canvas when store changes externally
80
+ watch(
81
+ () => emailEditorStore.blocks,
82
+ (newBlocks) => {
83
+ if (isSyncing.value) return
84
+ isSyncing.value = true
85
+ canvasValues.value = [...newBlocks]
86
+ nextTick(() => {
87
+ isSyncing.value = false
88
+ })
89
+ },
90
+ { deep: true },
91
+ )
92
+
93
+ defineOptions({
94
+ name: 'EmailEditorCanvas',
95
+ })
96
+ </script>
97
+
98
+ <template>
99
+ <div class="flex min-h-0 flex-col" :data-testid="canvasTestIds.container">
100
+ <!-- Canvas Content -->
101
+ <div class="flex-1 overflow-y-auto bg-muted" :data-testid="canvasTestIds.scrollArea">
102
+ <!-- Outer container with page background -->
103
+ <div
104
+ class="mx-auto"
105
+ :class="{
106
+ 'border border-t-0 border-muted': version === 'mobile',
107
+ 'border-b border-muted': version === 'desktop',
108
+ }"
109
+ @click="emailEditorStore.clearSelection()"
110
+ :style="{ backgroundColor: emailEditorStore.resolvedSettings.backgroundColor }"
111
+ :data-testid="canvasTestIds.pageBackground"
112
+ >
113
+ <!-- Email content container with body background -->
114
+ <div
115
+ class="min-h-[400px] flex flex-col mx-auto"
116
+ :style="{
117
+ backgroundColor: emailEditorStore.resolvedSettings.contentBackgroundColor,
118
+ maxWidth: `${emailEditorStore.resolvedSettings.contentWidth}px`,
119
+ gap: `${emailEditorStore.resolvedSettings.verticalSpacing}px`,
120
+ paddingLeft: `${emailEditorStore.resolvedSettings.horizontalSpacing}px`,
121
+ paddingRight: `${emailEditorStore.resolvedSettings.horizontalSpacing}px`,
122
+ }"
123
+ ref="canvasRef"
124
+ @click.stop
125
+ :data-testid="canvasTestIds.contentArea"
126
+ >
127
+ <IndexBlock
128
+ v-for="block in canvasValues"
129
+ :key="block.id"
130
+ :block="block"
131
+ :is-selected="emailEditorStore.selectedBlockId === block.id"
132
+ :show-delete="true"
133
+ :show-duplicate="block.type !== 'unsubscribe_link'"
134
+ :data-testid="`${canvasTestIds.blockItem}-${block.id}`"
135
+ @click="handleBlockClick"
136
+ @duplicate="handleDuplicate"
137
+ @delete="handleDelete"
138
+ />
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </template>
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ import ColorInput from '~/components/Common/ColorInput.vue'
3
+ import FontFamilySelect from '~/components/Common/FontFamilySelect.vue'
4
+ import type { SafeFonts } from '@dev.smartpricing/message-composer-utils/types'
5
+
6
+ const emailEditorStore = useEmailEditorStore()
7
+
8
+ const { brand, brands, globalSettings, brandColors } = storeToRefs(emailEditorStore)
9
+
10
+ defineOptions({
11
+ name: 'EmailEditorGeneralStyleSettings',
12
+ })
13
+ </script>
14
+
15
+ <template>
16
+ <div class="space-y-8" :data-testid="generalStyleSettingsTestIds.container">
17
+ <!-- Brand selector -->
18
+ <div class="space-y-3">
19
+ <h3 class="paragraph-md-bold">
20
+ {{ $t('editor.email.generalStyle.brandSection') }}
21
+ </h3>
22
+ <USelectMenu
23
+ v-model="brand"
24
+ :items="brands"
25
+ label-key="name"
26
+ :placeholder="$t('editor.email.generalStyle.selectBrand')"
27
+ class="w-full"
28
+ />
29
+ </div>
30
+
31
+ <!-- Typography section -->
32
+ <div class="space-y-3">
33
+ <h3 class="paragraph-md-bold">
34
+ {{ $t('editor.email.generalStyle.typographySection') }}
35
+ </h3>
36
+
37
+ <!-- Font Family -->
38
+ <FontFamilySelect
39
+ v-model="globalSettings.fontFamily"
40
+ :label="$t('editor.email.generalStyle.font')"
41
+ :data-testid="generalStyleSettingsTestIds.fontFamilySelect"
42
+ :default-value="brand?.default_email_settings.fontFamily as SafeFonts"
43
+ />
44
+ </div>
45
+
46
+ <!-- Colors section -->
47
+ <div class="space-y-3">
48
+ <h3 class="paragraph-md-bold">
49
+ {{ $t('editor.email.generalStyle.colorsSection') }}
50
+ </h3>
51
+
52
+ <!-- Text Color -->
53
+ <ColorInput
54
+ v-model="globalSettings.textColor"
55
+ :label="$t('editor.email.generalStyle.textColor')"
56
+ :brand-colors="brandColors"
57
+ :data-testid="generalStyleSettingsTestIds.textColorInput"
58
+ :default-value="brand?.default_email_settings.textColor"
59
+ />
60
+
61
+ <!-- Background Color (page) -->
62
+ <ColorInput
63
+ v-model="globalSettings.backgroundColor"
64
+ :label="$t('editor.email.generalStyle.backgroundColor')"
65
+ :brand-colors="brandColors"
66
+ :data-testid="generalStyleSettingsTestIds.backgroundColorInput"
67
+ :default-value="brand?.default_email_settings.backgroundColor"
68
+ />
69
+
70
+ <!-- Body Color (content area) -->
71
+ <ColorInput
72
+ v-model="globalSettings.contentBackgroundColor"
73
+ :label="$t('editor.email.generalStyle.bodyColor')"
74
+ :brand-colors="brandColors"
75
+ :data-testid="generalStyleSettingsTestIds.bodyColorInput"
76
+ :default-value="brand?.default_email_settings.contentBackgroundColor"
77
+ />
78
+ </div>
79
+
80
+ <!-- Spacing section -->
81
+ <div class="space-y-3">
82
+ <h3 class="paragraph-md-bold">
83
+ {{ $t('editor.email.generalStyle.spacingSection') }}
84
+ </h3>
85
+
86
+ <!-- Vertical Spacing -->
87
+ <UFormField
88
+ :label="$t('editor.email.generalStyle.verticalSpacing')"
89
+ :data-testid="generalStyleSettingsTestIds.verticalSpacingField"
90
+ >
91
+ <SSlider
92
+ v-model="globalSettings.verticalSpacing"
93
+ :min="0"
94
+ :max="50"
95
+ :data-testid="generalStyleSettingsTestIds.verticalSpacingSlider"
96
+ />
97
+ </UFormField>
98
+
99
+ <!-- Horizontal Spacing -->
100
+ <UFormField
101
+ :label="$t('editor.email.generalStyle.horizontalSpacing')"
102
+ :data-testid="generalStyleSettingsTestIds.horizontalSpacingField"
103
+ >
104
+ <SSlider
105
+ v-model="globalSettings.horizontalSpacing"
106
+ :min="0"
107
+ :max="50"
108
+ :data-testid="generalStyleSettingsTestIds.horizontalSpacingSlider"
109
+ />
110
+ </UFormField>
111
+ </div>
112
+
113
+ <!-- Reset button -->
114
+ <UButton
115
+ variant="outline"
116
+ color="neutral"
117
+ icon="ph:arrow-counter-clockwise"
118
+ block
119
+ :data-testid="generalStyleSettingsTestIds.resetButton"
120
+ @click="emailEditorStore.resetGlobalSettings()"
121
+ >
122
+ {{ $t('editor.email.generalStyle.reset') }}
123
+ </UButton>
124
+ </div>
125
+ </template>
@@ -0,0 +1,77 @@
1
+ <script lang="ts" setup>
2
+ import EmailEditorLeftSidebar from './LeftSidebar.vue'
3
+ import EmailEditorCanvas from './Canvas.vue'
4
+ import SendTestEmailModal from '../SendTestEmailModal.vue'
5
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
6
+ import type { TabsItem } from '#ui/types'
7
+
8
+ defineOptions({
9
+ name: 'EmailEditorIndex',
10
+ })
11
+
12
+ defineProps<{
13
+ message: PostEntity<EmailMessage>
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ 'block-added': [payload: { type: string; adding_mode: string }]
18
+ 'block-deleted': [payload: { type: string }]
19
+ 'block-duplicated': [payload: { type: string }]
20
+ 'block-sorted': [payload: { type: string; before_sort_position: number; new_position: number }]
21
+ 'widget-tab-changed': [tab: string]
22
+ 'test-sent': [payload: { to_recipients: number }]
23
+ }>()
24
+
25
+ const { t } = useI18n()
26
+ const tabs: TabsItem[] = [
27
+ { label: t('common.desktop'), value: 'desktop' },
28
+ { label: t('common.mobile'), value: 'mobile' },
29
+ ] as const
30
+
31
+ const currentView = ref<(typeof tabs)[number]['value']>('desktop')
32
+
33
+ const width = computed(() => {
34
+ return currentView.value === 'mobile' ? '400px' : '100%'
35
+ })
36
+ </script>
37
+
38
+ <template>
39
+ <div class="flex h-full" :data-testid="emailEditorIndexTestIds.container">
40
+ <!-- Left Sidebar: Block Library + Settings -->
41
+ <EmailEditorLeftSidebar
42
+ class="w-md flex-shrink-0"
43
+ @block-added="emit('block-added', $event)"
44
+ @widget-tab-changed="emit('widget-tab-changed', $event)"
45
+ />
46
+
47
+ <!-- Main Canvas -->
48
+ <div class="flex-1 min-h-0 flex flex-col">
49
+ <div class="grid grid-cols-3 gap-2 border-b border-neutral-200 py-1 px-4">
50
+ <div></div>
51
+ <div class="flex justify-center items-center">
52
+ <UTabs
53
+ size="xs"
54
+ variant="pill"
55
+ :items="tabs"
56
+ v-model="currentView"
57
+ :ui="{ root: 'gap-0 w-full' }"
58
+ :data-testid="emailEditorIndexTestIds.viewTabs"
59
+ />
60
+ </div>
61
+ <div class="flex justify-end items-center">
62
+ <SendTestEmailModal :message="message" @test-sent="emit('test-sent', $event)" />
63
+ <CommonMessagePreviewSlideover v-if="message" :message="message" />
64
+ </div>
65
+ </div>
66
+ <EmailEditorCanvas
67
+ :version="currentView"
68
+ :style="{ width }"
69
+ class="mx-auto flex-1"
70
+ @block-added="emit('block-added', $event)"
71
+ @block-deleted="emit('block-deleted', $event)"
72
+ @block-duplicated="emit('block-duplicated', $event)"
73
+ @block-sorted="emit('block-sorted', $event)"
74
+ />
75
+ </div>
76
+ </div>
77
+ </template>
@@ -0,0 +1,95 @@
1
+ <script lang="ts" setup>
2
+ import { watch } from 'vue'
3
+ import EmailEditorBlockLibrary from './BlockLibrary.vue'
4
+ import EmailEditorGeneralStyleSettings from './GeneralStyleSettings.vue'
5
+ import EmailEditorBlockSettings from './BlockSettings.vue'
6
+
7
+ const emailEditorStore = useEmailEditorStore()
8
+ const { t } = useI18n()
9
+
10
+ const emit = defineEmits<{
11
+ 'widget-tab-changed': [tab: string]
12
+ 'block-added': [payload: { type: string; adding_mode: string }]
13
+ }>()
14
+
15
+ // Tab state (only when no block selected)
16
+ const currentTab = ref<'elements' | 'general'>('elements')
17
+
18
+ watch(currentTab, (tab) => {
19
+ emit('widget-tab-changed', tab)
20
+ })
21
+
22
+ // Toggle between library+general vs block settings
23
+ const showLibrary = computed(() => !emailEditorStore.selectedBlockId)
24
+
25
+ const tabItems = computed(() => [
26
+ {
27
+ label: t('editor.email.sidebar.elementsTab'),
28
+ value: 'elements',
29
+ },
30
+ {
31
+ label: t('editor.email.sidebar.generalStyleTab'),
32
+ value: 'general',
33
+ },
34
+ ])
35
+
36
+ defineOptions({
37
+ name: 'EmailEditorLeftSidebar',
38
+ })
39
+ </script>
40
+
41
+ <template>
42
+ <div
43
+ class="flex h-full flex-col border-r border-neutral-200 bg-white"
44
+ :data-testid="leftSidebarTestIds.container"
45
+ >
46
+ <!-- No block selected: Show tabs with Elements/General Style -->
47
+ <div v-if="showLibrary" class="flex h-full flex-col">
48
+ <!-- Tab navigation -->
49
+ <UTabs
50
+ v-model="currentTab"
51
+ :items="tabItems"
52
+ size="md"
53
+ variant="link"
54
+ color="secondary"
55
+ :data-testid="leftSidebarTestIds.tabs"
56
+ />
57
+
58
+ <!-- Tab content -->
59
+ <div class="flex-1 overflow-y-auto p-4">
60
+ <EmailEditorBlockLibrary
61
+ v-if="currentTab === 'elements'"
62
+ @block-added="emit('block-added', $event)"
63
+ />
64
+ <EmailEditorGeneralStyleSettings v-else-if="currentTab === 'general'" />
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Block selected: Show settings (unchanged) -->
69
+ <div v-else-if="emailEditorStore.selectedBlock" class="flex h-full flex-col">
70
+ <!-- Header with back button -->
71
+ <div class="grid grid-cols-3 border-b border-neutral-200 px-4 py-3">
72
+ <div>
73
+ <UButton
74
+ variant="ghost"
75
+ color="neutral"
76
+ icon="i-heroicons-arrow-left"
77
+ size="sm"
78
+ square
79
+ :data-testid="leftSidebarTestIds.backButton"
80
+ @click="emailEditorStore.clearSelection()"
81
+ />
82
+ </div>
83
+ <h2 class="flex-1 text-center text-base font-medium italic">
84
+ {{ $t(`editor.email.blocks.${emailEditorStore.selectedBlock.type}.name`) }}
85
+ </h2>
86
+ <div></div>
87
+ </div>
88
+
89
+ <!-- Settings panel -->
90
+ <div class="flex-1 overflow-y-auto">
91
+ <EmailEditorBlockSettings :block="emailEditorStore.selectedBlock" />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </template>
@@ -0,0 +1,124 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ EmailMessage,
4
+ PostEntity,
5
+ BrandFromDb,
6
+ } from '@dev.smartpricing/message-composer-utils/types'
7
+ import { extractEmailParameters } from '@dev.smartpricing/message-composer-utils/utils'
8
+ import { ref, computed, watch } from 'vue'
9
+ import { useDebounceFn } from '@vueuse/core'
10
+ import HtmlPreviewIframe from '@/components/Common/HtmlPreviewIframe.vue'
11
+ import { usePreviewEmailMutation } from '~/queries/compilation'
12
+
13
+ defineOptions({ name: 'EmailInlinePreview' })
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ message: PostEntity<EmailMessage> | EmailMessage
18
+ brand?: BrandFromDb | null
19
+ dynamicData?: Record<string, string>
20
+ view?: 'desktop' | 'mobile'
21
+ showSubject?: boolean
22
+ }>(),
23
+ { view: 'desktop', showSubject: false, brand: null },
24
+ )
25
+
26
+ const iframeWidth = computed(() => {
27
+ return props.view === 'mobile' ? '400px' : '600px'
28
+ })
29
+
30
+ // Extract parameters client-side
31
+ const parameters = computed(() => extractEmailParameters(props.message))
32
+
33
+ const parameterValues = ref<Record<string, string>>({})
34
+ const { mutate: preview, data: previewed, isLoading: isPreviewing } = usePreviewEmailMutation()
35
+
36
+ const previewEmail = useDebounceFn(() => {
37
+ preview({
38
+ message: props.message,
39
+ parameters: parameterValues.value,
40
+ brand: props.brand
41
+ ? {
42
+ default_email_settings: props.brand.default_email_settings,
43
+ default_button_settings: props.brand.default_button_settings,
44
+ default_socials: normalizeBrandSocials(props.brand.default_socials),
45
+ logo_url: props.brand.logo_url,
46
+ }
47
+ : null,
48
+ })
49
+ }, 0)
50
+
51
+ watch(
52
+ parameters,
53
+ (params) => {
54
+ const defaults = props.dynamicData || {}
55
+ parameterValues.value = params.reduce(
56
+ (acc, param) => {
57
+ acc[param] = defaults[param] || `Sample ${param}`
58
+ return acc
59
+ },
60
+ {} as Record<string, string>,
61
+ )
62
+
63
+ previewEmail()
64
+ },
65
+ { immediate: true },
66
+ )
67
+
68
+ const iframeContent = ref<string>('')
69
+
70
+ watch(
71
+ () => previewed.value,
72
+ (result) => {
73
+ if (result?.render_type === 'email_v1') {
74
+ iframeContent.value = result.html
75
+ }
76
+ },
77
+ )
78
+ </script>
79
+
80
+ <template>
81
+ <div
82
+ class="flex-1 bg-neutral-50 flex items-start justify-center overflow-auto"
83
+ :data-testid="emailPreviewTestIds.root"
84
+ >
85
+ <div
86
+ v-if="isPreviewing"
87
+ class="space-y-2"
88
+ :style="{ width: iframeWidth }"
89
+ :data-testid="emailPreviewTestIds.loadingSkeleton"
90
+ >
91
+ <USkeleton class="h-6 w-1/3 bg-white" />
92
+ <USkeleton class="h-4 bg-white" />
93
+ <USkeleton class="h-4 w-5/8 bg-white" />
94
+ <USkeleton class="h-32 bg-white" />
95
+ <USkeleton class="h-12 bg-white" />
96
+ </div>
97
+
98
+ <div v-else-if="iframeContent" :style="{ width: iframeWidth }" class="relative overflow-auto">
99
+ <p
100
+ v-if="showSubject"
101
+ class="pb-4 bg-white mb-px sticky top-0"
102
+ :data-testid="emailPreviewTestIds.subject"
103
+ >
104
+ <span class="paragraph-sm-bold">{{ $t('email_preview.subject') }}:</span>
105
+ <span class="paragraph-sm">{{ message.body.subject }}</span>
106
+ </p>
107
+ <HtmlPreviewIframe
108
+ :html="iframeContent"
109
+ title="Email preview"
110
+ :min-height="600"
111
+ class="border-0 bg-white w-full"
112
+ :data-testid="emailPreviewTestIds.iframe"
113
+ />
114
+ </div>
115
+
116
+ <div
117
+ v-else
118
+ class="flex items-center justify-center text-gray-500"
119
+ :data-testid="emailPreviewTestIds.emptyState"
120
+ >
121
+ <p>{{ $t('email_preview.no_preview') }}</p>
122
+ </div>
123
+ </div>
124
+ </template>