@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.
- package/LICENSE +190 -0
- package/README.md +143 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +35 -0
- package/dist/config.js.map +1 -0
- package/dist/data-api/body.d.ts +1 -0
- package/dist/data-api/body.js +15 -0
- package/dist/data-api/body.js.map +1 -0
- package/dist/data-api/funnels.d.ts +32 -0
- package/dist/data-api/funnels.js +302 -0
- package/dist/data-api/funnels.js.map +1 -0
- package/dist/data-api/hidden-filter.d.ts +1 -0
- package/dist/data-api/hidden-filter.js +11 -0
- package/dist/data-api/hidden-filter.js.map +1 -0
- package/dist/data-api/index.d.ts +4 -0
- package/dist/data-api/index.js +21 -0
- package/dist/data-api/index.js.map +1 -0
- package/dist/data-api/tenant-scope.d.ts +15 -0
- package/dist/data-api/tenant-scope.js +43 -0
- package/dist/data-api/tenant-scope.js.map +1 -0
- package/dist/data-api/websites.d.ts +25 -0
- package/dist/data-api/websites.js +73 -0
- package/dist/data-api/websites.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/funnels.model.d.ts +25 -0
- package/dist/db/models/funnels.model.js +39 -0
- package/dist/db/models/funnels.model.js.map +1 -0
- package/dist/db/models/index.d.ts +7 -0
- package/dist/db/models/index.js +24 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/marketing_events.model.d.ts +49 -0
- package/dist/db/models/marketing_events.model.js +85 -0
- package/dist/db/models/marketing_events.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +33 -0
- package/dist/db/models/marketing_sessions.model.js +50 -0
- package/dist/db/models/marketing_sessions.model.js.map +1 -0
- package/dist/db/models/marketing_settings.model.d.ts +23 -0
- package/dist/db/models/marketing_settings.model.js +13 -0
- package/dist/db/models/marketing_settings.model.js.map +1 -0
- package/dist/db/models/page_snapshots.model.d.ts +27 -0
- package/dist/db/models/page_snapshots.model.js +32 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -0
- package/dist/db/models/retention.d.ts +6 -0
- package/dist/db/models/retention.js +26 -0
- package/dist/db/models/retention.js.map +1 -0
- package/dist/db/models/website_statistics.model.d.ts +31 -0
- package/dist/db/models/website_statistics.model.js +63 -0
- package/dist/db/models/website_statistics.model.js.map +1 -0
- package/dist/db/models/websites.model.d.ts +26 -0
- package/dist/db/models/websites.model.js +27 -0
- package/dist/db/models/websites.model.js.map +1 -0
- package/dist/db/tables/funnels.table.d.ts +31 -0
- package/dist/db/tables/funnels.table.js +54 -0
- package/dist/db/tables/funnels.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +7 -0
- package/dist/db/tables/index.js +24 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/marketing_events.table.d.ts +15 -0
- package/dist/db/tables/marketing_events.table.js +66 -0
- package/dist/db/tables/marketing_events.table.js.map +1 -0
- package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
- package/dist/db/tables/marketing_sessions.table.js +115 -0
- package/dist/db/tables/marketing_sessions.table.js.map +1 -0
- package/dist/db/tables/marketing_settings.table.d.ts +11 -0
- package/dist/db/tables/marketing_settings.table.js +51 -0
- package/dist/db/tables/marketing_settings.table.js.map +1 -0
- package/dist/db/tables/page_snapshots.table.d.ts +17 -0
- package/dist/db/tables/page_snapshots.table.js +72 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -0
- package/dist/db/tables/website_statistics.table.d.ts +14 -0
- package/dist/db/tables/website_statistics.table.js +60 -0
- package/dist/db/tables/website_statistics.table.js.map +1 -0
- package/dist/db/tables/websites.table.d.ts +13 -0
- package/dist/db/tables/websites.table.js +60 -0
- package/dist/db/tables/websites.table.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +39 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/implementations/dms-marketing/index.d.ts +2 -0
- package/dist/implementations/dms-marketing/index.js +8 -0
- package/dist/implementations/dms-marketing/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/all.d.ts +5 -0
- package/dist/pages/all.js +22 -0
- package/dist/pages/all.js.map +1 -0
- package/dist/pages/campaigns/page.d.ts +9 -0
- package/dist/pages/campaigns/page.js +27 -0
- package/dist/pages/campaigns/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +9 -0
- package/dist/pages/funnels/page.js +27 -0
- package/dist/pages/funnels/page.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/module.d.ts +1 -0
- package/dist/pages/module.js +13 -0
- package/dist/pages/module.js.map +1 -0
- package/dist/pages/overview/page.d.ts +9 -0
- package/dist/pages/overview/page.js +27 -0
- package/dist/pages/overview/page.js.map +1 -0
- package/dist/pages/pages/page.d.ts +9 -0
- package/dist/pages/pages/page.js +27 -0
- package/dist/pages/pages/page.js.map +1 -0
- package/dist/pages/settings/form.d.ts +3 -0
- package/dist/pages/settings/form.js +35 -0
- package/dist/pages/settings/form.js.map +1 -0
- package/dist/pages/settings/page.d.ts +9 -0
- package/dist/pages/settings/page.js +27 -0
- package/dist/pages/settings/page.js.map +1 -0
- package/dist/routes/collect.d.ts +7 -0
- package/dist/routes/collect.js +62 -0
- package/dist/routes/collect.js.map +1 -0
- package/dist/routes/experiments.d.ts +6 -0
- package/dist/routes/experiments.js +63 -0
- package/dist/routes/experiments.js.map +1 -0
- package/dist/routes/funnels.d.ts +16 -0
- package/dist/routes/funnels.js +44 -0
- package/dist/routes/funnels.js.map +1 -0
- package/dist/routes/index.d.ts +8 -0
- package/dist/routes/index.js +25 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/settings.d.ts +8 -0
- package/dist/routes/settings.js +50 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/snapshot.d.ts +10 -0
- package/dist/routes/snapshot.js +147 -0
- package/dist/routes/snapshot.js.map +1 -0
- package/dist/routes/stats.d.ts +44 -0
- package/dist/routes/stats.js +278 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/tracker.d.ts +6 -0
- package/dist/routes/tracker.js +57 -0
- package/dist/routes/tracker.js.map +1 -0
- package/dist/routes/websites.d.ts +13 -0
- package/dist/routes/websites.js +144 -0
- package/dist/routes/websites.js.map +1 -0
- package/dist/services/acquisition.d.ts +15 -0
- package/dist/services/acquisition.js +153 -0
- package/dist/services/acquisition.js.map +1 -0
- package/dist/services/bounded-cache.d.ts +14 -0
- package/dist/services/bounded-cache.js +45 -0
- package/dist/services/bounded-cache.js.map +1 -0
- package/dist/services/collect.d.ts +11 -0
- package/dist/services/collect.js +193 -0
- package/dist/services/collect.js.map +1 -0
- package/dist/services/experiment-definitions.d.ts +4 -0
- package/dist/services/experiment-definitions.js +36 -0
- package/dist/services/experiment-definitions.js.map +1 -0
- package/dist/services/experiments/assign.d.ts +5 -0
- package/dist/services/experiments/assign.js +52 -0
- package/dist/services/experiments/assign.js.map +1 -0
- package/dist/services/experiments/index.d.ts +3 -0
- package/dist/services/experiments/index.js +20 -0
- package/dist/services/experiments/index.js.map +1 -0
- package/dist/services/experiments/results.d.ts +20 -0
- package/dist/services/experiments/results.js +91 -0
- package/dist/services/experiments/results.js.map +1 -0
- package/dist/services/experiments/stats.d.ts +12 -0
- package/dist/services/experiments/stats.js +62 -0
- package/dist/services/experiments/stats.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +15 -0
- package/dist/services/funnels/compute.js +68 -0
- package/dist/services/funnels/compute.js.map +1 -0
- package/dist/services/funnels/index.d.ts +2 -0
- package/dist/services/funnels/index.js +19 -0
- package/dist/services/funnels/index.js.map +1 -0
- package/dist/services/funnels/results.d.ts +14 -0
- package/dist/services/funnels/results.js +72 -0
- package/dist/services/funnels/results.js.map +1 -0
- package/dist/services/geoip.d.ts +3 -0
- package/dist/services/geoip.js +38 -0
- package/dist/services/geoip.js.map +1 -0
- package/dist/services/heatmap.d.ts +10 -0
- package/dist/services/heatmap.js +180 -0
- package/dist/services/heatmap.js.map +1 -0
- package/dist/services/index.d.ts +26 -0
- package/dist/services/index.js +43 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/ingest-guard.d.ts +11 -0
- package/dist/services/ingest-guard.js +44 -0
- package/dist/services/ingest-guard.js.map +1 -0
- package/dist/services/keyed-queue.d.ts +4 -0
- package/dist/services/keyed-queue.js +19 -0
- package/dist/services/keyed-queue.js.map +1 -0
- package/dist/services/origin-guard.d.ts +5 -0
- package/dist/services/origin-guard.js +43 -0
- package/dist/services/origin-guard.js.map +1 -0
- package/dist/services/period.d.ts +2 -0
- package/dist/services/period.js +19 -0
- package/dist/services/period.js.map +1 -0
- package/dist/services/prune.d.ts +7 -0
- package/dist/services/prune.js +49 -0
- package/dist/services/prune.js.map +1 -0
- package/dist/services/query-param.d.ts +1 -0
- package/dist/services/query-param.js +11 -0
- package/dist/services/query-param.js.map +1 -0
- package/dist/services/request-body.d.ts +2 -0
- package/dist/services/request-body.js +41 -0
- package/dist/services/request-body.js.map +1 -0
- package/dist/services/request-meta.d.ts +3 -0
- package/dist/services/request-meta.js +19 -0
- package/dist/services/request-meta.js.map +1 -0
- package/dist/services/rollup-write.d.ts +4 -0
- package/dist/services/rollup-write.js +84 -0
- package/dist/services/rollup-write.js.map +1 -0
- package/dist/services/sessionize.d.ts +23 -0
- package/dist/services/sessionize.js +78 -0
- package/dist/services/sessionize.js.map +1 -0
- package/dist/services/settings.d.ts +6 -0
- package/dist/services/settings.js +113 -0
- package/dist/services/settings.js.map +1 -0
- package/dist/services/snapshots.d.ts +24 -0
- package/dist/services/snapshots.js +139 -0
- package/dist/services/snapshots.js.map +1 -0
- package/dist/services/tenant-funnel.d.ts +3 -0
- package/dist/services/tenant-funnel.js +17 -0
- package/dist/services/tenant-funnel.js.map +1 -0
- package/dist/services/tenant-website.d.ts +5 -0
- package/dist/services/tenant-website.js +34 -0
- package/dist/services/tenant-website.js.map +1 -0
- package/dist/services/url.d.ts +10 -0
- package/dist/services/url.js +81 -0
- package/dist/services/url.js.map +1 -0
- package/dist/services/user-agent.d.ts +2 -0
- package/dist/services/user-agent.js +17 -0
- package/dist/services/user-agent.js.map +1 -0
- package/dist/services/visitor-id.d.ts +1 -0
- package/dist/services/visitor-id.js +19 -0
- package/dist/services/visitor-id.js.map +1 -0
- package/dist/services/website-cache.d.ts +3 -0
- package/dist/services/website-cache.js +22 -0
- package/dist/services/website-cache.js.map +1 -0
- package/dist/types/constants.d.ts +74 -0
- package/dist/types/constants.js +79 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/events.d.ts +114 -0
- package/dist/types/events.js +29 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/settings.d.ts +67 -0
- package/dist/types/settings.js +58 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/snapshots.d.ts +95 -0
- package/dist/types/snapshots.js +41 -0
- package/dist/types/snapshots.js.map +1 -0
- package/dist/types/statistics.d.ts +38 -0
- package/dist/types/statistics.js +33 -0
- package/dist/types/statistics.js.map +1 -0
- package/dist/utils/experiment-type.d.ts +52 -0
- package/dist/utils/experiment-type.js +94 -0
- package/dist/utils/experiment-type.js.map +1 -0
- package/dist/utils/funnel-steps-type.d.ts +42 -0
- package/dist/utils/funnel-steps-type.js +62 -0
- package/dist/utils/funnel-steps-type.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +7 -0
- package/frontend-vue/app/components/CampaignsView.vue +330 -0
- package/frontend-vue/app/components/ExperimentInput.vue +128 -0
- package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
- package/frontend-vue/app/components/FunnelsView.vue +493 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
- package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
- package/frontend-vue/app/components/OverviewView.vue +493 -0
- package/frontend-vue/app/components/PagePreview.vue +915 -0
- package/frontend-vue/app/components/PagesView.vue +378 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
- package/frontend-vue/app/components/TextKpiCard.vue +28 -0
- package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
- package/frontend-vue/app/components/VariationsInput.vue +164 -0
- package/frontend-vue/app/composables/useChildId.ts +13 -0
- package/frontend-vue/app/composables/useExperiment.ts +59 -0
- package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
- package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
- package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
- package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
- package/frontend-vue/app/utils/dmsComponent.ts +18 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
- package/frontend-vue/dms.frontend.ts +35 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
- package/frontend-vue/package.json +30 -0
- package/frontend-vue/pnpm-lock.yaml +5226 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/drafts.test.ts +54 -0
- package/frontend-vue/tests/i18n.test.ts +86 -0
- package/frontend-vue/tests/period.test.ts +35 -0
- package/frontend-vue/tests/registration.test.ts +63 -0
- package/frontend-vue/tests/routes.test.ts +31 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
- package/frontend-vue/vitest.config.ts +20 -0
- package/package.json +91 -0
- package/static/snapshot.js +497 -0
- 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
|
+
]
|