@antelopejs/dms-mailing 0.6.1 → 0.7.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 (236) hide show
  1. package/README.md +8 -7
  2. package/dist/constants.d.ts +1 -2
  3. package/dist/constants.js +2 -3
  4. package/dist/constants.js.map +1 -1
  5. package/dist/data/templates-table.d.ts +4 -1
  6. package/dist/data/templates-table.js +55 -20
  7. package/dist/data/templates-table.js.map +1 -1
  8. package/dist/data-types/categories-type.d.ts +5 -5
  9. package/dist/db/models/index.d.ts +0 -1
  10. package/dist/db/models/index.js +0 -1
  11. package/dist/db/models/index.js.map +1 -1
  12. package/dist/db/models/send-events.model.js +2 -3
  13. package/dist/db/models/send-events.model.js.map +1 -1
  14. package/dist/db/models/sends.model.js +2 -3
  15. package/dist/db/models/sends.model.js.map +1 -1
  16. package/dist/db/models/templates.model.d.ts +10 -1
  17. package/dist/db/models/templates.model.js +29 -10
  18. package/dist/db/models/templates.model.js.map +1 -1
  19. package/dist/db/tables/index.d.ts +0 -1
  20. package/dist/db/tables/index.js +0 -1
  21. package/dist/db/tables/index.js.map +1 -1
  22. package/dist/db/tables/settings.table.d.ts +0 -1
  23. package/dist/db/tables/settings.table.js +0 -4
  24. package/dist/db/tables/settings.table.js.map +1 -1
  25. package/dist/db/tables/templates.table.d.ts +7 -4
  26. package/dist/db/tables/templates.table.js +4 -14
  27. package/dist/db/tables/templates.table.js.map +1 -1
  28. package/dist/engine/formats.d.ts +13 -0
  29. package/dist/engine/formats.js +85 -0
  30. package/dist/engine/formats.js.map +1 -0
  31. package/dist/engine/index.d.ts +1 -0
  32. package/dist/engine/index.js +1 -0
  33. package/dist/engine/index.js.map +1 -1
  34. package/dist/engine/resolve.d.ts +2 -1
  35. package/dist/engine/resolve.js +14 -7
  36. package/dist/engine/resolve.js.map +1 -1
  37. package/dist/engine/tokens.d.ts +2 -1
  38. package/dist/engine/tokens.js +3 -5
  39. package/dist/engine/tokens.js.map +1 -1
  40. package/dist/frontend-module.d.ts +2 -0
  41. package/dist/frontend-module.js +24 -0
  42. package/dist/frontend-module.js.map +1 -0
  43. package/dist/hooks/index.d.ts +0 -1
  44. package/dist/hooks/index.js +0 -1
  45. package/dist/hooks/index.js.map +1 -1
  46. package/dist/hooks/tenant-export.js +4 -1
  47. package/dist/hooks/tenant-export.js.map +1 -1
  48. package/dist/implementations/dms-mailing/index.d.ts +3 -3
  49. package/dist/implementations/dms-mailing/index.js +8 -7
  50. package/dist/implementations/dms-mailing/index.js.map +1 -1
  51. package/dist/index.js +2 -20
  52. package/dist/index.js.map +1 -1
  53. package/dist/pages/overview.d.ts +1 -1
  54. package/dist/pages/overview.js +11 -13
  55. package/dist/pages/overview.js.map +1 -1
  56. package/dist/pages/sends.d.ts +1 -1
  57. package/dist/pages/sends.js +16 -8
  58. package/dist/pages/sends.js.map +1 -1
  59. package/dist/pages/settings/form.js +8 -0
  60. package/dist/pages/settings/form.js.map +1 -1
  61. package/dist/pages/templates.js +3 -8
  62. package/dist/pages/templates.js.map +1 -1
  63. package/dist/pages/toolbar.d.ts +12 -0
  64. package/dist/pages/toolbar.js +41 -0
  65. package/dist/pages/toolbar.js.map +1 -0
  66. package/dist/realtime.d.ts +2 -2
  67. package/dist/realtime.js +8 -8
  68. package/dist/realtime.js.map +1 -1
  69. package/dist/routes/metrics.d.ts +9 -2
  70. package/dist/routes/metrics.js +90 -35
  71. package/dist/routes/metrics.js.map +1 -1
  72. package/dist/routes/period.d.ts +1 -0
  73. package/dist/routes/period.js +19 -0
  74. package/dist/routes/period.js.map +1 -1
  75. package/dist/routes/sends.js +6 -3
  76. package/dist/routes/sends.js.map +1 -1
  77. package/dist/routes/settings.js +6 -7
  78. package/dist/routes/settings.js.map +1 -1
  79. package/dist/routes/templates.d.ts +8 -4
  80. package/dist/routes/templates.js +41 -20
  81. package/dist/routes/templates.js.map +1 -1
  82. package/dist/services/categories.d.ts +2 -1
  83. package/dist/services/categories.js +9 -0
  84. package/dist/services/categories.js.map +1 -1
  85. package/dist/services/declarations.js +42 -184
  86. package/dist/services/declarations.js.map +1 -1
  87. package/dist/services/events.js +1 -1
  88. package/dist/services/events.js.map +1 -1
  89. package/dist/services/localized-text.d.ts +7 -0
  90. package/dist/services/localized-text.js +28 -0
  91. package/dist/services/localized-text.js.map +1 -0
  92. package/dist/services/send-import.js +0 -2
  93. package/dist/services/send-import.js.map +1 -1
  94. package/dist/services/send-stat-items.js +34 -12
  95. package/dist/services/send-stat-items.js.map +1 -1
  96. package/dist/services/send-stats.d.ts +7 -0
  97. package/dist/services/send-stats.js +17 -5
  98. package/dist/services/send-stats.js.map +1 -1
  99. package/dist/services/send.d.ts +2 -0
  100. package/dist/services/send.js +11 -12
  101. package/dist/services/send.js.map +1 -1
  102. package/dist/services/sender-declaration.d.ts +2 -0
  103. package/dist/services/sender-declaration.js +18 -0
  104. package/dist/services/sender-declaration.js.map +1 -0
  105. package/dist/services/settings.d.ts +1 -1
  106. package/dist/services/settings.js +1 -5
  107. package/dist/services/settings.js.map +1 -1
  108. package/dist/services/starters.d.ts +2 -2
  109. package/dist/services/starters.js +1 -10
  110. package/dist/services/starters.js.map +1 -1
  111. package/dist/services/templates.d.ts +13 -7
  112. package/dist/services/templates.js +72 -33
  113. package/dist/services/templates.js.map +1 -1
  114. package/dist/services/webhook-status.d.ts +2 -1
  115. package/dist/services/webhook-status.js +6 -5
  116. package/dist/services/webhook-status.js.map +1 -1
  117. package/dist/types/settings.d.ts +8 -0
  118. package/dist/types/settings.js +33 -6
  119. package/dist/types/settings.js.map +1 -1
  120. package/dist/validation/declarations.schema.d.ts +25 -7
  121. package/dist/validation/declarations.schema.js +16 -3
  122. package/dist/validation/declarations.schema.js.map +1 -1
  123. package/dist/validation/settings.schema.d.ts +41 -41
  124. package/dist/validation/settings.schema.js +30 -6
  125. package/dist/validation/settings.schema.js.map +1 -1
  126. package/dist/validation/templates.schema.d.ts +13 -0
  127. package/dist/validation/templates.schema.js +6 -1
  128. package/dist/validation/templates.schema.js.map +1 -1
  129. package/frontend-vue/app/components/CategoriesInput.vue +15 -13
  130. package/frontend-vue/app/components/FunnelCard.vue +46 -3
  131. package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
  132. package/frontend-vue/app/components/OverviewGate.vue +0 -8
  133. package/frontend-vue/app/components/ProviderPill.vue +50 -0
  134. package/frontend-vue/app/components/SendDrawer.vue +206 -183
  135. package/frontend-vue/app/components/StarterGallery.vue +1 -1
  136. package/frontend-vue/app/components/TemplateCard.vue +4 -1
  137. package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
  138. package/frontend-vue/app/components/TemplateGallery.vue +57 -17
  139. package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
  140. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
  141. package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
  142. package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
  143. package/frontend-vue/app/composables/useMailingApi.ts +3 -3
  144. package/frontend-vue/app/composables/useMailingReads.ts +2 -2
  145. package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
  146. package/frontend-vue/app/types/mailing.ts +18 -0
  147. package/frontend-vue/app/utils/categories.ts +58 -49
  148. package/frontend-vue/app/utils/funnel.ts +25 -0
  149. package/frontend-vue/app/utils/period.ts +0 -7
  150. package/frontend-vue/app/utils/starters.ts +3 -1
  151. package/frontend-vue/app/utils/tokens.ts +6 -1
  152. package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
  153. package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
  154. package/frontend-vue/package.json +1 -1
  155. package/frontend-vue/tests/categories-input.test.ts +4 -10
  156. package/frontend-vue/tests/categories.test.ts +55 -17
  157. package/frontend-vue/tests/conditions.test.ts +1 -4
  158. package/frontend-vue/tests/funnel-card.test.ts +27 -0
  159. package/frontend-vue/tests/funnel.test.ts +25 -0
  160. package/frontend-vue/tests/localized-defaults.test.ts +0 -29
  161. package/frontend-vue/tests/new-template-modal.test.ts +1 -1
  162. package/frontend-vue/tests/overview-gate.test.ts +1 -24
  163. package/frontend-vue/tests/page-reads.test.ts +9 -25
  164. package/frontend-vue/tests/period.test.ts +0 -12
  165. package/frontend-vue/tests/replay-toast.test.ts +2 -1
  166. package/frontend-vue/tests/send-drawer.test.ts +123 -0
  167. package/frontend-vue/tests/template-drawer.test.ts +1 -1
  168. package/frontend-vue/tests/template-gallery.test.ts +90 -3
  169. package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
  170. package/package.json +8 -7
  171. package/skills/dms-mailing/SKILL.md +25 -18
  172. package/src/constants.ts +2 -2
  173. package/src/data/sends-table.ts +1 -1
  174. package/src/data/templates-table.ts +125 -33
  175. package/src/db/models/index.ts +0 -1
  176. package/src/db/models/send-events.model.ts +2 -2
  177. package/src/db/models/sends.model.ts +7 -8
  178. package/src/db/models/templates.model.ts +64 -16
  179. package/src/db/tables/index.ts +0 -1
  180. package/src/db/tables/settings.table.ts +0 -8
  181. package/src/db/tables/templates.table.ts +12 -17
  182. package/src/engine/formats.ts +124 -0
  183. package/src/engine/index.ts +1 -0
  184. package/src/engine/resolve.ts +18 -7
  185. package/src/engine/tokens.ts +7 -5
  186. package/src/frontend-module.ts +36 -0
  187. package/src/hooks/index.ts +0 -1
  188. package/src/hooks/tenant-export.ts +8 -1
  189. package/src/implementations/dms-mailing/index.ts +5 -16
  190. package/src/index.ts +3 -29
  191. package/src/pages/overview.ts +20 -16
  192. package/src/pages/sends.ts +27 -11
  193. package/src/pages/settings/form.ts +16 -0
  194. package/src/pages/templates.ts +4 -8
  195. package/src/pages/toolbar.ts +77 -0
  196. package/src/realtime.ts +17 -8
  197. package/src/routes/metrics.ts +158 -45
  198. package/src/routes/period.ts +32 -0
  199. package/src/routes/sends.ts +13 -4
  200. package/src/routes/settings.ts +11 -8
  201. package/src/routes/templates.ts +81 -37
  202. package/src/services/categories.ts +14 -1
  203. package/src/services/declarations.ts +64 -317
  204. package/src/services/events.ts +1 -1
  205. package/src/services/localized-text.ts +53 -0
  206. package/src/services/send-import.ts +2 -2
  207. package/src/services/send-stat-items.ts +60 -13
  208. package/src/services/send-stats.ts +37 -5
  209. package/src/services/send.ts +19 -20
  210. package/src/services/sender-declaration.ts +27 -0
  211. package/src/services/settings.ts +3 -10
  212. package/src/services/starters.ts +3 -15
  213. package/src/services/templates.ts +109 -63
  214. package/src/services/webhook-status.ts +3 -3
  215. package/src/types/settings.ts +55 -5
  216. package/src/validation/declarations.schema.ts +29 -3
  217. package/src/validation/settings.schema.ts +56 -14
  218. package/src/validation/templates.schema.ts +10 -0
  219. package/dist/db/models/template-slugs.model.d.ts +0 -27
  220. package/dist/db/models/template-slugs.model.js +0 -43
  221. package/dist/db/models/template-slugs.model.js.map +0 -1
  222. package/dist/db/tables/template-slugs.table.d.ts +0 -7
  223. package/dist/db/tables/template-slugs.table.js +0 -35
  224. package/dist/db/tables/template-slugs.table.js.map +0 -1
  225. package/dist/hooks/database-ready.d.ts +0 -7
  226. package/dist/hooks/database-ready.js +0 -26
  227. package/dist/hooks/database-ready.js.map +0 -1
  228. package/dist/services/fingerprint.d.ts +0 -1
  229. package/dist/services/fingerprint.js +0 -20
  230. package/dist/services/fingerprint.js.map +0 -1
  231. package/frontend-vue/app/components/PageHeader.vue +0 -94
  232. package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
  233. package/src/db/models/template-slugs.model.ts +0 -61
  234. package/src/db/tables/template-slugs.table.ts +0 -29
  235. package/src/hooks/database-ready.ts +0 -47
  236. package/src/services/fingerprint.ts +0 -22
