@dev.smartpricing/message-composer-layer 1.0.25 → 1.0.26

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.
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import { shallowRef } from 'vue'
3
+ import type { BrandFromDb } from '@dev.smartpricing/message-composer-utils/types'
4
+
5
+ const emailEditorStore = useEmailEditorStore()
6
+ const { brand, brands } = storeToRefs(emailEditorStore)
7
+
8
+ const { t } = useI18n()
9
+ const { confirm } = useConfirm()
10
+
11
+ // Local selection tracks USelectMenu value; reverted on cancel
12
+ const selectedBrand = shallowRef<BrandFromDb | undefined>(brand.value)
13
+
14
+ function handleBrandChange(newBrand: BrandFromDb) {
15
+ if (!newBrand || newBrand.id === brand.value?.id) return
16
+
17
+ confirm({
18
+ title: t('dialogs.email_change_brand.title'),
19
+ message: t('dialogs.email_change_brand.message'),
20
+ confirmProps: {
21
+ label: t('dialogs.email_change_brand.confirm', [newBrand.name]),
22
+ },
23
+ cancelProps: {
24
+ label: t('dialogs.email_change_brand.cancel'),
25
+ },
26
+ action: () => {
27
+ emailEditorStore.resetToBrand(newBrand)
28
+ },
29
+ }).then((confirmed) => {
30
+ if (!confirmed) {
31
+ selectedBrand.value = brand.value
32
+ }
33
+ })
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <USelectMenu
39
+ v-model="selectedBrand"
40
+ :items="brands"
41
+ label-key="name"
42
+ :placeholder="$t('editor.email.generalStyle.selectBrand')"
43
+ class="w-full"
44
+ :data-testid="brandSwitcherTestIds.select"
45
+ @update:model-value="handleBrandChange"
46
+ />
47
+ </template>
@@ -3,7 +3,7 @@ import type {
3
3
  BrandDefaultEmailSettings,
4
4
  BrandDefaultButtonSettings,
5
5
  } from '@dev.smartpricing/message-composer-utils/types'
6
- import ColorThief from 'colorthief'
6
+ import { getPaletteSync } from 'colorthief'
7
7
 
8
8
  import FontFamilySelect from '~/components/Common/FontFamilySelect.vue'
9
9
  import ColorInput from '~/components/Common/ColorInput.vue'
@@ -38,14 +38,12 @@ async function extractColorsFromImage(file: File) {
38
38
  img.onload = () => resolve()
39
39
  img.onerror = reject
40
40
  })
41
- // @ts-expect-error colorthief types lack construct signature
42
- const colorThief = new ColorThief()
43
- const palette = colorThief.getPalette(img, 4)
41
+ const palette = getPaletteSync(img, { colorCount: 4 })
44
42
  const extracted = new Set(['#FFFFFF', '#000000'])
