@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,292 @@
1
+ <script lang="ts" setup>
2
+ import type {
3
+ GenerateTextRequest,
4
+ GenerateTextResponse,
5
+ GenerateTextWidgetType,
6
+ } from '@dev.smartpricing/message-composer-utils/types'
7
+ import {
8
+ GENERATE_TEXT_PROMPT_MIN_LENGTH,
9
+ GENERATE_TEXT_PROMPT_MAX_LENGTH,
10
+ } from '@dev.smartpricing/message-composer-utils/types'
11
+ import { useGenerateTextMutation } from '~/queries'
12
+ import { useAppContextStore } from '@/stores/appContext'
13
+
14
+ defineOptions({
15
+ name: 'AiGenerateSlideover',
16
+ })
17
+
18
+ const props = defineProps<{
19
+ widgetType: GenerateTextWidgetType
20
+ context?: string
21
+ }>()
22
+
23
+ const emit = defineEmits<{
24
+ select: [html: string]
25
+ }>()
26
+
27
+ const { t } = useI18n()
28
+ const appContext = useAppContextStore()
29
+
30
+ const isOpen = defineModel<boolean>('open', { default: false })
31
+
32
+ const prompt = ref('')
33
+ const suggestions = ref<string[]>([])
34
+ const step = ref<'prompt' | 'results'>('prompt')
35
+ const error = ref('')
36
+
37
+ const { mutateAsync: generateText, isLoading } = useGenerateTextMutation()
38
+
39
+ const charCount = computed(() => prompt.value.length)
40
+ const isPromptValid = computed(
41
+ () =>
42
+ charCount.value >= GENERATE_TEXT_PROMPT_MIN_LENGTH &&
43
+ charCount.value <= GENERATE_TEXT_PROMPT_MAX_LENGTH,
44
+ )
45
+
46
+ function resetState() {
47
+ prompt.value = ''
48
+ suggestions.value = []
49
+ step.value = 'prompt'
50
+ error.value = ''
51
+ }
52
+
53
+ watch(isOpen, (val) => {
54
+ if (!val) resetState()
55
+ })
56
+
57
+ async function handleGenerate() {
58
+ if (!isPromptValid.value) return
59
+
60
+ error.value = ''
61
+ suggestions.value = []
62
+ step.value = 'results'
63
+
64
+ const params: GenerateTextRequest = {
65
+ prompt: prompt.value,
66
+ widgetType: props.widgetType,
67
+ language: appContext.mainLanguage,
68
+ context: props.context,
69
+ }
70
+
71
+ try {
72
+ const data = (await generateText(params)) as GenerateTextResponse
73
+ suggestions.value = data.suggestions ?? []
74
+ } catch (err) {
75
+ console.error(err)
76
+ error.value = t('editor.generate.error')
77
+ }
78
+ }
79
+
80
+ function handleSelect(suggestion: string) {
81
+ emit('select', suggestion)
82
+ isOpen.value = false
83
+ }
84
+
85
+ function handleCreateNew() {
86
+ suggestions.value = []
87
+ error.value = ''
88
+ step.value = 'prompt'
89
+ }
90
+
91
+ const widgetLabel = computed(() => {
92
+ const labelKeys: Record<GenerateTextWidgetType, string> = {
93
+ heading: 'editor.email.blocks.heading.name',
94
+ paragraph: 'editor.email.blocks.paragraph.name',
95
+ whatsapp_body: 'editor.whatsapp.body.title',
96
+ }
97
+ return t(labelKeys[props.widgetType] ?? props.widgetType)
98
+ })
99
+ </script>
100
+
101
+ <template>
102
+ <USlideover
103
+ v-model:open="isOpen"
104
+ :title="widgetLabel"
105
+ close-icon="ph:x"
106
+ side="right"
107
+ :data-testid="aiGenerateSlideoverTestIds.slideover"
108
+ :close="{ 'data-testid': aiGenerateSlideoverTestIds.closeButton }"
109
+ :ui="{ content: 'max-w-5xl', body: 'sm:p-0 p-0', footer: 'justify-end' }"
110
+ >
111
+ <slot>
112
+ <UButton
113
+ :data-testid="aiGenerateSlideoverTestIds.openButton"
114
+ size="sm"
115
+ variant="ghost"
116
+ :label="$t('editor.actions.generate_text')"
117
+ />
118
+ </slot>
119
+ <template #body>
120
+ <div class="flex flex-col gap-4 p-4">
121
+ <h3 class="text-lg font-semibold" :data-testid="aiGenerateSlideoverTestIds.title">
122
+ {{ t('editor.generate.title') }}
123
+ </h3>
124
+
125
+ <!-- Step 1: Prompt -->
126
+ <template v-if="step === 'prompt'">
127
+ <p class="text-sm text-muted" :data-testid="aiGenerateSlideoverTestIds.description">
128
+ {{ t('editor.generate.description') }}
129
+ </p>
130
+
131
+ <UTextarea
132
+ v-model="prompt"
133
+ :placeholder="t('editor.generate.placeholder')"
134
+ :rows="6"
135
+ autoresize
136
+ :maxrows="10"
137
+ :data-testid="aiGenerateSlideoverTestIds.promptTextarea"
138
+ />
139
+
140
+ <div class="flex items-center justify-between">
141
+ <p class="text-xs text-muted" :data-testid="aiGenerateSlideoverTestIds.disclaimer">
142
+ {{ t('editor.generate.disclaimer') }}
143
+ </p>
144
+ <span
145
+ class="text-xs text-muted tabular-nums"
146
+ :data-testid="aiGenerateSlideoverTestIds.charCount"
147
+ >
148
+ {{ charCount }}/{{ GENERATE_TEXT_PROMPT_MAX_LENGTH }}
149
+ </span>
150
+ </div>
151
+
152
+ <!-- Error message -->
153
+ <UAlert
154
+ v-if="error"
155
+ color="error"
156
+ variant="subtle"
157
+ :title="error"
158
+ icon="i-lucide-alert-circle"
159
+ :data-testid="aiGenerateSlideoverTestIds.errorAlert"
160
+ >
161
+ <template #actions>
162
+ <UButton
163
+ variant="link"
164
+ color="error"
165
+ :label="t('editor.generate.retry')"
166
+ :data-testid="aiGenerateSlideoverTestIds.retryButton"
167
+ @click="handleGenerate"
168
+ />
169
+ </template>
170
+ </UAlert>
171
+
172
+ <div class="flex justify-end gap-2 pt-2">
173
+ <UButton
174
+ variant="outline"
175
+ color="neutral"
176
+ :label="t('editor.generate.cancel')"
177
+ :data-testid="aiGenerateSlideoverTestIds.cancelButton"
178
+ @click="isOpen = false"
179
+ />
180
+ <UButton
181
+ :label="t('editor.generate.submit')"
182
+ :disabled="!isPromptValid || isLoading"
183
+ :loading="isLoading"
184
+ :data-testid="aiGenerateSlideoverTestIds.submitButton"
185
+ @click="handleGenerate"
186
+ />
187
+ </div>
188
+ </template>
189
+
190
+ <!-- Step 2: Results -->
191
+ <template v-if="step === 'results'">
192
+ <!-- User's request -->
193
+ <div :data-testid="aiGenerateSlideoverTestIds.yourRequest">
194
+ <p class="text-sm font-medium">
195
+ {{ t('editor.generate.your_request') }}
196
+ </p>
197
+ <p class="text-sm text-muted mt-1">
198
+ {{ prompt }}
199
+ </p>
200
+ </div>
201
+
202
+ <!-- Loading skeletons -->
203
+ <template v-if="isLoading">
204
+ <p
205
+ class="text-sm font-medium"
206
+ :data-testid="aiGenerateSlideoverTestIds.chooseOptionLabel"
207
+ >
208
+ {{ t('editor.generate.choose_option') }}
209
+ </p>
210
+ <div
211
+ v-for="i in 3"
212
+ :key="i"
213
+ class="rounded-lg border border-accented p-4"
214
+ :data-testid="aiGenerateSlideoverTestIds.skeletonCard"
215
+ >
216
+ <USkeleton class="h-4 w-full mb-2" />
217
+ <USkeleton class="h-4 w-3/4 mb-2" />
218
+ <USkeleton class="h-4 w-1/2" />
219
+ </div>
220
+ </template>
221
+
222
+ <!-- Suggestions -->
223
+ <template v-else-if="suggestions.length">
224
+ <p
225
+ class="text-sm font-medium"
226
+ :data-testid="aiGenerateSlideoverTestIds.chooseOptionLabel"
227
+ >
228
+ {{ t('editor.generate.choose_option') }}
229
+ </p>
230
+
231
+ <div
232
+ v-for="(suggestion, index) in suggestions"
233
+ :key="index"
234
+ class="rounded-lg border border-accented p-4 space-y-3"
235
+ :data-testid="aiGenerateSlideoverTestIds.suggestionCard"
236
+ >
237
+ <div
238
+ class="prose prose-sm max-w-none"
239
+ :data-testid="aiGenerateSlideoverTestIds.suggestionContent"
240
+ v-html="suggestion"
241
+ />
242
+ <div class="flex justify-end">
243
+ <UButton
244
+ :label="t('editor.generate.use_it')"
245
+ size="sm"
246
+ :data-testid="aiGenerateSlideoverTestIds.useItButton"
247
+ @click="handleSelect(suggestion)"
248
+ />
249
+ </div>
250
+ </div>
251
+ </template>
252
+
253
+ <!-- Error in results step -->
254
+ <UAlert
255
+ v-if="error"
256
+ color="error"
257
+ variant="subtle"
258
+ :title="error"
259
+ icon="i-lucide-alert-circle"
260
+ :data-testid="aiGenerateSlideoverTestIds.errorAlert"
261
+ >
262
+ <template #actions>
263
+ <UButton
264
+ variant="link"
265
+ color="error"
266
+ :label="t('editor.generate.retry')"
267
+ :data-testid="aiGenerateSlideoverTestIds.retryButton"
268
+ @click="handleGenerate"
269
+ />
270
+ </template>
271
+ </UAlert>
272
+
273
+ <div class="flex justify-end gap-2 pt-2">
274
+ <UButton
275
+ variant="outline"
276
+ color="neutral"
277
+ :label="t('editor.generate.cancel')"
278
+ :data-testid="aiGenerateSlideoverTestIds.cancelButton"
279
+ @click="isOpen = false"
280
+ />
281
+ <UButton
282
+ variant="outline"
283
+ :label="t('editor.generate.create_new')"
284
+ :data-testid="aiGenerateSlideoverTestIds.createNewButton"
285
+ @click="handleCreateNew"
286
+ />
287
+ </div>
288
+ </template>
289
+ </div>
290
+ </template>
291
+ </USlideover>
292
+ </template>
@@ -0,0 +1,154 @@
1
+ <script setup lang="ts">
2
+ import type { Editor } from '@tiptap/vue-3'
3
+
4
+ const props = defineProps<{
5
+ editor: Editor
6
+ }>()
7
+
8
+ const { t } = useI18n()
9
+ const open = ref(false)
10
+ const url = ref('')
11
+
12
+ const active = computed(() => props.editor.isActive('link'))
13
+ const disabled = computed(() => {
14
+ if (!props.editor.isEditable) return true
15
+ const { selection } = props.editor.state
16
+ return selection.empty && !props.editor.isActive('link')
17
+ })
18
+
19
+ watch(
20
+ () => props.editor,
21
+ (editor, _, onCleanup) => {
22
+ if (!editor) return
23
+
24
+ const updateUrl = () => {
25
+ const { href } = editor.getAttributes('link')
26
+ url.value = href || ''
27
+ }
28
+
29
+ updateUrl()
30
+ editor.on('selectionUpdate', updateUrl)
31
+
32
+ onCleanup(() => {
33
+ editor.off('selectionUpdate', updateUrl)
34
+ })
35
+ },
36
+ { immediate: true },
37
+ )
38
+
39
+ function setLink() {
40
+ if (!url.value) return
41
+
42
+ const { selection } = props.editor.state
43
+ const isEmpty = selection.empty
44
+
45
+ let chain = props.editor.chain().focus()
46
+ chain = chain.extendMarkRange('link').setLink({ href: url.value })
47
+
48
+ if (isEmpty) {
49
+ chain = chain.insertContent({ type: 'text', text: url.value })
50
+ }
51
+
52
+ chain.run()
53
+ open.value = false
54
+ }
55
+
56
+ function removeLink() {
57
+ props.editor
58
+ .chain()
59
+ .focus()
60
+ .extendMarkRange('link')
61
+ .unsetLink()
62
+ .setMeta('preventAutolink', true)
63
+ .run()
64
+
65
+ url.value = ''
66
+ open.value = false
67
+ }
68
+
69
+ function openLink() {
70
+ if (!url.value) return
71
+ window.open(url.value, '_blank', 'noopener,noreferrer')
72
+ }
73
+
74
+ function handleKeyDown(event: KeyboardEvent) {
75
+ if (event.key === 'Enter') {
76
+ event.preventDefault()
77
+ setLink()
78
+ }
79
+ }
80
+ </script>
81
+
82
+ <template>
83
+ <UPopover
84
+ v-model:open="open"
85
+ :ui="{ content: 'p-0.5' }"
86
+ :data-testid="editorLinkPopoverTestIds.popover"
87
+ >
88
+ <UTooltip :text="t('editor.toolbar.link')">
89
+ <UButton
90
+ icon="ph:link"
91
+ color="neutral"
92
+ active-color="primary"
93
+ variant="ghost"
94
+ active-variant="soft"
95
+ size="sm"
96
+ :active="active"
97
+ :disabled="disabled"
98
+ :data-testid="editorLinkPopoverTestIds.triggerButton"
99
+ />
100
+ </UTooltip>
101
+
102
+ <template #content>
103
+ <UInput
104
+ v-model="url"
105
+ autofocus
106
+ name="url"
107
+ type="url"
108
+ variant="none"
109
+ :placeholder="t('editor.toolbar.link_placeholder')"
110
+ class="w-64"
111
+ :data-testid="editorLinkPopoverTestIds.urlInput"
112
+ @keydown="handleKeyDown"
113
+ >
114
+ <template #trailing>
115
+ <div class="flex items-center">
116
+ <UButton
117
+ icon="ph:arrow-elbow-down-left"
118
+ variant="ghost"
119
+ size="xs"
120
+ :disabled="!url && !active"
121
+ :title="t('editor.toolbar.link_apply')"
122
+ :data-testid="editorLinkPopoverTestIds.applyButton"
123
+ @click="setLink"
124
+ />
125
+
126
+ <USeparator orientation="vertical" class="h-4 mx-0.5" />
127
+
128
+ <UButton
129
+ icon="ph:arrow-square-out"
130
+ color="neutral"
131
+ variant="ghost"
132
+ size="xs"
133
+ :disabled="!url && !active"
134
+ :title="t('editor.toolbar.link_open')"
135
+ :data-testid="editorLinkPopoverTestIds.openButton"
136
+ @click="openLink"
137
+ />
138
+
139
+ <UButton
140
+ icon="ph:trash"
141
+ color="neutral"
142
+ variant="ghost"
143
+ size="xs"
144
+ :disabled="!url && !active"
145
+ :title="t('editor.toolbar.link_remove')"
146
+ :data-testid="editorLinkPopoverTestIds.removeButton"
147
+ @click="removeLink"
148
+ />
149
+ </div>
150
+ </template>
151
+ </UInput>
152
+ </template>
153
+ </UPopover>
154
+ </template>