@antelopejs/dms-marketing 0.2.2

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 (308) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +143 -0
  3. package/dist/config.d.ts +13 -0
  4. package/dist/config.js +35 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/data-api/body.d.ts +1 -0
  7. package/dist/data-api/body.js +15 -0
  8. package/dist/data-api/body.js.map +1 -0
  9. package/dist/data-api/funnels.d.ts +32 -0
  10. package/dist/data-api/funnels.js +302 -0
  11. package/dist/data-api/funnels.js.map +1 -0
  12. package/dist/data-api/hidden-filter.d.ts +1 -0
  13. package/dist/data-api/hidden-filter.js +11 -0
  14. package/dist/data-api/hidden-filter.js.map +1 -0
  15. package/dist/data-api/index.d.ts +4 -0
  16. package/dist/data-api/index.js +21 -0
  17. package/dist/data-api/index.js.map +1 -0
  18. package/dist/data-api/tenant-scope.d.ts +15 -0
  19. package/dist/data-api/tenant-scope.js +43 -0
  20. package/dist/data-api/tenant-scope.js.map +1 -0
  21. package/dist/data-api/websites.d.ts +25 -0
  22. package/dist/data-api/websites.js +73 -0
  23. package/dist/data-api/websites.js.map +1 -0
  24. package/dist/db/index.d.ts +2 -0
  25. package/dist/db/index.js +19 -0
  26. package/dist/db/index.js.map +1 -0
  27. package/dist/db/models/funnels.model.d.ts +25 -0
  28. package/dist/db/models/funnels.model.js +39 -0
  29. package/dist/db/models/funnels.model.js.map +1 -0
  30. package/dist/db/models/index.d.ts +7 -0
  31. package/dist/db/models/index.js +24 -0
  32. package/dist/db/models/index.js.map +1 -0
  33. package/dist/db/models/marketing_events.model.d.ts +49 -0
  34. package/dist/db/models/marketing_events.model.js +85 -0
  35. package/dist/db/models/marketing_events.model.js.map +1 -0
  36. package/dist/db/models/marketing_sessions.model.d.ts +33 -0
  37. package/dist/db/models/marketing_sessions.model.js +50 -0
  38. package/dist/db/models/marketing_sessions.model.js.map +1 -0
  39. package/dist/db/models/marketing_settings.model.d.ts +23 -0
  40. package/dist/db/models/marketing_settings.model.js +13 -0
  41. package/dist/db/models/marketing_settings.model.js.map +1 -0
  42. package/dist/db/models/page_snapshots.model.d.ts +27 -0
  43. package/dist/db/models/page_snapshots.model.js +32 -0
  44. package/dist/db/models/page_snapshots.model.js.map +1 -0
  45. package/dist/db/models/retention.d.ts +6 -0
  46. package/dist/db/models/retention.js +26 -0
  47. package/dist/db/models/retention.js.map +1 -0
  48. package/dist/db/models/website_statistics.model.d.ts +31 -0
  49. package/dist/db/models/website_statistics.model.js +63 -0
  50. package/dist/db/models/website_statistics.model.js.map +1 -0
  51. package/dist/db/models/websites.model.d.ts +26 -0
  52. package/dist/db/models/websites.model.js +27 -0
  53. package/dist/db/models/websites.model.js.map +1 -0
  54. package/dist/db/tables/funnels.table.d.ts +31 -0
  55. package/dist/db/tables/funnels.table.js +54 -0
  56. package/dist/db/tables/funnels.table.js.map +1 -0
  57. package/dist/db/tables/index.d.ts +7 -0
  58. package/dist/db/tables/index.js +24 -0
  59. package/dist/db/tables/index.js.map +1 -0
  60. package/dist/db/tables/marketing_events.table.d.ts +15 -0
  61. package/dist/db/tables/marketing_events.table.js +66 -0
  62. package/dist/db/tables/marketing_events.table.js.map +1 -0
  63. package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
  64. package/dist/db/tables/marketing_sessions.table.js +115 -0
  65. package/dist/db/tables/marketing_sessions.table.js.map +1 -0
  66. package/dist/db/tables/marketing_settings.table.d.ts +11 -0
  67. package/dist/db/tables/marketing_settings.table.js +51 -0
  68. package/dist/db/tables/marketing_settings.table.js.map +1 -0
  69. package/dist/db/tables/page_snapshots.table.d.ts +17 -0
  70. package/dist/db/tables/page_snapshots.table.js +72 -0
  71. package/dist/db/tables/page_snapshots.table.js.map +1 -0
  72. package/dist/db/tables/website_statistics.table.d.ts +14 -0
  73. package/dist/db/tables/website_statistics.table.js +60 -0
  74. package/dist/db/tables/website_statistics.table.js.map +1 -0
  75. package/dist/db/tables/websites.table.d.ts +13 -0
  76. package/dist/db/tables/websites.table.js +60 -0
  77. package/dist/db/tables/websites.table.js.map +1 -0
  78. package/dist/hooks/index.d.ts +1 -0
  79. package/dist/hooks/index.js +39 -0
  80. package/dist/hooks/index.js.map +1 -0
  81. package/dist/implementations/dms-marketing/index.d.ts +2 -0
  82. package/dist/implementations/dms-marketing/index.js +8 -0
  83. package/dist/implementations/dms-marketing/index.js.map +1 -0
  84. package/dist/index.d.ts +7 -0
  85. package/dist/index.js +89 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/pages/all.d.ts +5 -0
  88. package/dist/pages/all.js +22 -0
  89. package/dist/pages/all.js.map +1 -0
  90. package/dist/pages/campaigns/page.d.ts +9 -0
  91. package/dist/pages/campaigns/page.js +27 -0
  92. package/dist/pages/campaigns/page.js.map +1 -0
  93. package/dist/pages/funnels/page.d.ts +9 -0
  94. package/dist/pages/funnels/page.js +27 -0
  95. package/dist/pages/funnels/page.js.map +1 -0
  96. package/dist/pages/index.d.ts +4 -0
  97. package/dist/pages/index.js +21 -0
  98. package/dist/pages/index.js.map +1 -0
  99. package/dist/pages/module.d.ts +1 -0
  100. package/dist/pages/module.js +13 -0
  101. package/dist/pages/module.js.map +1 -0
  102. package/dist/pages/overview/page.d.ts +9 -0
  103. package/dist/pages/overview/page.js +27 -0
  104. package/dist/pages/overview/page.js.map +1 -0
  105. package/dist/pages/pages/page.d.ts +9 -0
  106. package/dist/pages/pages/page.js +27 -0
  107. package/dist/pages/pages/page.js.map +1 -0
  108. package/dist/pages/settings/form.d.ts +3 -0
  109. package/dist/pages/settings/form.js +35 -0
  110. package/dist/pages/settings/form.js.map +1 -0
  111. package/dist/pages/settings/page.d.ts +9 -0
  112. package/dist/pages/settings/page.js +27 -0
  113. package/dist/pages/settings/page.js.map +1 -0
  114. package/dist/routes/collect.d.ts +7 -0
  115. package/dist/routes/collect.js +62 -0
  116. package/dist/routes/collect.js.map +1 -0
  117. package/dist/routes/experiments.d.ts +6 -0
  118. package/dist/routes/experiments.js +63 -0
  119. package/dist/routes/experiments.js.map +1 -0
  120. package/dist/routes/funnels.d.ts +16 -0
  121. package/dist/routes/funnels.js +44 -0
  122. package/dist/routes/funnels.js.map +1 -0
  123. package/dist/routes/index.d.ts +8 -0
  124. package/dist/routes/index.js +25 -0
  125. package/dist/routes/index.js.map +1 -0
  126. package/dist/routes/settings.d.ts +8 -0
  127. package/dist/routes/settings.js +50 -0
  128. package/dist/routes/settings.js.map +1 -0
  129. package/dist/routes/snapshot.d.ts +10 -0
  130. package/dist/routes/snapshot.js +147 -0
  131. package/dist/routes/snapshot.js.map +1 -0
  132. package/dist/routes/stats.d.ts +44 -0
  133. package/dist/routes/stats.js +278 -0
  134. package/dist/routes/stats.js.map +1 -0
  135. package/dist/routes/tracker.d.ts +6 -0
  136. package/dist/routes/tracker.js +57 -0
  137. package/dist/routes/tracker.js.map +1 -0
  138. package/dist/routes/websites.d.ts +13 -0
  139. package/dist/routes/websites.js +144 -0
  140. package/dist/routes/websites.js.map +1 -0
  141. package/dist/services/acquisition.d.ts +15 -0
  142. package/dist/services/acquisition.js +153 -0
  143. package/dist/services/acquisition.js.map +1 -0
  144. package/dist/services/bounded-cache.d.ts +14 -0
  145. package/dist/services/bounded-cache.js +45 -0
  146. package/dist/services/bounded-cache.js.map +1 -0
  147. package/dist/services/collect.d.ts +11 -0
  148. package/dist/services/collect.js +193 -0
  149. package/dist/services/collect.js.map +1 -0
  150. package/dist/services/experiment-definitions.d.ts +4 -0
  151. package/dist/services/experiment-definitions.js +36 -0
  152. package/dist/services/experiment-definitions.js.map +1 -0
  153. package/dist/services/experiments/assign.d.ts +5 -0
  154. package/dist/services/experiments/assign.js +52 -0
  155. package/dist/services/experiments/assign.js.map +1 -0
  156. package/dist/services/experiments/index.d.ts +3 -0
  157. package/dist/services/experiments/index.js +20 -0
  158. package/dist/services/experiments/index.js.map +1 -0
  159. package/dist/services/experiments/results.d.ts +20 -0
  160. package/dist/services/experiments/results.js +91 -0
  161. package/dist/services/experiments/results.js.map +1 -0
  162. package/dist/services/experiments/stats.d.ts +12 -0
  163. package/dist/services/experiments/stats.js +62 -0
  164. package/dist/services/experiments/stats.js.map +1 -0
  165. package/dist/services/funnels/compute.d.ts +15 -0
  166. package/dist/services/funnels/compute.js +68 -0
  167. package/dist/services/funnels/compute.js.map +1 -0
  168. package/dist/services/funnels/index.d.ts +2 -0
  169. package/dist/services/funnels/index.js +19 -0
  170. package/dist/services/funnels/index.js.map +1 -0
  171. package/dist/services/funnels/results.d.ts +14 -0
  172. package/dist/services/funnels/results.js +72 -0
  173. package/dist/services/funnels/results.js.map +1 -0
  174. package/dist/services/geoip.d.ts +3 -0
  175. package/dist/services/geoip.js +38 -0
  176. package/dist/services/geoip.js.map +1 -0
  177. package/dist/services/heatmap.d.ts +10 -0
  178. package/dist/services/heatmap.js +180 -0
  179. package/dist/services/heatmap.js.map +1 -0
  180. package/dist/services/index.d.ts +26 -0
  181. package/dist/services/index.js +43 -0
  182. package/dist/services/index.js.map +1 -0
  183. package/dist/services/ingest-guard.d.ts +11 -0
  184. package/dist/services/ingest-guard.js +44 -0
  185. package/dist/services/ingest-guard.js.map +1 -0
  186. package/dist/services/keyed-queue.d.ts +4 -0
  187. package/dist/services/keyed-queue.js +19 -0
  188. package/dist/services/keyed-queue.js.map +1 -0
  189. package/dist/services/origin-guard.d.ts +5 -0
  190. package/dist/services/origin-guard.js +43 -0
  191. package/dist/services/origin-guard.js.map +1 -0
  192. package/dist/services/period.d.ts +2 -0
  193. package/dist/services/period.js +19 -0
  194. package/dist/services/period.js.map +1 -0
  195. package/dist/services/prune.d.ts +7 -0
  196. package/dist/services/prune.js +49 -0
  197. package/dist/services/prune.js.map +1 -0
  198. package/dist/services/query-param.d.ts +1 -0
  199. package/dist/services/query-param.js +11 -0
  200. package/dist/services/query-param.js.map +1 -0
  201. package/dist/services/request-body.d.ts +2 -0
  202. package/dist/services/request-body.js +41 -0
  203. package/dist/services/request-body.js.map +1 -0
  204. package/dist/services/request-meta.d.ts +3 -0
  205. package/dist/services/request-meta.js +19 -0
  206. package/dist/services/request-meta.js.map +1 -0
  207. package/dist/services/rollup-write.d.ts +4 -0
  208. package/dist/services/rollup-write.js +84 -0
  209. package/dist/services/rollup-write.js.map +1 -0
  210. package/dist/services/sessionize.d.ts +23 -0
  211. package/dist/services/sessionize.js +78 -0
  212. package/dist/services/sessionize.js.map +1 -0
  213. package/dist/services/settings.d.ts +6 -0
  214. package/dist/services/settings.js +113 -0
  215. package/dist/services/settings.js.map +1 -0
  216. package/dist/services/snapshots.d.ts +24 -0
  217. package/dist/services/snapshots.js +139 -0
  218. package/dist/services/snapshots.js.map +1 -0
  219. package/dist/services/tenant-funnel.d.ts +3 -0
  220. package/dist/services/tenant-funnel.js +17 -0
  221. package/dist/services/tenant-funnel.js.map +1 -0
  222. package/dist/services/tenant-website.d.ts +5 -0
  223. package/dist/services/tenant-website.js +34 -0
  224. package/dist/services/tenant-website.js.map +1 -0
  225. package/dist/services/url.d.ts +10 -0
  226. package/dist/services/url.js +81 -0
  227. package/dist/services/url.js.map +1 -0
  228. package/dist/services/user-agent.d.ts +2 -0
  229. package/dist/services/user-agent.js +17 -0
  230. package/dist/services/user-agent.js.map +1 -0
  231. package/dist/services/visitor-id.d.ts +1 -0
  232. package/dist/services/visitor-id.js +19 -0
  233. package/dist/services/visitor-id.js.map +1 -0
  234. package/dist/services/website-cache.d.ts +3 -0
  235. package/dist/services/website-cache.js +22 -0
  236. package/dist/services/website-cache.js.map +1 -0
  237. package/dist/types/constants.d.ts +74 -0
  238. package/dist/types/constants.js +79 -0
  239. package/dist/types/constants.js.map +1 -0
  240. package/dist/types/events.d.ts +114 -0
  241. package/dist/types/events.js +29 -0
  242. package/dist/types/events.js.map +1 -0
  243. package/dist/types/index.d.ts +5 -0
  244. package/dist/types/index.js +22 -0
  245. package/dist/types/index.js.map +1 -0
  246. package/dist/types/settings.d.ts +67 -0
  247. package/dist/types/settings.js +58 -0
  248. package/dist/types/settings.js.map +1 -0
  249. package/dist/types/snapshots.d.ts +95 -0
  250. package/dist/types/snapshots.js +41 -0
  251. package/dist/types/snapshots.js.map +1 -0
  252. package/dist/types/statistics.d.ts +38 -0
  253. package/dist/types/statistics.js +33 -0
  254. package/dist/types/statistics.js.map +1 -0
  255. package/dist/utils/experiment-type.d.ts +52 -0
  256. package/dist/utils/experiment-type.js +94 -0
  257. package/dist/utils/experiment-type.js.map +1 -0
  258. package/dist/utils/funnel-steps-type.d.ts +42 -0
  259. package/dist/utils/funnel-steps-type.js +62 -0
  260. package/dist/utils/funnel-steps-type.js.map +1 -0
  261. package/frontend-vue/.editorconfig +12 -0
  262. package/frontend-vue/.prettierrc +7 -0
  263. package/frontend-vue/app/components/CampaignsView.vue +330 -0
  264. package/frontend-vue/app/components/ExperimentInput.vue +128 -0
  265. package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
  266. package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
  267. package/frontend-vue/app/components/FunnelsView.vue +493 -0
  268. package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
  269. package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
  270. package/frontend-vue/app/components/OverviewView.vue +493 -0
  271. package/frontend-vue/app/components/PagePreview.vue +915 -0
  272. package/frontend-vue/app/components/PagesView.vue +378 -0
  273. package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
  274. package/frontend-vue/app/components/TextKpiCard.vue +28 -0
  275. package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
  276. package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
  277. package/frontend-vue/app/components/VariationsInput.vue +164 -0
  278. package/frontend-vue/app/composables/useChildId.ts +13 -0
  279. package/frontend-vue/app/composables/useExperiment.ts +59 -0
  280. package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
  281. package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
  282. package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
  283. package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
  284. package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
  285. package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
  286. package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
  287. package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
  288. package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
  289. package/frontend-vue/app/utils/dmsComponent.ts +18 -0
  290. package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
  291. package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
  292. package/frontend-vue/dms.frontend.ts +35 -0
  293. package/frontend-vue/eslint.config.mjs +16 -0
  294. package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
  295. package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
  296. package/frontend-vue/package.json +30 -0
  297. package/frontend-vue/pnpm-lock.yaml +5226 -0
  298. package/frontend-vue/pnpm-workspace.yaml +2 -0
  299. package/frontend-vue/tests/drafts.test.ts +54 -0
  300. package/frontend-vue/tests/i18n.test.ts +86 -0
  301. package/frontend-vue/tests/period.test.ts +35 -0
  302. package/frontend-vue/tests/registration.test.ts +63 -0
  303. package/frontend-vue/tests/routes.test.ts +31 -0
  304. package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
  305. package/frontend-vue/vitest.config.ts +20 -0
  306. package/package.json +91 -0
  307. package/static/snapshot.js +497 -0
  308. package/static/tracker.js +503 -0