45
- for (const [r, g, b] of palette) {
46
- const hex =
47
- `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase()
48
- extracted.add(hex)
43
+ if (palette) {
44
+ for (const color of palette) {
45
+ extracted.add(color.hex().toUpperCase())
46
+ }
49
47
  }
50
48
  colors.value = [...extracted]
51
49
  } catch (err) {
@@ -89,7 +89,7 @@ defineOptions({
89
89
  }}</span>
90
90
  </div>
91
91
  <div v-else :style="buttonStyles" class="button-block" :data-testid="buttonBlockTestIds.button">
92
- <EditorContent :editor="editor" class="button-content" />
92
+ <EditorContent :editor="editor" class="button-content pointer-events-none" />
93
93
  </div>
94
94
  </div>
95
95
  </template>
@@ -65,7 +65,7 @@ defineOptions({
65
65
  <template>
66
66
  <div :style="containerStyles" :data-testid="discountCodeBlockTestIds.container">
67
67
  <div :style="discountCodeStyles" :data-testid="discountCodeBlockTestIds.code">
68
- <EditorContent v-if="editor" :editor="editor" />
68
+ <EditorContent v-if="editor" :editor="editor" class="pointer-events-none" />
69
69
  <span v-else class="text-neutral-400">
70
70
  {{ $t('editor.email.blocks.discount.placeholder') }}
71
71
  </span>
@@ -49,7 +49,7 @@ defineOptions({
49
49
  color: block.settings.textColor ?? emailEditorStore.resolvedSettings.textColor,
50
50
  }"
51
51
  >
52
- <EditorContent :editor="editor" />
52
+ <EditorContent :editor="editor" class="pointer-events-none" />
53
53
  <div
54
54
  v-show="editor?.isEmpty"
55
55
  class="absolute inset-0 w-full h-full flex items-center justify-center text-neutral-400 text-sm"
@@ -49,7 +49,7 @@ defineOptions({
49
49
  color: block.settings.textColor ?? emailEditorStore.resolvedSettings.textColor,
50
50
  }"
51
51
  >
52
- <EditorContent :editor="editor" />
52
+ <EditorContent :editor="editor" class="pointer-events-none" />
53
53
  <div
54
54
  v-show="editor?.isEmpty"
55
55
  class="absolute inset-0 w-full h-full flex items-center justify-center text-neutral-400 text-sm"
@@ -49,7 +49,7 @@ defineOptions({
49
49
  color: block.settings.textColor ?? emailEditorStore.resolvedSettings.textColor,
50
50
  }"
51
51
  >
52
- <EditorContent :editor="editor" />
52
+ <EditorContent :editor="editor" class="pointer-events-none" />
53
53
  <div
54
54
  v-show="editor?.isEmpty"
55
55
  class="absolute inset-0 w-full h-full flex items-center justify-center text-neutral-400 text-sm"
@@ -48,7 +48,11 @@ defineOptions({
48
48
  textAlign: block.settings.alignment,
49
49
  }"
50
50
  >
51
- <EditorContent :editor="editor" :data-testid="unsubscribeLinkBlockTestIds.editor" />
51
+ <EditorContent
52
+ :editor="editor"
53
+ :data-testid="unsubscribeLinkBlockTestIds.editor"
54
+ class="pointer-events-none"
55
+ />
52
56
  <div
53
57
  v-show="editor?.isEmpty"
54
58
  class="absolute inset-0 w-full h-full flex items-center justify-center text-neutral-400 text-sm"
@@ -5,7 +5,7 @@ import type { SafeFonts } from '@dev.smartpricing/message-composer-utils/types'
5
5
 
6
6
  const emailEditorStore = useEmailEditorStore()
7
7
 
8
- const { brand, brands, globalSettings, brandColors } = storeToRefs(emailEditorStore)
8
+ const { brand, globalSettings, brandColors } = storeToRefs(emailEditorStore)
9
9
 
10
10
  defineOptions({
11
11
  name: 'EmailEditorGeneralStyleSettings',
@@ -19,13 +19,7 @@ defineOptions({
19
19
  <h3 class="paragraph-md-bold">
20
20
  {{ $t('editor.email.generalStyle.brandSection') }}
21
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
- />
22
+ <BrandSwitcher />
29
23
  </div>
30
24
 
31
25
  <!-- Typography section -->
@@ -119,7 +113,7 @@ defineOptions({
119
113
  :data-testid="generalStyleSettingsTestIds.resetButton"
120
114
  @click="emailEditorStore.resetGlobalSettings()"
121
115
  >
122
- {{ $t('editor.email.generalStyle.reset') }}
116
+ {{ $t('editor.email.common.reset') }}
123
117
  </UButton>
124
118
  </div>
125
119
  </template>
@@ -15,7 +15,7 @@ export interface ComposerContext {
15
15
  metaAccessToken?: Ref<string>
16
16
  }
17
17
 
18
- export const COMPOSER_CONTEXT_KEY: InjectionKey<ComposerContext> = Symbol('composer-context')
18
+ export const COMPOSER_CONTEXT_KEY: InjectionKey<ComposerContext> = Symbol.for('composer-context')
19
19
 
20
20
  /** Must be called in parent's app.vue or plugin before layer components render. */
21
21
  export function defineComposerContext(context: ComposerContext): void {
@@ -5,6 +5,7 @@ import {
5
5
  type PostEntity,
6
6
  type EmailMessage,
7
7
  type BrandFromDb,
8
+ type SocialPlatform,
8
9
  } from '@dev.smartpricing/message-composer-utils/types'
9
10
  import { resolveEmailSettings } from '@dev.smartpricing/message-composer-utils/utils'
10
11
  import { applySocialDefaults } from '~/utils/socialDefaults'
@@ -130,6 +131,59 @@ export const useEmailEditorStore = defineStore('emailEditor', () => {
130
131
  globalSettings.value = {}
131
132
  }
132
133
 
134
+ /**
135
+ * Clears all brand-cascadable overrides so the new brand's values take effect.
136
+ * Resets globalSettings and walks blocks clearing:
137
+ * - header: src (logo)
138
+ * - button: textColor, backgroundColor
139
+ * - heading/paragraph/footer/unsubscribe_link/discount: fontFamily
140
+ * - socials: URLs only for platforms where new brand provides a URL
141
+ * - grid: recurses into columns
142
+ */
143
+ function resetBlocksToBrand(blockList: EmailBlock[], newBrand: BrandFromDb): void {
144
+ for (const block of blockList) {
145
+ switch (block.type) {
146
+ case 'header':
147
+ block.settings.src = ''
148
+ break
149
+ case 'button':
150
+ block.settings.textColor = undefined
151
+ block.settings.backgroundColor = undefined
152
+ block.settings.fontFamily = undefined
153
+ break
154
+ case 'heading':
155
+ case 'paragraph':
156
+ case 'footer':
157
+ case 'unsubscribe_link':
158
+ case 'discount':
159
+ block.settings.fontFamily = undefined
160
+ break
161
+ case 'socials': {
162
+ const brandSocials = newBrand.default_socials
163
+ if (!brandSocials) break
164
+ for (const platform of Object.keys(block.settings.socials) as SocialPlatform[]) {
165
+ const social = block.settings.socials[platform]
166
+ if (social && brandSocials[platform]) {
167
+ social.url = ''
168
+ }
169
+ }
170
+ break
171
+ }
172
+ case 'grid':
173
+ for (const col of block.settings.columnData) {
174
+ resetBlocksToBrand(col.blocks, newBrand)
175
+ }
176
+ break
177
+ }
178
+ }
179
+ }
180
+
181
+ function resetToBrand(newBrand: BrandFromDb) {
182
+ resetGlobalSettings()
183
+ resetBlocksToBrand(blocks.value, newBrand)
184
+ brand.value = newBrand
185
+ }
186
+
133
187
  function resetEditor() {
134
188
  blocks.value = []
135
189
  selectedBlockId.value = null
@@ -204,6 +258,7 @@ export const useEmailEditorStore = defineStore('emailEditor', () => {
204
258
  // Actions - Settings
205
259
  updateGlobalSettings,
206
260
  resetGlobalSettings,
261
+ resetToBrand,
207
262
  setBrand,
208
263
  fetchBrands,
209
264
  initBrand,
@@ -223,6 +223,10 @@ export const generalStyleSettingsTestIds = {
223
223
  resetButton: 'general-style-settings-reset-button',
224
224
  } as const
225
225
 
226
+ export const brandSwitcherTestIds = {
227
+ select: 'brand-switcher-select',
228
+ } as const
229
+
226
230
  export const leftSidebarTestIds = {
227
231
  container: 'left-sidebar-container',
228
232
  tabs: 'left-sidebar-tabs',
@@ -156,6 +156,13 @@ Diese Nachrichten müssen funktional sein, nicht werblich.
156
156
  cancel: 'Abbrechen',
157
157
  change_to: 'Zu {0} ändern',
158
158
  },
159
+ email_change_brand: {
160
+ title: 'Marke ändern',
161
+ message:
162
+ 'Beim Ändern der Marke werden E-Mail-Farben, Schriftarten, Header-Logo und Button-Stile auf die Standardwerte der neuen Marke zurückgesetzt. Benutzerdefinierte Anpassungen gehen verloren.',
163
+ confirm: 'Zu {0} ändern',
164
+ cancel: 'Abbrechen',
165
+ },
159
166
  grid_reduce_columns: {
160
167
  title: 'Anzahl der Spalten bearbeiten',
161
168
  message: 'Einige Elemente können verloren gehen und nicht wiederhergestellt werden.',
@@ -153,6 +153,13 @@ These messages must be functional, not promotional.
153
153
  cancel: 'Cancel',
154
154
  change_to: 'Change to {0}',
155
155
  },
156
+ email_change_brand: {
157
+ title: 'Change brand',
158
+ message:
159
+ "Changing brand will reset email colors, fonts, header logo and button styles to the new brand's defaults. Custom overrides will be lost.",
160
+ confirm: 'Change to {0}',
161
+ cancel: 'Cancel',
162
+ },
156
163
  grid_reduce_columns: {
157
164
  title: 'Edit number of columns',
158
165
  message: 'Some elements may be lost and cannot be recovered.',
@@ -153,6 +153,13 @@ Estos mensajes deben ser funcionales, no promocionales.
153
153
  cancel: 'Cancelar',
154
154
  change_to: 'Cambiar a {0}',
155
155
  },
156
+ email_change_brand: {
157
+ title: 'Cambiar marca',
158
+ message:
159
+ 'Al cambiar la marca, los colores del correo, las fuentes, el logo del encabezado y los estilos de los botones se restablecerán a los valores predeterminados de la nueva marca. Las personalizaciones se perderán.',
160
+ confirm: 'Cambiar a {0}',
161
+ cancel: 'Cancelar',
162
+ },
156
163
  grid_reduce_columns: {
157
164
  title: 'Editar número de columnas',
158
165
  message: 'Algunos elementos pueden perderse y no se podrán recuperar.',
@@ -153,6 +153,13 @@ Ces messages doivent être fonctionnels, et non promotionnels.
153
153
  cancel: 'Annuler',
154
154
  change_to: 'Changer en {0}',
155
155
  },
156
+ email_change_brand: {
157
+ title: 'Changer de marque',
158
+ message:
159
+ "Le changement de marque réinitialisera les couleurs de l'e-mail, les polices, le logo de l'en-tête et les styles des boutons aux valeurs par défaut de la nouvelle marque. Les personnalisations seront perdues.",
160
+ confirm: 'Changer en {0}',
161
+ cancel: 'Annuler',
162
+ },
156
163
  grid_reduce_columns: {
157
164
  title: 'Modifier le nombre de colonnes',
158
165
  message: 'Certains éléments peuvent être perdus et ne pourront pas être récupérés.',
@@ -153,6 +153,13 @@ Questi messaggi devono essere funzionali, non promozionali.
153
153
  cancel: 'Annulla',
154
154
  change_to: 'Cambia in {0}',
155
155
  },
156
+ email_change_brand: {
157
+ title: 'Cambia brand',
158
+ message:
159
+ "Cambiando brand, i colori dell'email, i font, il logo dell'header e gli stili dei pulsanti verranno ripristinati ai valori predefiniti del nuovo brand. Le personalizzazioni verranno perse.",
160
+ confirm: 'Cambia in {0}',
161
+ cancel: 'Annulla',
162
+ },
156
163
  grid_reduce_columns: {
157
164
  title: 'Modifica numero di colonne',
158
165
  message: 'Alcuni elementi potrebbero andare persi e non possono essere recuperati.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.smartpricing/message-composer-layer",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -14,40 +14,40 @@
14
14
  "dependencies": {
15
15
  "@formkit/drag-and-drop": "^0.5.3",
16
16
  "@nuxtjs/i18n": "^10.3.0",
17
- "@pinia/colada": "^1.2.1",
18
- "@pinia/colada-nuxt": "^1.0.0",
17
+ "@pinia/colada": "^1.3.0",
18
+ "@pinia/colada-nuxt": "^1.0.1",
19
19
  "@pinia/nuxt": "^0.11.3",
20
- "@tiptap/extension-character-count": "^3.22.5",
21
- "@tiptap/extension-emoji": "^3.22.5",
22
- "@tiptap/extension-link": "^3.22.5",
23
- "@tiptap/extension-mention": "^3.22.5",
24
- "@tiptap/extension-text-align": "^3.22.5",
25
- "@tiptap/extension-underline": "^3.22.5",
26
- "@tiptap/extensions": "^3.22.5",
27
- "@tiptap/pm": "^3.22.5",
28
- "@tiptap/starter-kit": "^3.22.5",
29
- "@tiptap/vue-3": "^3.22.5",
30
- "@vueuse/core": "^14.2.1",
31
- "@vueuse/nuxt": "^14.2.1",
32
- "colorthief": "^2.4.0",
20
+ "@tiptap/extension-character-count": "^3.23.4",
21
+ "@tiptap/extension-emoji": "^3.23.4",
22
+ "@tiptap/extension-link": "^3.23.4",
23
+ "@tiptap/extension-mention": "^3.23.4",
24
+ "@tiptap/extension-text-align": "^3.23.4",
25
+ "@tiptap/extension-underline": "^3.23.4",
26
+ "@tiptap/extensions": "^3.23.4",
27
+ "@tiptap/pm": "^3.23.4",
28
+ "@tiptap/starter-kit": "^3.23.4",
29
+ "@tiptap/vue-3": "^3.23.4",
30
+ "@vueuse/core": "^14.3.0",
31
+ "@vueuse/nuxt": "^14.3.0",
32
+ "colorthief": "^3.3.1",
33
33
  "jwt-decode": "^4.0.0",
34
34
  "monaco-editor": "^0.55.1",
35
- "nuxt": "^4.4.2",
35
+ "nuxt": "^4.4.5",
36
36
  "ofetch": "^1.5.1",
37
37
  "typescript": "^6.0.3",
38
- "vue": "^3.5.33",
39
- "vue-router": "^5.0.6",
38
+ "vue": "^3.5.34",
39
+ "vue-router": "^5.0.7",
40
40
  "vue3-emoji-picker": "^1.1.8",
41
- "zod": "^4.4.1",
42
- "@dev.smartpricing/message-composer-utils": "3.1.25"
41
+ "zod": "^4.4.3",
42
+ "@dev.smartpricing/message-composer-utils": "3.1.26"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@nuxt/eslint": "^1.15.2",
46
46
  "@nuxt/test-utils": "^4.0.3",
47
- "@playwright/test": "^1.59.1",
47
+ "@playwright/test": "^1.60.0",
48
48
  "dotenv": "^17.4.2",
49
49
  "nuxt-ui-layer": "git+ssh://git@github.com:smartpricing/smartness-nuxt-ui#v1.6.6",
50
- "vue-tsc": "^3.2.8"
50
+ "vue-tsc": "^3.2.9"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "nuxt-ui-layer": ">=1.3.0"