@@ -412,208 +412,231 @@ onMounted(() => {
412
412
 
413
413
  <template>
414
414
  <!--
415
- As tall as the side panel's body: the send scrolls inside, the actions
416
- stay under it at the bottom, with nothing passing beneath them.
415
+ The drawer's body is the scroll container and sets the inset: the send
416
+ adds no padding of its own. It grows in the body's flex column, so the
417
+ footer sits at the bottom of the drawer, and sticks there once the send
418
+ scrolls.
417
419
  -->
418
- <div class="flex h-full min-h-0 flex-col">
419
- <USkeleton v-if="loading && !send" class="m-5 h-64" />
420
+ <div class="@container flex flex-1 flex-col gap-6" data-send-drawer>
421
+ <USkeleton v-if="loading && !send" class="h-64" />
420
422
 
421
423
  <template v-else-if="send">
422
- <div class="min-h-0 flex-1 overflow-y-auto">
423
- <header
424
- class="border-default flex flex-col gap-1.5 border-b px-5 pb-4 pt-1"
424
+ <header class="border-default flex flex-col gap-1.5 border-b pb-4">
425
+ <div class="flex items-center gap-1">
426
+ <DmsEyebrow
427
+ class="min-w-0 flex-1"
428
+ truncate
429
+ :label="key('eyebrow', { id: shortSendId(send._id) })"
430
+ />
431
+ <UButton
432
+ icon="i-ph-link"
433
+ color="neutral"
434
+ variant="ghost"
435
+ size="sm"
436
+ :aria-label="key('copy_link')"
437
+ @click="copyLink"
438
+ />
439
+ </div>
440
+ <h3
441
+ class="text-highlighted flex flex-wrap items-center gap-2 text-lg font-semibold"
425
442
  >
426
- <div class="flex items-center gap-1">
427
- <DmsEyebrow
428
- class="min-w-0 flex-1"
429
- truncate
430
- :label="key('eyebrow', { id: shortSendId(send._id) })"
431
- />
443
+ <span class="min-w-0 truncate">
444
+ {{ send.recipientName || send.recipientEmail }}
445
+ </span>
446
+ <DmsStatusPill
447
+ :tone="SEND_STATUS_TONES[send.status]"
448
+ :label="t(`dms_mailing.sends.status.${send.status}`)"
449
+ />
450
+ <DmsStatusPill
451
+ v-if="send.isTest"
452
+ tone="warning"
453
+ dot="none"
454
+ size="sm"
455
+ uppercase
456
+ :label="key('test')"
457
+ />
458
+ </h3>
459
+ <!-- The address gives way first: the date drops to its own line
460
+ rather than being cut off with it. -->
461
+ <p
462
+ class="text-muted flex flex-wrap gap-x-1.5 text-[12.5px]"
463
+ data-send-recipient
464
+ >
465
+ <span class="min-w-0 truncate font-mono" :title="send.recipientEmail">
466
+ {{ send.recipientEmail }}
467
+ </span>
468
+ <span class="whitespace-nowrap">
469
+ · {{ dateOf(send.createdAt, DATE_FORMAT) }}
470
+ </span>
471
+ </p>
472
+ </header>
473
+
474
+ <section class="flex flex-col gap-3">
475
+ <!-- Three short values: side by side from a 28rem group (a 600px
476
+ drawer) rather than the DmsStatGroup's own 36rem (dms#399). -->
477
+ <DmsStatGroup
478
+ layout="joined"
479
+ :columns="3"
480
+ columns-from="md"
481
+ :items="stats"
482
+ />
483
+ <DmsBanner
484
+ v-if="problem"
485
+ :tone="isResolved ? 'info' : problemTone(problem.kind)"
486
+ :size="isResolved ? 'sm' : undefined"
487
+ :icon="problem.icon"
488
+ :title="t(problem.titleKey, problem.params)"
489
+ :description="
490
+ isResolved ? undefined : t(problem.descriptionKey, problem.params)
491
+ "
492
+ data-problem-banner
493
+ />
494
+ <DmsBanner
495
+ v-if="suppression"
496
+ :tone="suppression.tone"
497
+ size="sm"
498
+ :icon="suppression.icon"
499
+ :title="t(suppression.titleKey, suppression.params)"
500
+ >
501
+ <template #description>
502
+ <span
503
+ class="text-highlighted block truncate font-mono text-[12px]"
504
+ :title="suppression.params.email"
505
+ data-suppressed-address
506
+ >
507
+ {{ suppression.params.email }}
508
+ </span>
509
+ {{ t(suppression.descriptionKey, suppression.params) }}
510
+ </template>
511
+ <template v-if="suppression.hasListLink" #actions>
432
512
  <UButton
433
- icon="i-ph-link"
513
+ size="xs"
434
514
  color="neutral"
435
- variant="ghost"
436
- size="sm"
437
- :aria-label="key('copy_link')"
438
- @click="copyLink"
439
- />
440
- </div>
441
- <h3
442
- class="text-highlighted flex flex-wrap items-center gap-2 text-lg font-semibold"
443
- >
444
- <span class="min-w-0 truncate">
445
- {{ send.recipientName || send.recipientEmail }}
446
- </span>
447
- <DmsStatusPill
448
- :tone="SEND_STATUS_TONES[send.status]"
449
- :label="t(`dms_mailing.sends.status.${send.status}`)"
515
+ variant="outline"
516
+ trailing-icon="i-ph-arrow-right"
517
+ :label="t('dms_mailing.suppressions.notice.open_list')"
518
+ @click="openSuppressionList"
450
519
  />
451
- <DmsStatusPill
452
- v-if="send.isTest"
453
- tone="warning"
454
- dot="none"
455
- size="sm"
456
- uppercase
457
- :label="key('test')"
458
- />
459
- </h3>
460
- <p class="text-muted truncate text-[12.5px]">
461
- <span class="font-mono">{{ send.recipientEmail }}</span>
462
- · {{ dateOf(send.createdAt, DATE_FORMAT) }}
463
- </p>
464
- </header>
465
-
466
- <div class="flex flex-1 flex-col gap-6 p-5">
467
- <section class="flex flex-col gap-3">
468
- <DmsStatGroup layout="joined" :columns="3" :items="stats" />
469
- <DmsBanner
470
- v-if="problem"
471
- :tone="isResolved ? 'info' : problemTone(problem.kind)"
472
- :size="isResolved ? 'sm' : undefined"
473
- :icon="problem.icon"
474
- :title="t(problem.titleKey, problem.params)"
475
- :description="
476
- isResolved
477
- ? undefined
478
- : t(problem.descriptionKey, problem.params)
479
- "
480
- data-problem-banner
520
+ </template>
521
+ </DmsBanner>
522
+ <DmsBanner
523
+ v-for="link in linkedSends"
524
+ :key="link.id"
525
+ tone="info"
526
+ size="sm"
527
+ icon="i-ph-arrow-clockwise"
528
+ :title="link.text"
529
+ >
530
+ <template #actions>
531
+ <UButton
532
+ v-if="showRow"
533
+ size="xs"
534
+ color="neutral"
535
+ variant="outline"
536
+ trailing-icon="i-ph-arrow-right"
537
+ :label="key('see_send')"
538
+ @click="void showSend(link.id)"
481
539
  />
482
- <DmsBanner
483
- v-if="suppression"
484
- :tone="suppression.tone"
485
- size="sm"
486
- :icon="suppression.icon"
487
- :title="t(suppression.titleKey, suppression.params)"
540
+ </template>
541
+ </DmsBanner>
542
+ </section>
543
+
544
+ <!--
545
+ Side by side once the drawer is resized wide:
546
+ both are short lists, and the data sent below keeps the full width.
547
+ -->
548
+ <div class="@2xl:grid-cols-2 grid gap-6" data-send-details-grid>
549
+ <section class="flex min-w-0 flex-col gap-2">
550
+ <DmsEyebrow :label="key('timeline')" />
551
+ <ol class="flex flex-col">
552
+ <li
553
+ v-for="item in timeline"
554
+ :key="item.id"
555
+ :class="item.isPending && 'opacity-50'"
488
556
  >
489
- <template #description>
557
+ <DmsActivityItem
558
+ :icon="item.icon"
559
+ :icon-color="item.isPending ? 'neutral' : item.tone"
560
+ :subtitle="
561
+ item.isPending ? key('pending') : item.detail || undefined
562
+ "
563
+ :mono="item.isDetailMono && !item.isPending"
564
+ :trailing="stepTime(item)"
565
+ :interactive="false"
566
+ wrap
567
+ >
490
568
  <span
491
- class="text-highlighted block truncate font-mono text-[12px]"
492
- :title="suppression.params.email"
493
- data-suppressed-address
569
+ :class="
570
+ item.tone === 'error' && !item.isPending
571
+ ? 'text-error'
572
+ : undefined
573
+ "
494
574
  >
495
- {{ suppression.params.email }}
575
+ {{ stepTitle(item) }}
496
576
  </span>
497
- {{ t(suppression.descriptionKey, suppression.params) }}
498
- </template>
499
- <template v-if="suppression.hasListLink" #actions>
500
- <UButton
501
- size="xs"
502
- color="neutral"
503
- variant="outline"
504
- trailing-icon="i-ph-arrow-right"
505
- :label="t('dms_mailing.suppressions.notice.open_list')"
506
- @click="openSuppressionList"
507
- />
508
- </template>
509
- </DmsBanner>
510
- <DmsBanner
511
- v-for="link in linkedSends"
512
- :key="link.id"
513
- tone="info"
577
+ </DmsActivityItem>
578
+ </li>
579
+ </ol>
580
+ </section>
581
+
582
+ <section class="flex min-w-0 flex-col gap-2">
583
+ <DmsEyebrow :label="key('origin')" />
584
+ <div
585
+ class="border-default divide-default divide-y overflow-hidden rounded-lg border"
586
+ >
587
+ <DmsListRow
588
+ :to="editorLink"
589
+ icon="i-ph-envelope-simple"
590
+ icon-size="2xs"
514
591
  size="sm"
515
- icon="i-ph-arrow-clockwise"
516
- :title="link.text"
592
+ :title="originTitle"
517
593
  >
518
- <template #actions>
519
- <UButton
520
- v-if="showRow"
521
- size="xs"
522
- color="neutral"
523
- variant="outline"
524
- trailing-icon="i-ph-arrow-right"
525
- :label="key('see_send')"
526
- @click="void showSend(link.id)"
594
+ <template #trailing>
595
+ <UIcon
596
+ name="i-ph-arrow-square-out"
597
+ class="text-dimmed size-4"
598
+ aria-hidden="true"
527
599
  />
528
600
  </template>
529
- </DmsBanner>
530
- </section>
531
-
532
- <section class="flex flex-col gap-2">
533
- <DmsEyebrow :label="key('timeline')" />
534
- <ol class="flex flex-col">
535
- <li
536
- v-for="item in timeline"
537
- :key="item.id"
538
- :class="item.isPending && 'opacity-50'"
539
- >
540
- <DmsActivityItem
541
- :icon="item.icon"
542
- :icon-color="item.isPending ? 'neutral' : item.tone"
543
- :subtitle="
544
- item.isPending ? key('pending') : item.detail || undefined
545
- "
546
- :mono="item.isDetailMono && !item.isPending"
547
- :trailing="stepTime(item)"
548
- :interactive="false"
549
- >
550
- <span
551
- :class="
552
- item.tone === 'error' && !item.isPending
553
- ? 'text-error'
554
- : undefined
555
- "
556
- >
557
- {{ stepTitle(item) }}
558
- </span>
559
- </DmsActivityItem>
560
- </li>
561
- </ol>
562
- </section>
563
-
564
- <section class="flex flex-col gap-2">
565
- <DmsEyebrow :label="key('origin')" />
566
- <div
567
- class="border-default divide-default divide-y overflow-hidden rounded-lg border"
601
+ </DmsListRow>
602
+ <!-- The "source" label goes under the value: beside it, a dotted
603
+ name like `automation.overdue-reminders` broke at its hyphen
604
+ against the label in a narrow drawer. -->
605
+ <DmsListRow
606
+ v-if="send.source"
607
+ icon="i-ph-code"
608
+ icon-size="2xs"
609
+ size="sm"
610
+ :meta="[key('source')]"
611
+ data-send-source
568
612
  >
569
- <DmsListRow
570
- :to="editorLink"
571
- icon="i-ph-envelope-simple"
572
- icon-size="2xs"
573
- size="sm"
574
- :title="originTitle"
575
- >
576
- <template #trailing>
577
- <UIcon
578
- name="i-ph-arrow-square-out"
579
- class="text-dimmed size-4"
580
- aria-hidden="true"
581
- />
582
- </template>
583
- </DmsListRow>
584
- <DmsListRow
585
- v-if="send.source"
586
- icon="i-ph-code"
587
- icon-size="2xs"
588
- size="sm"
589
- :trailing="key('source')"
613
+ <span
614
+ class="min-w-0 font-mono text-[12.5px] [overflow-wrap:anywhere]"
590
615
  >
591
- <span class="font-mono text-[12.5px]">{{ send.source }}</span>
592
- </DmsListRow>
593
- </div>
594
- </section>
595
-
596
- <section class="flex flex-col gap-2">
597
- <div class="flex items-center justify-between gap-2">
598
- <DmsEyebrow :label="key('payload')" />
599
- <UButton
600
- v-if="payload"
601
- size="xs"
602
- variant="link"
603
- :label="key('copy_json')"
604
- @click="copyPayload"
605
- />
606
- </div>
607
- <pre
608
- class="bg-elevated/50 border-default text-toned max-h-72 overflow-auto rounded-lg border p-3 font-mono text-[11.5px] leading-relaxed"
609
- >{{ payload || '{}' }}</pre>
610
- </section>
611
- </div>
616
+ {{ send.source }}
617
+ </span>
618
+ </DmsListRow>
619
+ </div>
620
+ </section>
612
621
  </div>
613
622
 
614
- <footer
615
- class="bg-default border-default flex shrink-0 items-center gap-2 border-t px-5 py-3"
616
- >
623
+ <section class="flex flex-col gap-2">
624
+ <div class="flex items-center justify-between gap-2">
625
+ <DmsEyebrow :label="key('payload')" />
626
+ <UButton
627
+ v-if="payload"
628
+ size="xs"
629
+ variant="link"
630
+ :label="key('copy_json')"
631
+ @click="copyPayload"
632
+ />
633
+ </div>
634
+ <pre
635
+ class="bg-elevated/50 border-default text-toned max-h-72 overflow-auto rounded-lg border p-3 font-mono text-[11.5px] leading-relaxed"
636
+ >{{ payload || '{}' }}</pre>
637
+ </section>
638
+
639
+ <DmsContainerFooter data-send-footer>
617
640
  <UButton
618
641
  v-for="(action, index) in actions"
619
642
  :key="action.id"
@@ -628,7 +651,7 @@ onMounted(() => {
628
651
  :loading="action.id === 'send_again' && sending"
629
652
  @click="ACTION_HANDLERS[action.id]()"
630
653
  />
631
- </footer>
654
+ </DmsContainerFooter>
632
655
  </template>
633
656
  </div>
634
657
  </template>
@@ -56,7 +56,7 @@ async function createFrom(starter: StarterSummary): Promise<void> {
56
56
  if (creatingId.value) return
57
57
  creatingId.value = starter.id
58
58
  try {
59
- const [id] = await api.createTemplate({
59
+ const { _id: id } = await api.createTemplate({
60
60
  name: starter.name,
61
61
  slug: starter.slug,
62
62
  category: knownCategory(starter.category),
@@ -17,6 +17,9 @@ import type {
17
17
  TemplateStats,
18
18
  TemplateStatus,
19
19
  } from '../types/mailing'
20
+ // Imported rather than resolved by name: every module component is a lazy
21
+ // chunk, and a card would first render without its thumbnail.
22
+ import TemplatePreviewFrame from './TemplatePreviewFrame.vue'
20
23
 
21
24
  interface TemplateCardProps {
22
25
  template: TemplateRow
@@ -172,7 +175,7 @@ function chipLabel(code: string, present: boolean): string {
172
175
  class="bg-elevated/40 relative flex h-[172px] justify-center overflow-hidden bg-[radial-gradient(var(--ui-border)_1px,transparent_1px)] bg-[length:14px_14px] pt-4"
173
176
  >
174
177
  <div class="pointer-events-none rounded-t-md shadow-sm">
175
- <MailingTemplatePreviewFrame
178
+ <TemplatePreviewFrame
176
179
  :template-id="template._id"
177
180
  :version="previewVersion"
178
181
  :width="PREVIEW_WIDTH"
@@ -513,7 +513,7 @@ function onResolved(preview: PreviewResponse): void {
513
513
  </script>
514
514
 
515
515
  <template>
516
- <div class="flex w-[30rem] max-w-full flex-col">
516
+ <div class="flex w-[30rem] max-w-full flex-1 flex-col">
517
517
  <USkeleton v-if="loading && !template" class="h-64 w-full" />
518
518
 
519
519
  <template v-else-if="template">
@@ -882,9 +882,7 @@ function onResolved(preview: PreviewResponse): void {
882
882
  />
883
883
  </div>
884
884
 
885
- <div
886
- class="border-default bg-default sticky bottom-0 -mx-1 mt-auto flex items-center gap-2 border-t px-1 py-3"
887
- >
885
+ <DmsContainerFooter data-template-footer>
888
886
  <UButton
889
887
  icon="i-ph-flask"
890
888
  color="neutral"
@@ -905,7 +903,7 @@ function onResolved(preview: PreviewResponse): void {
905
903
  :label="t('dms_mailing.templates.actions.open_editor')"
906
904
  @click="openEditor(locale)"
907
905
  />
908
- </div>
906
+ </DmsContainerFooter>
909
907
  </template>
910
908
  </div>
911
909
  </template>
@@ -1,4 +1,8 @@
1
1
  <script setup lang="ts">
2
+ // Imported rather than resolved by name: every module component is a lazy
3
+ // chunk, and the grid would first render its "new template" tile alone,
4
+ // then move it as the cards arrive.
5
+ import TemplateCard from './TemplateCard.vue'
2
6
  import { useTemplateCategories } from '../composables/useTemplateCategories'
3
7
  import { useEventListener } from '@vueuse/core'
4
8
  import {
@@ -50,7 +54,8 @@ const props = defineProps<TemplateGalleryProps>()
50
54
  const { t } = useI18n()
51
55
  const overlays = useTemplateDrawer()
52
56
 
53
- const { categories } = useTemplateCategories()
57
+ const { categories, isAnswered: areCategoriesAnswered } =
58
+ useTemplateCategories()
54
59
  const overview = useTemplatesOverview()
55
60
  const total = useTemplateTotal()
56
61
 
@@ -121,8 +126,20 @@ const search = computed(() => searchTermOf(props.context.query))
121
126
  const narrowed = computed(() => isNarrowed(props.context.query))
122
127
  const hasNoItems = computed(() => !props.context.items.length)
123
128
 
129
+ /**
130
+ * The cards draw their category group and their figures from reads of their
131
+ * own: until both answered (a client navigation reads them in the browser),
132
+ * the cards would come under "Uncategorised", then move to their group.
133
+ */
134
+ const areCardsReady = computed(
135
+ () =>
136
+ (!isGrouped.value || areCategoriesAnswered.value) &&
137
+ Boolean(overview.data.value || overview.error.value),
138
+ )
139
+
124
140
  const STATE_RULES: StateRule[] = [
125
141
  ['loading', () => props.context.loading && hasNoItems.value],
142
+ ['loading', () => !hasNoItems.value && !areCardsReady.value],
126
143
  [
127
144
  'loading',
128
145
  () => hasNoItems.value && !narrowed.value && templateCount.value === null,
@@ -218,24 +235,47 @@ useEventListener(
218
235
 
219
236
  <template>
220
237
  <div class="flex flex-col gap-6 p-4">
221
- <div
238
+ <!-- Laid out as a group of cards: the heading, the thumbnail and the
239
+ card's three lines at their own heights (a zero-width space keeps a
240
+ text line's height), so the cards replace it without moving. -->
241
+ <section
222
242
  v-if="state === 'loading'"
223
- class="grid grid-cols-[repeat(auto-fill,minmax(272px,1fr))] gap-4"
243
+ class="flex flex-col gap-3"
224
244
  aria-busy="true"
245
+ data-gallery-skeleton
225
246
  >
226
- <DmsCard
227
- v-for="index in SKELETON_CARDS"
228
- :key="index"
229
- :padded="false"
230
- class="overflow-hidden"
231
- >
232
- <USkeleton class="h-[172px] w-full rounded-none" />
233
- <div class="flex flex-col gap-2 p-3.5">
234
- <USkeleton class="h-3 w-3/5" />
235
- <USkeleton class="h-2.5 w-2/5" />
236
- </div>
237
- </DmsCard>
238
- </div>
247
+ <div v-if="isGrouped" class="flex items-center gap-2 text-[13px]">
248
+ <USkeleton class="size-4" />
249
+ <USkeleton class="h-3 w-24" />
250
+ &#8203;
251
+ </div>
252
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(272px,1fr))] gap-4">
253
+ <DmsCard
254
+ v-for="index in SKELETON_CARDS"
255
+ :key="index"
256
+ :padded="false"
257
+ class="flex flex-col overflow-hidden"
258
+ >
259
+ <USkeleton class="h-[172px] w-full rounded-none" />
260
+ <div
261
+ class="border-default flex flex-1 flex-col gap-1.5 border-t px-3.5 pb-3 pt-3"
262
+ >
263
+ <div class="flex h-6 items-center">
264
+ <USkeleton class="h-3.5 w-3/5" />
265
+ </div>
266
+ <div class="flex h-[18px] items-center">
267
+ <USkeleton class="h-2.5 w-2/5" />
268
+ </div>
269
+ <div
270
+ class="border-default mt-1 flex items-center gap-3 border-t pt-2 font-mono text-[11.5px]"
271
+ >
272
+ <USkeleton class="h-2.5 w-1/2" />
273
+ &#8203;
274
+ </div>
275
+ </div>
276
+ </DmsCard>
277
+ </div>
278
+ </section>
239
279
 
240
280
  <MailingStarterGallery
241
281
  v-else-if="state === 'first_run'"
@@ -308,7 +348,7 @@ useEventListener(
308
348
  <span class="bg-(--ui-border) ml-2 h-px flex-1" />
309
349
  </div>
310
350
  <div class="grid grid-cols-[repeat(auto-fill,minmax(272px,1fr))] gap-4">
311
- <MailingTemplateCard
351
+ <TemplateCard
312
352
  v-for="template in group.items"
313
353
  :key="template._id"
314
354
  :template="template"