@@ -0,0 +1,183 @@
1
+ import { computed, onBeforeUnmount, ref, watch, type Ref } from 'vue'
2
+
3
+ /**
4
+ * Geometry of a page snapshot rendered in a same-origin, script-less frame.
5
+ *
6
+ * The frame is `srcdoc`, sandboxed without scripts and with the console's
7
+ * origin, so its document is ours to read: no handshake, no timeout, no
8
+ * silence to diagnose. The size is still taken from at most ONE reading,
9
+ * with the frame pinned at a probe viewport until then: sizing the frame
10
+ * from a reading taken through it runs away, every `100vh` section growing
11
+ * one tick at a time, exactly as it would with a live page. Once published,
12
+ * later growth is ignored until `reset()`.
13
+ */
14
+
15
+ /** Fallback document height when nothing measured it. */
16
+ export const DEFAULT_RENDER_HEIGHT = 3200
17
+
18
+ /** Floor on any render height, measured or typed. */
19
+ export const MIN_RENDER_HEIGHT = 400
20
+
21
+ /** Ceilings on the drawn surface: the canvas allocates width × height × 4 B. */
22
+ export const MAX_RENDER_HEIGHT = 20000
23
+ export const MAX_RENDER_WIDTH = 4000
24
+
25
+ /** Post-load growth worth waiting for: web fonts swapping in. */
26
+ const SETTLE_MS = 300
27
+
28
+ const SRCDOC_URL = 'about:srcdoc'
29
+
30
+ /**
31
+ * `off` — no frame mounted.
32
+ * `pending` — a frame is up, nothing published yet; it stays at probe size.
33
+ * `measured` — a size was published; geometry comes from the snapshot.
34
+ */
35
+ export type SnapshotTier = 'off' | 'pending' | 'measured'
36
+
37
+ /** What the overlay asks the snapshot to locate for it. */
38
+ export interface AnchorRequest {
39
+ selector: string
40
+ nth: number
41
+ }
42
+
43
+ /** Box of one anchor, in the snapshot document's own coordinates. */
44
+ export interface AnchorBox extends AnchorRequest {
45
+ x: number
46
+ y: number
47
+ width: number
48
+ height: number
49
+ }
50
+
51
+ export function useSnapshotFrame(frame: Ref<HTMLIFrameElement | null>) {
52
+ /** Published size — the caller may size the frame from this, once. */
53
+ const measuredWidth = ref<number | null>(null)
54
+ const measuredHeight = ref<number | null>(null)
55
+
56
+ /** Published or given up on: no further reading may be taken. */
57
+ let closed = false
58
+ let settleTimer: ReturnType<typeof setTimeout> | null = null
59
+ let watchedFrame: HTMLIFrameElement | null = null
60
+
61
+ function clearTimer(): void {
62
+ if (settleTimer !== null) {
63
+ clearTimeout(settleTimer)
64
+ settleTimer = null
65
+ }
66
+ }
67
+
68
+ /** The srcdoc document, never the blank one a fresh frame starts on. */
69
+ function frameDocument(): Document | null {
70
+ const doc = frame.value?.contentDocument ?? null
71
+ return doc && doc.URL === SRCDOC_URL ? doc : null
72
+ }
73
+
74
+ function publish(): void {
75
+ const doc = frameDocument()
76
+ if (closed || !doc) {
77
+ return
78
+ }
79
+ closed = true
80
+ clearTimer()
81
+ const root = doc.documentElement
82
+ // Reported as measured, not clamped: a document too large to draw is a
83
+ // decision for the caller, and silently shrinking it here would misplace
84
+ // every click by the ratio it was shrunk.
85
+ measuredWidth.value = Math.max(root.scrollWidth, 1)
86
+ measuredHeight.value = Math.max(root.scrollHeight, MIN_RENDER_HEIGHT)
87
+ }
88
+
89
+ function onFrameLoad(): void {
90
+ const doc = frameDocument()
91
+ if (!doc || closed) {
92
+ return
93
+ }
94
+ void (doc.fonts?.ready ?? Promise.resolve()).then(() => {
95
+ if (closed) {
96
+ return
97
+ }
98
+ clearTimer()
99
+ settleTimer = setTimeout(publish, SETTLE_MS)
100
+ })
101
+ }
102
+
103
+ /**
104
+ * Stop reading without publishing. The caller uses this when it resizes
105
+ * the frame by hand (a typed height): from then on every reading measures
106
+ * that layout, not the snapshot's.
107
+ */
108
+ function close(): void {
109
+ closed = true
110
+ clearTimer()
111
+ }
112
+
113
+ /** Start over: forget the size and wait for the frame to load. */
114
+ function reset(): void {
115
+ clearTimer()
116
+ closed = false
117
+ measuredWidth.value = null
118
+ measuredHeight.value = null
119
+
120
+ watchedFrame?.removeEventListener('load', onFrameLoad)
121
+ watchedFrame = frame.value
122
+ watchedFrame?.addEventListener('load', onFrameLoad)
123
+ if (frameDocument()?.readyState === 'complete') {
124
+ onFrameLoad()
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Where a set of anchors sits in the snapshot. `nth` is strict: a document
130
+ * that no longer has that match must not project the click onto a sibling
131
+ * of the same selector — no box means the caller keeps the click's
132
+ * document fractions. Read after the frame has settled at its final size;
133
+ * boxes taken while it is being resized measure the wrong layout.
134
+ */
135
+ function measureAnchors(anchors: AnchorRequest[]): AnchorBox[] {
136
+ const doc = frameDocument()
137
+ const view = doc?.defaultView
138
+ if (!doc || !view) {
139
+ return []
140
+ }
141
+ const boxes: AnchorBox[] = []
142
+ for (const anchor of anchors) {
143
+ let matches: NodeListOf<Element>
144
+ try {
145
+ matches = doc.querySelectorAll(anchor.selector)
146
+ }
147
+ catch {
148
+ continue
149
+ }
150
+ const box = matches[anchor.nth]?.getBoundingClientRect()
151
+ if (!box || box.width <= 0 || box.height <= 0) {
152
+ continue
153
+ }
154
+ boxes.push({
155
+ ...anchor,
156
+ x: box.left + view.scrollX,
157
+ y: box.top + view.scrollY,
158
+ width: box.width,
159
+ height: box.height,
160
+ })
161
+ }
162
+ return boxes
163
+ }
164
+
165
+ onBeforeUnmount(() => {
166
+ watchedFrame?.removeEventListener('load', onFrameLoad)
167
+ watchedFrame = null
168
+ clearTimer()
169
+ })
170
+
171
+ // A new frame is a new reading: the ref is only set once the iframe is
172
+ // actually rendered, which the caller does conditionally and keyed.
173
+ watch(frame, reset, { immediate: true })
174
+
175
+ const tier = computed<SnapshotTier>(() => {
176
+ if (!frame.value) {
177
+ return 'off'
178
+ }
179
+ return measuredHeight.value !== null ? 'measured' : 'pending'
180
+ })
181
+
182
+ return { measuredWidth, measuredHeight, tier, reset, close, measureAnchors }
183
+ }
@@ -0,0 +1,46 @@
1
+ import { defineComponent, h } from 'vue'
2
+ import { defineDmsPlugin } from '#dms/frontend-module'
3
+ import FunnelStepsInput from '../components/FunnelStepsInput.vue'
4
+ import type { FunnelStepDraft } from '../composables/useFunnelSteps'
5
+ import { parseFunnelStepsValue } from '../composables/useFunnelSteps'
6
+
7
+ const MAX_PREVIEW_STEPS = 3
8
+
9
+ function renderStepsPreview(value: unknown): string {
10
+ const steps = parseFunnelStepsValue(value)
11
+ if (!steps.length) {
12
+ return '—'
13
+ }
14
+ const items = steps.slice(0, MAX_PREVIEW_STEPS).map((step) => step.value)
15
+ const remaining = steps.length - MAX_PREVIEW_STEPS
16
+ const tail = remaining > 0 ? ` +${remaining}` : ''
17
+ return `${items.join(' → ')}${tail}`
18
+ }
19
+
20
+ const FunnelStepsDisplay = defineComponent({
21
+ name: 'FunnelStepsDisplay',
22
+ props: {
23
+ modelValue: {
24
+ type: [Array, String] as unknown as () => FunnelStepDraft[] | string | null,
25
+ default: null,
26
+ },
27
+ },
28
+ setup(props) {
29
+ return () =>
30
+ h(FunnelStepsInput, {
31
+ modelValue: props.modelValue,
32
+ disabled: true,
33
+ })
34
+ },
35
+ })
36
+
37
+ export default defineDmsPlugin(() => {
38
+ const { registerDataType } = useDataTypes()
39
+ registerDataType({
40
+ id: 'marketing_funnel_steps',
41
+ displayComponent: FunnelStepsDisplay,
42
+ formatter: {
43
+ default: (value: unknown) => renderStepsPreview(value),
44
+ },
45
+ })
46
+ })
@@ -0,0 +1,18 @@
1
+ import type { Component } from 'vue'
2
+ import { resolveDmsComponent } from '#dms/frontend-module'
3
+
4
+ /**
5
+ * A DMS core component addressed by name.
6
+ *
7
+ * The core frontend module registers its components before any other module's
8
+ * entry runs, so a name that does not resolve is a programming error rather
9
+ * than a state worth rendering around — and the callers hand the result to
10
+ * APIs that take a component, not an optional one.
11
+ */
12
+ export function dmsComponent(name: string): Component {
13
+ const component = resolveDmsComponent(name)
14
+ if (!component) {
15
+ throw new Error(`Unknown DMS component: ${name}`)
16
+ }
17
+ return component
18
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The heat ramp shared by the click heatmap and the scroll-depth overlay, so
3
+ * the two overlays read as one scale.
4
+ *
5
+ * Literal colors on purpose: this is a data scale, not interface chrome. The
6
+ * theme carries semantic tokens (primary, error, muted…) but no sequential
7
+ * ramp, and "red means hot" derives from none of them.
8
+ */
9
+
10
+ export type HeatRgb = readonly [number, number, number]
11
+
12
+ /** Position on the 0-1 ramp → color, linearly interpolated in between. */
13
+ export const HEAT_STOPS: ReadonlyArray<readonly [number, HeatRgb]> = [
14
+ [0.0, [0, 80, 255]],
15
+ [0.4, [0, 200, 255]],
16
+ [0.6, [80, 220, 60]],
17
+ [0.8, [255, 220, 0]],
18
+ [1.0, [255, 40, 0]],
19
+ ]
20
+
21
+ export function heatRgba(rgb: HeatRgb, alpha: number): string {
22
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`
23
+ }
24
+
25
+ export function heatColorAt(fraction: number, alpha: number): string {
26
+ const bounded = Math.min(Math.max(fraction, 0), 1)
27
+ let low = HEAT_STOPS[0]!
28
+ let high = HEAT_STOPS[HEAT_STOPS.length - 1]!
29
+ for (let i = 0; i < HEAT_STOPS.length - 1; i++) {
30
+ if (bounded >= HEAT_STOPS[i]![0] && bounded <= HEAT_STOPS[i + 1]![0]) {
31
+ low = HEAT_STOPS[i]!
32
+ high = HEAT_STOPS[i + 1]!
33
+ break
34
+ }
35
+ }
36
+ const span = high[0] - low[0]
37
+ const mix = span > 0 ? (bounded - low[0]) / span : 0
38
+ const channel = (index: number) =>
39
+ Math.round(low[1][index]! + (high[1][index]! - low[1][index]!) * mix)
40
+ return heatRgba([channel(0), channel(1), channel(2)], alpha)
41
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Viewport-relative units of a snapshot, rewritten to the pixels they
3
+ * resolved to in the visitor's viewport.
4
+ *
5
+ * The snapshot frame is as tall as the whole document, so inside it `100vh`
6
+ * is the document height, not a screen: every `min-height: 70vh` section
7
+ * grows to 70 % of the page and the rest is pushed past the frame's edge.
8
+ * No frame can be told otherwise; the stylesheet can. Only declaration
9
+ * values are rewritten — a Tailwind arbitrary value puts `100vh` in the
10
+ * selector too (`.h-\[100vh\]`), and rewriting it there would orphan the
11
+ * rule — and a colon escaped with a backslash (`.md\:h-full`) is a selector
12
+ * character, not the start of a value.
13
+ *
14
+ * Horizontal units are left alone: the frame is laid out at the chosen
15
+ * width, so `vw` already means what it should.
16
+ */
17
+
18
+ export interface SnapshotViewport {
19
+ width: number
20
+ height: number
21
+ }
22
+
23
+ const VIEWPORT_UNIT = /(-?(?:\d+\.?\d*|\.\d+))(dvh|svh|lvh|vh|vmin|vmax)(?![\w-])/g
24
+ const DECLARATION_VALUE = /((?<!\\):)([^;{}]*)/g
25
+ const STYLE_BLOCK = /(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi
26
+ const STYLE_ATTRIBUTE = /(\sstyle=")([^"]*)(")/gi
27
+
28
+ function pixelsOf(amount: string, unit: string, viewport: SnapshotViewport): number {
29
+ const base = unit.endsWith('vh')
30
+ ? viewport.height
31
+ : unit === 'vmin'
32
+ ? Math.min(viewport.width, viewport.height)
33
+ : Math.max(viewport.width, viewport.height)
34
+ return Math.round(Number.parseFloat(amount) * base) / 100
35
+ }
36
+
37
+ function rewriteValue(value: string, viewport: SnapshotViewport): string {
38
+ return value.replace(
39
+ VIEWPORT_UNIT,
40
+ (_match, amount: string, unit: string) => `${pixelsOf(amount, unit, viewport)}px`,
41
+ )
42
+ }
43
+
44
+ function rewriteDeclarations(css: string, viewport: SnapshotViewport): string {
45
+ return css.replace(
46
+ DECLARATION_VALUE,
47
+ (_match, colon: string, value: string) => colon + rewriteValue(value, viewport),
48
+ )
49
+ }
50
+
51
+ export function rewriteViewportUnits(html: string, viewport: SnapshotViewport): string {
52
+ return html
53
+ .replace(
54
+ STYLE_BLOCK,
55
+ (_match, open: string, css: string, close: string) =>
56
+ open + rewriteDeclarations(css, viewport) + close,
57
+ )
58
+ .replace(
59
+ STYLE_ATTRIBUTE,
60
+ (_match, open: string, css: string, close: string) =>
61
+ open + rewriteDeclarations(css, viewport) + close,
62
+ )
63
+ }
@@ -0,0 +1,35 @@
1
+ import { defineAsyncComponent, type Component } from 'vue'
2
+ import type { DmsFrontendModule } from '#dms/frontend-module'
3
+ import funnelStepsDataType from './app/plugins/funnel-steps-data-type.client'
4
+
5
+ interface VueModule {
6
+ default: Component
7
+ }
8
+
9
+ /**
10
+ * Every component of this module is addressed by name from the backend: the
11
+ * pages declare `CustomComponent('DmsMarketing<Name>')` and the DataTypes
12
+ * resolve their inputs the same way. Registering the whole directory under
13
+ * that prefix keeps the two sides in sync without a manual list.
14
+ */
15
+ const components = import.meta.glob<VueModule>('./app/components/**/*.vue')
16
+
17
+ const frontendModule: DmsFrontendModule = {
18
+ setup(sdk) {
19
+ for (const [path, loader] of Object.entries(components).sort()) {
20
+ const name = path
21
+ .split('/')
22
+ .at(-1)!
23
+ .replace(/\.vue$/, '')
24
+ sdk.registerComponent(
25
+ `DmsMarketing${name}`,
26
+ defineAsyncComponent(async () => (await loader()).default),
27
+ )
28
+ }
29
+ // Client-only: the display it registers carries callables, which the SSR
30
+ // payload strips.
31
+ sdk.registerPlugin(funnelStepsDataType, { clientOnly: true })
32
+ },
33
+ }
34
+
35
+ export default frontendModule
@@ -0,0 +1,16 @@
1
+ import vue from 'eslint-plugin-vue'
2
+ import ts from 'typescript-eslint'
3
+
4
+ export default [
5
+ { ignores: ['node_modules/**', 'dist/**'] },
6
+ ...vue.configs['flat/essential'],
7
+ {
8
+ files: ['**/*.ts', '**/*.vue'],
9
+ languageOptions: { parserOptions: { parser: ts.parser } },
10
+ rules: {
11
+ curly: ['error', 'all'],
12
+ 'vue/multi-word-component-names': 'off',
13
+ },
14
+ },
15
+ { files: ['**/*.ts'], languageOptions: { parser: ts.parser } },
16
+ ]