@antelopejs/dms-marketing 0.6.0 → 0.7.0
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/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/websites.d.ts +10 -4
- package/dist/data-api/websites.js +79 -3
- package/dist/data-api/websites.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -0
- package/dist/db/models/page_snapshots.model.js +6 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/websites.model.d.ts +1 -0
- package/dist/db/models/websites.model.js +9 -0
- package/dist/db/models/websites.model.js.map +1 -1
- package/dist/db/tables/page_snapshots.table.d.ts +1 -0
- package/dist/db/tables/page_snapshots.table.js +4 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -1
- package/dist/db/tables/websites.table.d.ts +2 -0
- package/dist/db/tables/websites.table.js +4 -0
- package/dist/db/tables/websites.table.js.map +1 -1
- package/dist/frontend-module.d.ts +6 -0
- package/dist/frontend-module.js +46 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/index.js +3 -11
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +1 -0
- package/dist/pages/acquisition/page.js +9 -4
- package/dist/pages/acquisition/page.js.map +1 -1
- package/dist/pages/blocks.d.ts +11 -2
- package/dist/pages/blocks.js +43 -5
- package/dist/pages/blocks.js.map +1 -1
- package/dist/pages/funnel/page.d.ts +1 -0
- package/dist/pages/funnel/page.js +1 -0
- package/dist/pages/funnel/page.js.map +1 -1
- package/dist/pages/funnels/page.d.ts +1 -0
- package/dist/pages/funnels/page.js +4 -2
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -0
- package/dist/pages/overview/page.js +5 -6
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -0
- package/dist/pages/pages/page.js +1 -0
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/settings/form.js +1 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/websites/page.d.ts +2 -3
- package/dist/pages/websites/page.js +63 -5
- package/dist/pages/websites/page.js.map +1 -1
- package/dist/routes/dashboard-tracking.js +5 -1
- package/dist/routes/dashboard-tracking.js.map +1 -1
- package/dist/routes/funnels.d.ts +2 -0
- package/dist/routes/funnels.js +6 -5
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/public-endpoints.js +1 -1
- package/dist/routes/public-endpoints.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +30 -13
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +10 -4
- package/dist/routes/stats.js +77 -23
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/websites.d.ts +5 -1
- package/dist/routes/websites.js +31 -13
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +1 -0
- package/dist/services/acquisition-report.js +1 -0
- package/dist/services/acquisition-report.js.map +1 -1
- package/dist/services/collect.js +7 -2
- package/dist/services/collect.js.map +1 -1
- package/dist/services/dashboard-tracking.d.ts +7 -14
- package/dist/services/dashboard-tracking.js +42 -26
- package/dist/services/dashboard-tracking.js.map +1 -1
- package/dist/services/funnels-table.d.ts +3 -2
- package/dist/services/funnels-table.js +9 -9
- package/dist/services/funnels-table.js.map +1 -1
- package/dist/services/heatmap.d.ts +6 -0
- package/dist/services/heatmap.js +82 -17
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/prune.js +6 -1
- package/dist/services/prune.js.map +1 -1
- package/dist/services/realtime.d.ts +3 -0
- package/dist/services/realtime.js +49 -0
- package/dist/services/realtime.js.map +1 -0
- package/dist/services/request-body.d.ts +1 -0
- package/dist/services/request-body.js +18 -0
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/sessionize.d.ts +1 -0
- package/dist/services/sessionize.js +9 -0
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/snapshots.d.ts +28 -1
- package/dist/services/snapshots.js +147 -26
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.d.ts +11 -0
- package/dist/services/tenant-website.js +27 -5
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/website-report.d.ts +5 -0
- package/dist/services/website-report.js +13 -1
- package/dist/services/website-report.js.map +1 -1
- package/dist/services/workspace-scope.d.ts +31 -0
- package/dist/services/workspace-scope.js +72 -0
- package/dist/services/workspace-scope.js.map +1 -0
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/constants.js +8 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +25 -25
- package/dist/types/snapshots.d.ts +29 -29
- package/dist/types/snapshots.js +1 -1
- package/dist/types/snapshots.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +13 -13
- package/frontend-vue/app/components/ChannelsCard.vue +19 -2
- package/frontend-vue/app/components/Context.vue +50 -376
- package/frontend-vue/app/components/FunnelBuilder.vue +28 -22
- package/frontend-vue/app/components/FunnelFigure.vue +15 -2
- package/frontend-vue/app/components/FunnelReport.vue +3 -0
- package/frontend-vue/app/components/FunnelTemplates.vue +15 -13
- package/frontend-vue/app/components/InstallGuide.vue +1 -1
- package/frontend-vue/app/components/PagePreview.vue +330 -74
- package/frontend-vue/app/components/PagesExplorer.vue +230 -85
- package/frontend-vue/app/components/TopListTabs.vue +51 -18
- package/frontend-vue/app/components/WebsiteCard.vue +394 -0
- package/frontend-vue/app/components/WebsiteCreate.vue +159 -0
- package/frontend-vue/app/components/WebsitePicker.vue +210 -0
- package/frontend-vue/app/components/WebsiteSettings.vue +539 -0
- package/frontend-vue/app/components/WorkspaceScope.vue +119 -0
- package/frontend-vue/app/composables/useContextWebsite.ts +105 -0
- package/frontend-vue/app/composables/useCopyDraft.ts +47 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +92 -4
- package/frontend-vue/app/composables/useMarketingReads.ts +32 -23
- package/frontend-vue/app/composables/useMarketingRoutes.ts +25 -2
- package/frontend-vue/app/composables/useRecordCrumb.ts +17 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +35 -1
- package/frontend-vue/app/utils/anchor-drift.ts +58 -0
- package/frontend-vue/app/utils/drift-dismissals.ts +20 -0
- package/frontend-vue/app/utils/fill-height.ts +141 -0
- package/frontend-vue/app/utils/format.ts +21 -2
- package/frontend-vue/app/utils/funnel-draft.ts +22 -0
- package/frontend-vue/app/utils/heatmap.ts +17 -0
- package/frontend-vue/app/utils/page-snapshot.ts +75 -0
- package/frontend-vue/app/utils/snapshot-options.ts +148 -0
- package/frontend-vue/app/utils/snapshotScrollRoot.ts +93 -0
- package/frontend-vue/app/utils/website-card.ts +93 -0
- package/frontend-vue/app/utils/website-purge.ts +95 -0
- package/frontend-vue/app/utils/website-settings.ts +121 -0
- package/frontend-vue/app/utils/website-state.ts +93 -0
- package/frontend-vue/app/utils/workspace-scope.ts +51 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +112 -36
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +112 -36
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/anchor-drift.test.ts +59 -0
- package/frontend-vue/tests/build-flags.test.ts +38 -0
- package/frontend-vue/tests/context-bar.test.ts +226 -0
- package/frontend-vue/tests/copy-draft.test.ts +64 -0
- package/frontend-vue/tests/drift-dismissals.test.ts +12 -0
- package/frontend-vue/tests/fill-height.test.ts +110 -0
- package/frontend-vue/tests/format.test.ts +14 -0
- package/frontend-vue/tests/funnel-draft.test.ts +37 -0
- package/frontend-vue/tests/heatmap.test.ts +25 -0
- package/frontend-vue/tests/install-guide.test.ts +25 -0
- package/frontend-vue/tests/marketing-reads.test.ts +20 -6
- package/frontend-vue/tests/page-snapshot.test.ts +62 -0
- package/frontend-vue/tests/record-crumb.test.ts +43 -0
- package/frontend-vue/tests/registration.test.ts +4 -1
- package/frontend-vue/tests/routes.test.ts +21 -0
- package/frontend-vue/tests/save-funnel.test.ts +47 -0
- package/frontend-vue/tests/server-parity.test.ts +3 -0
- package/frontend-vue/tests/snapshot-options.test.ts +148 -0
- package/frontend-vue/tests/snapshot-scroll-root.test.ts +84 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +5 -1
- package/frontend-vue/tests/top-list-tabs.test.ts +158 -0
- package/frontend-vue/tests/website-card.test.ts +119 -0
- package/frontend-vue/tests/website-purge.test.ts +70 -0
- package/frontend-vue/tests/website-settings.test.ts +133 -0
- package/frontend-vue/tests/website-state.test.ts +96 -0
- package/frontend-vue/tests/workspace-scope-control.test.ts +113 -0
- package/frontend-vue/tests/workspace-scope.test.ts +55 -0
- package/package.json +5 -4
- package/skills/dms-marketing/SKILL.md +47 -6
- package/static/snapshot.js +539 -48
- package/static/tracker.js +154 -5
- package/frontend-vue/app/components/WebsitesGrid.vue +0 -812
|
@@ -91,7 +91,22 @@ function stepsKey(steps: MarketingFunnelStep[]): string {
|
|
|
91
91
|
/>
|
|
92
92
|
</template>
|
|
93
93
|
</DmsEmptyState>
|
|
94
|
+
<!-- The blank start comes first: the templates loading after it never
|
|
95
|
+
move it. -->
|
|
94
96
|
<div class="mt-6 grid gap-3 md:grid-cols-3">
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="flex flex-col items-start gap-2 rounded-xl border border-dashed border-default p-4 text-left transition-colors hover:border-primary"
|
|
100
|
+
@click="startFrom(null)"
|
|
101
|
+
>
|
|
102
|
+
<DmsEyebrow :label="t('page.marketing.funnels.empty.blank')" />
|
|
103
|
+
<span class="text-sm font-semibold text-highlighted">
|
|
104
|
+
{{ t('page.marketing.funnels.empty.scratch') }}
|
|
105
|
+
</span>
|
|
106
|
+
<span class="text-xs text-muted">
|
|
107
|
+
{{ t('page.marketing.funnels.empty.scratch_description') }}
|
|
108
|
+
</span>
|
|
109
|
+
</button>
|
|
95
110
|
<template v-if="templates === null && !waiting">
|
|
96
111
|
<USkeleton v-for="index in 2" :key="index" class="h-32 rounded-xl" />
|
|
97
112
|
</template>
|
|
@@ -132,19 +147,6 @@ function stepsKey(steps: MarketingFunnelStep[]): string {
|
|
|
132
147
|
}}
|
|
133
148
|
</span>
|
|
134
149
|
</button>
|
|
135
|
-
<button
|
|
136
|
-
type="button"
|
|
137
|
-
class="flex flex-col items-start gap-2 rounded-xl border border-dashed border-default p-4 text-left transition-colors hover:border-primary"
|
|
138
|
-
@click="startFrom(null)"
|
|
139
|
-
>
|
|
140
|
-
<DmsEyebrow :label="t('page.marketing.funnels.empty.blank')" />
|
|
141
|
-
<span class="text-sm font-semibold text-highlighted">
|
|
142
|
-
{{ t('page.marketing.funnels.empty.scratch') }}
|
|
143
|
-
</span>
|
|
144
|
-
<span class="text-xs text-muted">
|
|
145
|
-
{{ t('page.marketing.funnels.empty.scratch_description') }}
|
|
146
|
-
</span>
|
|
147
|
-
</button>
|
|
148
150
|
</div>
|
|
149
151
|
</section>
|
|
150
152
|
</template>
|
|
@@ -371,7 +371,7 @@ const stepTwoState = computed(() =>
|
|
|
371
371
|
<span class="font-mono">{{ website.domain }}</span>
|
|
372
372
|
{{ t('page.marketing.install.and_subdomains') }}
|
|
373
373
|
<DmsAutoLink
|
|
374
|
-
:to="websitesLink()"
|
|
374
|
+
:to="websitesLink(website.id)"
|
|
375
375
|
class="text-primary hover:underline"
|
|
376
376
|
>
|
|
377
377
|
{{ t('page.marketing.common.edit') }}
|
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
usePageSnapshot,
|
|
17
17
|
type PagePreviewRequest,
|
|
18
18
|
} from '../composables/useMarketingReads'
|
|
19
|
-
import { funnelLink, settingsLink } from '../composables/useMarketingRoutes'
|
|
20
|
-
import { formatNumber, formatShortDate } from '../utils/format'
|
|
21
19
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
funnelLink,
|
|
21
|
+
settingsLink,
|
|
22
|
+
websitesLink,
|
|
23
|
+
} from '../composables/useMarketingRoutes'
|
|
24
|
+
import { formatNumber, formatShortDate } from '../utils/format'
|
|
25
|
+
import { heatmapDataLabel, heatmapResetImpact } from '../utils/heatmap'
|
|
25
26
|
import { heatLegendGradient } from '../utils/heatmapPalette'
|
|
26
27
|
import {
|
|
27
28
|
DEFAULT_RENDER_HEIGHT,
|
|
@@ -32,7 +33,13 @@ import {
|
|
|
32
33
|
type AnchorBox,
|
|
33
34
|
type AnchorRequest,
|
|
34
35
|
} from '../composables/useSnapshotFrame'
|
|
36
|
+
import {
|
|
37
|
+
confirmSnapshotChange,
|
|
38
|
+
snapshotOptionItems,
|
|
39
|
+
} from '../utils/snapshot-options'
|
|
35
40
|
import { rewriteViewportUnits } from '../utils/snapshotViewportUnits'
|
|
41
|
+
import { anchorDrift } from '../utils/anchor-drift'
|
|
42
|
+
import { dismissDrift, driftDismissed } from '../utils/drift-dismissals'
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
45
|
* Click and scroll-depth overlays of one tracked page, over a snapshot of
|
|
@@ -82,8 +89,25 @@ const props = defineProps<{
|
|
|
82
89
|
pageviews?: number
|
|
83
90
|
/** Funnels with a page step on this path. */
|
|
84
91
|
usedIn?: MarketingFunnelListItem[]
|
|
92
|
+
/**
|
|
93
|
+
* Height the card fills, toolbar to bottom, the snapshot scrolling inside
|
|
94
|
+
* it whatever its length (see PagesExplorer). Null or absent: the card
|
|
95
|
+
* grows with the snapshot, as when it is stacked under the list.
|
|
96
|
+
*/
|
|
97
|
+
height?: number | null
|
|
98
|
+
/**
|
|
99
|
+
* Another workspace's dashboard, or `all` (see PagesExplorer): the page is
|
|
100
|
+
* read there, read-only — no reset, no snapshot option — and its capture
|
|
101
|
+
* comes masked from the backend. Absent or empty: the website's own.
|
|
102
|
+
*/
|
|
103
|
+
workspace?: string
|
|
104
|
+
/** Whether any site of that scope keeps snapshots. */
|
|
105
|
+
workspaceSnapshots?: boolean
|
|
85
106
|
}>()
|
|
86
107
|
|
|
108
|
+
/** Read past the viewer's workspace (see `workspace`). */
|
|
109
|
+
const foreign = computed(() => !!props.workspace)
|
|
110
|
+
|
|
87
111
|
/** The site's snapshot options were saved here, as the backend answered
|
|
88
112
|
* them; the parent owns the row. */
|
|
89
113
|
const emit = defineEmits<{
|
|
@@ -129,7 +153,8 @@ const PROBE_VIEWPORT_HEIGHT = 900
|
|
|
129
153
|
const PANE_HEIGHT = 620
|
|
130
154
|
/** Overflow beyond the chosen layout width worth telling the operator about. */
|
|
131
155
|
const OVERFLOW_TOLERANCE = 1.05
|
|
132
|
-
|
|
156
|
+
/** The upload cap of a backend that does not say its own. */
|
|
157
|
+
const DEFAULT_SNAPSHOT_BYTE_CAP = 1024 * 1024
|
|
133
158
|
const LIGHT_CANVAS = '#ffffff'
|
|
134
159
|
const DARK_CANVAS = '#111111'
|
|
135
160
|
|
|
@@ -145,6 +170,7 @@ const previewRequest: PagePreviewRequest = {
|
|
|
145
170
|
websiteId: () => props.website?._id,
|
|
146
171
|
path: () => props.path,
|
|
147
172
|
period: () => props.period,
|
|
173
|
+
workspace: () => props.workspace,
|
|
148
174
|
}
|
|
149
175
|
const heatmapRead = usePageHeatmap(previewRequest)
|
|
150
176
|
const heatmap = computed(() =>
|
|
@@ -265,8 +291,10 @@ const layout = computed<MarketingSnapshotLayout>(
|
|
|
265
291
|
?.layout ?? 'desktop',
|
|
266
292
|
)
|
|
267
293
|
|
|
268
|
-
const snapshotsEnabled = computed(
|
|
269
|
-
|
|
294
|
+
const snapshotsEnabled = computed(() =>
|
|
295
|
+
foreign.value
|
|
296
|
+
? props.workspaceSnapshots === true
|
|
297
|
+
: (props.website?.snapshotsEnabled ?? false),
|
|
270
298
|
)
|
|
271
299
|
|
|
272
300
|
// Not fetched while the site has snapshots off: the option's banner is the
|
|
@@ -277,6 +305,7 @@ const snapshotRead = usePageSnapshot({
|
|
|
277
305
|
layout,
|
|
278
306
|
enabled: snapshotsEnabled,
|
|
279
307
|
maskText: () => props.website?.snapshotMaskText === true,
|
|
308
|
+
workspace: () => props.workspace,
|
|
280
309
|
})
|
|
281
310
|
const isSnapshotAsked = computed(
|
|
282
311
|
() => snapshotsEnabled.value && !!props.website?._id && !!props.path,
|
|
@@ -322,11 +351,37 @@ function mebibytes(bytes: number): string {
|
|
|
322
351
|
|
|
323
352
|
const savingOptions = ref(false)
|
|
324
353
|
|
|
354
|
+
const countSiteHeatmap = useHeatmapCountRead(() => props.website?._id ?? '')
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Whether the change may go ahead: it deletes the site's snapshots and
|
|
358
|
+
* changes what later captures keep, so it is asked first — with the count
|
|
359
|
+
* when there are some (see asksBeforeSnapshotChange). Declining leaves the
|
|
360
|
+
* website as it is, and the menu's checkbox with it.
|
|
361
|
+
*/
|
|
362
|
+
function confirmSnapshotDiscard(
|
|
363
|
+
website: MarketingWebsite,
|
|
364
|
+
patch: MarketingWebsitePatch,
|
|
365
|
+
): Promise<boolean> {
|
|
366
|
+
return confirmSnapshotChange(website, patch, {
|
|
367
|
+
count: async () => {
|
|
368
|
+
savingOptions.value = true
|
|
369
|
+
try {
|
|
370
|
+
return (await countSiteHeatmap()).snapshots
|
|
371
|
+
} finally {
|
|
372
|
+
savingOptions.value = false
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
confirm,
|
|
376
|
+
t,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
325
380
|
async function updateSnapshotOptions(
|
|
326
381
|
patch: MarketingWebsitePatch,
|
|
327
382
|
): Promise<void> {
|
|
328
383
|
const website = props.website
|
|
329
|
-
if (!website) {
|
|
384
|
+
if (!website || !(await confirmSnapshotDiscard(website, patch))) {
|
|
330
385
|
return
|
|
331
386
|
}
|
|
332
387
|
savingOptions.value = true
|
|
@@ -345,7 +400,7 @@ async function updateSnapshotOptions(
|
|
|
345
400
|
// --- Frame geometry ---------------------------------------------------------
|
|
346
401
|
|
|
347
402
|
const frame = ref<HTMLIFrameElement | null>(null)
|
|
348
|
-
const { measuredWidth, measuredHeight, tier, measureAnchors } =
|
|
403
|
+
const { measuredWidth, measuredHeight, tier, measureAnchors, missingAnchors } =
|
|
349
404
|
useSnapshotFrame(frame)
|
|
350
405
|
|
|
351
406
|
const frameKey = computed(() =>
|
|
@@ -417,6 +472,9 @@ const overlayWidth = computed(() =>
|
|
|
417
472
|
* fraction, never a blank one.
|
|
418
473
|
*/
|
|
419
474
|
const anchorBoxes = ref(new Map<string, AnchorBox>())
|
|
475
|
+
/** Keys of the anchors the snapshot has no element for; null until the
|
|
476
|
+
* frame has been measured. */
|
|
477
|
+
const missingAnchorKeys = ref<Set<string> | null>(null)
|
|
420
478
|
|
|
421
479
|
function anchorKey(anchor: AnchorRequest): string {
|
|
422
480
|
return `${anchor.selector}\0${anchor.nth}`
|
|
@@ -467,6 +525,7 @@ async function refreshAnchorBoxes(): Promise<void> {
|
|
|
467
525
|
if (anchorBoxes.value.size > 0) {
|
|
468
526
|
anchorBoxes.value = new Map()
|
|
469
527
|
}
|
|
528
|
+
missingAnchorKeys.value = null
|
|
470
529
|
return
|
|
471
530
|
}
|
|
472
531
|
await nextTick()
|
|
@@ -479,8 +538,23 @@ async function refreshAnchorBoxes(): Promise<void> {
|
|
|
479
538
|
resolved.set(anchorKey(box), box)
|
|
480
539
|
}
|
|
481
540
|
anchorBoxes.value = resolved
|
|
541
|
+
missingAnchorKeys.value = new Set(
|
|
542
|
+
missingAnchors(requestedAnchors.value).map(anchorKey),
|
|
543
|
+
)
|
|
482
544
|
}
|
|
483
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Clicks aiming at elements this snapshot does not have: drawn at their
|
|
548
|
+
* document fraction, and past a threshold a sign the page changed since it
|
|
549
|
+
* was captured (see anchorDrift).
|
|
550
|
+
*/
|
|
551
|
+
const drift = computed(() => {
|
|
552
|
+
const missing = missingAnchorKeys.value
|
|
553
|
+
return missing
|
|
554
|
+
? anchorDrift(points.value, (anchor) => missing.has(anchorKey(anchor)))
|
|
555
|
+
: null
|
|
556
|
+
})
|
|
557
|
+
|
|
484
558
|
watch(
|
|
485
559
|
[requestedAnchors, tier, renderHeight, overlayWidth],
|
|
486
560
|
() => void refreshAnchorBoxes(),
|
|
@@ -547,20 +621,26 @@ const scrolledHeight = computed(() =>
|
|
|
547
621
|
Math.round(renderHeight.value * scale.value),
|
|
548
622
|
)
|
|
549
623
|
|
|
624
|
+
const filled = computed(() => !!props.height)
|
|
625
|
+
|
|
550
626
|
/**
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
627
|
+
* Given a height, the pane takes what the card leaves it, the same for
|
|
628
|
+
* every snapshot: a long page scrolls inside it, a short one ends on the
|
|
629
|
+
* end-of-page band. Without one, the pane grows with the page up to the
|
|
630
|
+
* window's height. The border is counted, or a 2px scroll would remain.
|
|
554
631
|
*/
|
|
555
|
-
const paneStyle = computed(() =>
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
632
|
+
const paneStyle = computed(() =>
|
|
633
|
+
filled.value
|
|
634
|
+
? { height: '100%' }
|
|
635
|
+
: {
|
|
636
|
+
height: `${scrolledHeight.value + 2}px`,
|
|
637
|
+
maxHeight: 'calc(100dvh - 8rem)',
|
|
638
|
+
minHeight: '12rem',
|
|
639
|
+
},
|
|
640
|
+
)
|
|
560
641
|
|
|
561
642
|
// --- Emptiness, explained ---------------------------------------------------
|
|
562
643
|
|
|
563
|
-
|
|
564
644
|
/** Silence has shared causes (collection off, no view sampled) before the
|
|
565
645
|
* per-overlay one; both overlays explain themselves through this. */
|
|
566
646
|
function captureEmptyReason(fallbackKey: string): string {
|
|
@@ -668,10 +748,40 @@ interface StatusStrip {
|
|
|
668
748
|
neutral?: boolean
|
|
669
749
|
icon: string
|
|
670
750
|
title: string
|
|
671
|
-
description
|
|
751
|
+
description?: string
|
|
752
|
+
/** The explanation, behind an info button instead of under the title: a
|
|
753
|
+
* strip that must stay one line above the snapshot. */
|
|
754
|
+
details?: string
|
|
672
755
|
action?: StripAction
|
|
756
|
+
/** A second, quieter action: the setting behind the strip, where it lives. */
|
|
757
|
+
secondary?: StripAction
|
|
758
|
+
/** Closes the strip; the next one that applies, if any, takes its place. */
|
|
759
|
+
dismiss?: () => void
|
|
673
760
|
}
|
|
674
761
|
|
|
762
|
+
/** Where the site's snapshot switch lives: the Websites page's settings,
|
|
763
|
+
* scrolled to it. The menu's toggles are only a shortcut to it. */
|
|
764
|
+
const snapshotSettingsLink = computed(() =>
|
|
765
|
+
props.website && !foreign.value
|
|
766
|
+
? websitesLink(props.website._id, 'snapshots')
|
|
767
|
+
: '',
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
function snapshotSettingsAction(): StripAction | undefined {
|
|
771
|
+
return snapshotSettingsLink.value
|
|
772
|
+
? {
|
|
773
|
+
label: t('page.marketing.pages.preview.snapshot_settings'),
|
|
774
|
+
to: snapshotSettingsLink.value,
|
|
775
|
+
}
|
|
776
|
+
: undefined
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** The drift strip was closed on this page this session (see
|
|
780
|
+
* drift-dismissals). */
|
|
781
|
+
const driftDismissedHere = computed(
|
|
782
|
+
() => !!props.website && driftDismissed(props.website._id, props.path),
|
|
783
|
+
)
|
|
784
|
+
|
|
675
785
|
/**
|
|
676
786
|
* Highest priority first: one strip says what needs attention, instead of
|
|
677
787
|
* every applicable alert stacked above the page. The numbers in the header
|
|
@@ -697,10 +807,14 @@ const statusStrip = computed<StatusStrip | null>(() => {
|
|
|
697
807
|
icon: 'i-ph-camera-slash',
|
|
698
808
|
title: t('page.marketing.pages.preview.snapshots_off'),
|
|
699
809
|
description: t('page.marketing.pages.preview.snapshots_off_hint'),
|
|
700
|
-
action:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
810
|
+
action: foreign.value
|
|
811
|
+
? undefined
|
|
812
|
+
: {
|
|
813
|
+
label: t('page.marketing.pages.preview.enable_snapshots'),
|
|
814
|
+
run: () =>
|
|
815
|
+
void updateSnapshotOptions({ snapshotsEnabled: true }),
|
|
816
|
+
},
|
|
817
|
+
secondary: snapshotSettingsAction(),
|
|
704
818
|
}
|
|
705
819
|
: null,
|
|
706
820
|
noSnapshotYet.value
|
|
@@ -714,6 +828,7 @@ const statusStrip = computed<StatusStrip | null>(() => {
|
|
|
714
828
|
: t('page.marketing.pages.preview.no_snapshot_hint_rate', {
|
|
715
829
|
rate: props.sampleRateLabel,
|
|
716
830
|
}),
|
|
831
|
+
secondary: snapshotSettingsAction(),
|
|
717
832
|
}
|
|
718
833
|
: null,
|
|
719
834
|
snapshot.value && tooHeavy.value
|
|
@@ -723,8 +838,44 @@ const statusStrip = computed<StatusStrip | null>(() => {
|
|
|
723
838
|
title: t('page.marketing.pages.preview.too_heavy'),
|
|
724
839
|
description: t('page.marketing.pages.preview.too_heavy_hint', {
|
|
725
840
|
size: mebibytes(snapshot.value.bytes),
|
|
726
|
-
limit: mebibytes(
|
|
841
|
+
limit: mebibytes(
|
|
842
|
+
snapshot.value.maxBytes ?? DEFAULT_SNAPSHOT_BYTE_CAP,
|
|
843
|
+
),
|
|
844
|
+
}),
|
|
845
|
+
secondary: snapshotSettingsAction(),
|
|
846
|
+
}
|
|
847
|
+
: null,
|
|
848
|
+
overlayMode.value === 'clicks' &&
|
|
849
|
+
drift.value?.significant &&
|
|
850
|
+
!driftDismissedHere.value
|
|
851
|
+
? {
|
|
852
|
+
tone: 'warning',
|
|
853
|
+
icon: 'i-ph-git-diff',
|
|
854
|
+
title: t('page.marketing.pages.preview.drift', {
|
|
855
|
+
share: String(
|
|
856
|
+
formatPercentage(drift.value.share, locale.value, {
|
|
857
|
+
maximumFractionDigits: 0,
|
|
858
|
+
}),
|
|
859
|
+
),
|
|
860
|
+
}),
|
|
861
|
+
// One line: the share and the way out. Why, and what the reset
|
|
862
|
+
// does, wait behind the info button.
|
|
863
|
+
details: t('page.marketing.pages.preview.drift_hint', {
|
|
864
|
+
date: snapshot.value
|
|
865
|
+
? formatShortDate(snapshot.value.capturedAt, locale.value)
|
|
866
|
+
: '',
|
|
727
867
|
}),
|
|
868
|
+
action: foreign.value
|
|
869
|
+
? undefined
|
|
870
|
+
: {
|
|
871
|
+
label: t('page.marketing.pages.reset.page_button'),
|
|
872
|
+
run: () => void resetPageHeatmap(),
|
|
873
|
+
},
|
|
874
|
+
dismiss: () => {
|
|
875
|
+
if (props.website) {
|
|
876
|
+
dismissDrift(props.website._id, props.path)
|
|
877
|
+
}
|
|
878
|
+
},
|
|
728
879
|
}
|
|
729
880
|
: null,
|
|
730
881
|
truncatedState.value
|
|
@@ -788,13 +939,23 @@ const statusLine = computed(() => {
|
|
|
788
939
|
})
|
|
789
940
|
})
|
|
790
941
|
|
|
791
|
-
|
|
792
|
-
|
|
942
|
+
/** Where the snapshot was taken: origin and path, as stored. */
|
|
943
|
+
const capturedUrl = computed(() =>
|
|
944
|
+
snapshot.value
|
|
945
|
+
? (snapshot.value.url ?? `${snapshot.value.origin}${props.path}`)
|
|
946
|
+
: '',
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
const maskLine = computed(() => {
|
|
950
|
+
if (snapshot.value?.masked || foreign.value) {
|
|
951
|
+
return t('page.marketing.pages.preview.workspace_masked')
|
|
952
|
+
}
|
|
953
|
+
return t(
|
|
793
954
|
props.website?.snapshotMaskText
|
|
794
955
|
? 'page.marketing.pages.preview.all_masked'
|
|
795
956
|
: 'page.marketing.pages.preview.fields_masked',
|
|
796
|
-
)
|
|
797
|
-
)
|
|
957
|
+
)
|
|
958
|
+
})
|
|
798
959
|
|
|
799
960
|
// --- Most clicked -----------------------------------------------------------
|
|
800
961
|
|
|
@@ -926,45 +1087,53 @@ watch(
|
|
|
926
1087
|
{ flush: 'post' },
|
|
927
1088
|
)
|
|
928
1089
|
|
|
929
|
-
|
|
1090
|
+
// Another workspace's page is read-only: its options and reset are its own.
|
|
1091
|
+
const moreItems = computed(() =>
|
|
930
1092
|
[
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1093
|
+
[
|
|
1094
|
+
...(foreign.value
|
|
1095
|
+
? []
|
|
1096
|
+
: snapshotOptionItems(
|
|
1097
|
+
props.website,
|
|
1098
|
+
(patch) => void updateSnapshotOptions(patch),
|
|
1099
|
+
t,
|
|
1100
|
+
)),
|
|
1101
|
+
...(snapshotSettingsLink.value
|
|
1102
|
+
? [
|
|
1103
|
+
{
|
|
1104
|
+
label: t('page.marketing.pages.preview.snapshot_settings'),
|
|
1105
|
+
icon: 'i-ph-gear-six',
|
|
1106
|
+
to: snapshotSettingsLink.value,
|
|
1107
|
+
},
|
|
1108
|
+
]
|
|
1109
|
+
: []),
|
|
1110
|
+
],
|
|
1111
|
+
...(externalUrl.value
|
|
1112
|
+
? [
|
|
1113
|
+
[
|
|
1114
|
+
{
|
|
1115
|
+
label: t('page.marketing.pages.preview.open_in_tab'),
|
|
1116
|
+
icon: 'i-ph-arrow-square-out',
|
|
1117
|
+
to: externalUrl.value,
|
|
1118
|
+
target: '_blank',
|
|
1119
|
+
},
|
|
1120
|
+
],
|
|
1121
|
+
]
|
|
1122
|
+
: []),
|
|
1123
|
+
...(foreign.value
|
|
1124
|
+
? []
|
|
1125
|
+
: [
|
|
1126
|
+
[
|
|
1127
|
+
{
|
|
1128
|
+
label: t('page.marketing.pages.reset.page_button'),
|
|
1129
|
+
icon: 'i-ph-eraser',
|
|
1130
|
+
color: 'error' as const,
|
|
1131
|
+
onSelect: () => void resetPageHeatmap(),
|
|
1132
|
+
},
|
|
1133
|
+
],
|
|
1134
|
+
]),
|
|
1135
|
+
].filter((group) => group.length > 0),
|
|
1136
|
+
)
|
|
968
1137
|
|
|
969
1138
|
// --- Surface ----------------------------------------------------------------
|
|
970
1139
|
|
|
@@ -1018,7 +1187,10 @@ const surfaceEmpty = computed(() => {
|
|
|
1018
1187
|
</script>
|
|
1019
1188
|
|
|
1020
1189
|
<template>
|
|
1021
|
-
<section
|
|
1190
|
+
<section
|
|
1191
|
+
class="dms-card flex min-w-0 flex-col overflow-hidden p-0"
|
|
1192
|
+
:style="filled ? { height: `${props.height}px` } : undefined"
|
|
1193
|
+
>
|
|
1022
1194
|
<!-- Toolbar -->
|
|
1023
1195
|
<header
|
|
1024
1196
|
class="flex flex-wrap items-center justify-between gap-3 border-b border-default px-4 py-3"
|
|
@@ -1078,7 +1250,11 @@ const surfaceEmpty = computed(() => {
|
|
|
1078
1250
|
:aria-label="t('page.marketing.pages.preview.layout')"
|
|
1079
1251
|
/>
|
|
1080
1252
|
</span>
|
|
1081
|
-
<UDropdownMenu
|
|
1253
|
+
<UDropdownMenu
|
|
1254
|
+
v-if="moreItems.length > 0"
|
|
1255
|
+
:items="moreItems"
|
|
1256
|
+
:content="{ align: 'end' }"
|
|
1257
|
+
>
|
|
1082
1258
|
<UButton
|
|
1083
1259
|
icon="i-ph-dots-three"
|
|
1084
1260
|
size="xs"
|
|
@@ -1099,9 +1275,29 @@ const surfaceEmpty = computed(() => {
|
|
|
1099
1275
|
:icon="statusStrip.icon"
|
|
1100
1276
|
:title="statusStrip.title"
|
|
1101
1277
|
:description="statusStrip.description"
|
|
1278
|
+
:dismissible="!!statusStrip.dismiss"
|
|
1279
|
+
@dismiss="statusStrip.dismiss?.()"
|
|
1102
1280
|
>
|
|
1103
|
-
<template v-if="statusStrip.
|
|
1281
|
+
<template v-if="statusStrip.details" #description>
|
|
1282
|
+
<UPopover :content="{ align: 'start' }">
|
|
1283
|
+
<UButton
|
|
1284
|
+
icon="i-ph-info"
|
|
1285
|
+
size="xs"
|
|
1286
|
+
color="neutral"
|
|
1287
|
+
variant="ghost"
|
|
1288
|
+
class="-my-1 align-middle"
|
|
1289
|
+
:aria-label="t('page.marketing.pages.preview.strip_details')"
|
|
1290
|
+
/>
|
|
1291
|
+
<template #content>
|
|
1292
|
+
<p class="max-w-sm p-3 text-xs leading-relaxed text-muted">
|
|
1293
|
+
{{ statusStrip.details }}
|
|
1294
|
+
</p>
|
|
1295
|
+
</template>
|
|
1296
|
+
</UPopover>
|
|
1297
|
+
</template>
|
|
1298
|
+
<template v-if="statusStrip.action || statusStrip.secondary" #actions>
|
|
1104
1299
|
<UButton
|
|
1300
|
+
v-if="statusStrip.action"
|
|
1105
1301
|
size="xs"
|
|
1106
1302
|
color="neutral"
|
|
1107
1303
|
variant="outline"
|
|
@@ -1110,6 +1306,15 @@ const surfaceEmpty = computed(() => {
|
|
|
1110
1306
|
:to="statusStrip.action.to"
|
|
1111
1307
|
@click="statusStrip.action.run?.()"
|
|
1112
1308
|
/>
|
|
1309
|
+
<UButton
|
|
1310
|
+
v-if="statusStrip.secondary"
|
|
1311
|
+
size="xs"
|
|
1312
|
+
color="neutral"
|
|
1313
|
+
variant="ghost"
|
|
1314
|
+
:label="statusStrip.secondary.label"
|
|
1315
|
+
:to="statusStrip.secondary.to"
|
|
1316
|
+
@click="statusStrip.secondary.run?.()"
|
|
1317
|
+
/>
|
|
1113
1318
|
</template>
|
|
1114
1319
|
</DmsBanner>
|
|
1115
1320
|
</div>
|
|
@@ -1121,23 +1326,63 @@ const surfaceEmpty = computed(() => {
|
|
|
1121
1326
|
<UIcon name="i-ph-camera" class="size-3.5 text-success" />
|
|
1122
1327
|
{{ statusLine }}
|
|
1123
1328
|
</span>
|
|
1329
|
+
<span
|
|
1330
|
+
class="min-w-0 truncate font-mono text-dimmed"
|
|
1331
|
+
:title="t('page.marketing.pages.preview.captured_from_hint')"
|
|
1332
|
+
>
|
|
1333
|
+
{{
|
|
1334
|
+
t('page.marketing.pages.preview.captured_from', { url: capturedUrl })
|
|
1335
|
+
}}
|
|
1336
|
+
</span>
|
|
1124
1337
|
<span class="text-dimmed">· {{ maskLine }}</span>
|
|
1338
|
+
<UButton
|
|
1339
|
+
v-if="snapshotSettingsLink"
|
|
1340
|
+
:to="snapshotSettingsLink"
|
|
1341
|
+
size="xs"
|
|
1342
|
+
color="neutral"
|
|
1343
|
+
variant="link"
|
|
1344
|
+
icon="i-ph-gear-six"
|
|
1345
|
+
class="ms-auto"
|
|
1346
|
+
:label="t('page.marketing.pages.preview.snapshot_settings')"
|
|
1347
|
+
/>
|
|
1348
|
+
</p>
|
|
1349
|
+
<!-- The status line's place while the capture is read, at its height (a
|
|
1350
|
+
text-xs line): arriving in a free row, it pushed the pane down. -->
|
|
1351
|
+
<p
|
|
1352
|
+
v-else-if="!snapshotSettled"
|
|
1353
|
+
class="flex items-center border-b border-default px-4 py-2"
|
|
1354
|
+
aria-hidden="true"
|
|
1355
|
+
>
|
|
1356
|
+
<USkeleton class="h-4 w-72 max-w-full" />
|
|
1125
1357
|
</p>
|
|
1126
1358
|
|
|
1127
|
-
<div
|
|
1128
|
-
|
|
1359
|
+
<div
|
|
1360
|
+
class="grid min-h-0 xl:grid-cols-[minmax(0,1fr)_15rem]"
|
|
1361
|
+
:class="filled ? 'flex-1 xl:grid-rows-[minmax(0,1fr)]' : ''"
|
|
1362
|
+
>
|
|
1363
|
+
<div class="flex min-h-0 min-w-0 flex-col p-3">
|
|
1129
1364
|
<USkeleton
|
|
1130
1365
|
v-if="surface === 'loading'"
|
|
1131
1366
|
class="w-full rounded-lg"
|
|
1132
|
-
:
|
|
1367
|
+
:class="filled ? 'min-h-0 flex-1' : ''"
|
|
1368
|
+
:style="filled ? undefined : { height: `${PANE_HEIGHT}px` }"
|
|
1133
1369
|
/>
|
|
1134
1370
|
|
|
1135
1371
|
<!-- Kept mounted across refetches: tearing it down would re-render the
|
|
1136
1372
|
snapshot — and re-measure it — on every period change. -->
|
|
1137
|
-
<div
|
|
1373
|
+
<div
|
|
1374
|
+
v-else-if="surface === 'frame'"
|
|
1375
|
+
class="relative"
|
|
1376
|
+
:class="filled ? 'min-h-0 flex-1' : ''"
|
|
1377
|
+
>
|
|
1138
1378
|
<div
|
|
1139
1379
|
ref="container"
|
|
1140
1380
|
class="overflow-x-hidden overflow-y-scroll rounded-lg border border-default bg-elevated"
|
|
1381
|
+
:class="
|
|
1382
|
+
filled
|
|
1383
|
+
? 'bg-[repeating-linear-gradient(135deg,transparent_0_8px,var(--ui-border)_8px_9px)]'
|
|
1384
|
+
: ''
|
|
1385
|
+
"
|
|
1141
1386
|
:style="paneStyle"
|
|
1142
1387
|
>
|
|
1143
1388
|
<!-- A layout narrower than the pane keeps its width, centred. -->
|
|
@@ -1220,6 +1465,16 @@ const surfaceEmpty = computed(() => {
|
|
|
1220
1465
|
:fold-ratio="scrollFoldRatio"
|
|
1221
1466
|
/>
|
|
1222
1467
|
</div>
|
|
1468
|
+
<!-- Where the captured page stops: whatever of the pane is left
|
|
1469
|
+
below is not page, and must not read as a blank part of it. -->
|
|
1470
|
+
<p
|
|
1471
|
+
v-if="filled"
|
|
1472
|
+
class="mx-auto flex items-center justify-center gap-1.5 border-t border-dashed border-accented py-2 text-[11px] text-dimmed"
|
|
1473
|
+
:style="{ width: `${surfaceWidth}px` }"
|
|
1474
|
+
>
|
|
1475
|
+
<UIcon name="i-ph-arrow-line-down" class="size-3.5" />
|
|
1476
|
+
{{ t('page.marketing.pages.preview.end_of_page') }}
|
|
1477
|
+
</p>
|
|
1223
1478
|
</div>
|
|
1224
1479
|
|
|
1225
1480
|
<!-- Outside the scrollport on purpose: inside, it is laid out at the
|
|
@@ -1261,6 +1516,7 @@ const surfaceEmpty = computed(() => {
|
|
|
1261
1516
|
|
|
1262
1517
|
<aside
|
|
1263
1518
|
class="flex flex-col gap-4 border-t border-default p-4 xl:border-t-0 xl:border-l"
|
|
1519
|
+
:class="filled ? 'min-h-0 overflow-y-auto' : ''"
|
|
1264
1520
|
>
|
|
1265
1521
|
<template v-if="overlayMode === 'clicks'">
|
|
1266
1522
|
<div class="flex flex-col gap-1.5">
|