@aglyn/plugins-marketing 1.0.0-beta.143

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 (183) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +56 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
  8. package/src/lib/components/announcement-bar-card.component.js +292 -0
  9. package/src/lib/components/announcement-bar-card.component.js.map +1 -0
  10. package/src/lib/components/campaign-composer.d.ts +84 -0
  11. package/src/lib/components/campaign-composer.js +1643 -0
  12. package/src/lib/components/campaign-composer.js.map +1 -0
  13. package/src/lib/components/campaign-conversions-card.d.ts +22 -0
  14. package/src/lib/components/campaign-conversions-card.js +642 -0
  15. package/src/lib/components/campaign-conversions-card.js.map +1 -0
  16. package/src/lib/components/campaign-detail-card.d.ts +55 -0
  17. package/src/lib/components/campaign-detail-card.js +771 -0
  18. package/src/lib/components/campaign-detail-card.js.map +1 -0
  19. package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
  20. package/src/lib/components/campaign-edit-drawer.js +252 -0
  21. package/src/lib/components/campaign-edit-drawer.js.map +1 -0
  22. package/src/lib/components/campaign-email-zones.d.ts +111 -0
  23. package/src/lib/components/campaign-email-zones.js +54 -0
  24. package/src/lib/components/campaign-email-zones.js.map +1 -0
  25. package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
  26. package/src/lib/components/campaign-glance-card.component.js +151 -0
  27. package/src/lib/components/campaign-glance-card.component.js.map +1 -0
  28. package/src/lib/components/campaign-members-section.d.ts +87 -0
  29. package/src/lib/components/campaign-members-section.js +499 -0
  30. package/src/lib/components/campaign-members-section.js.map +1 -0
  31. package/src/lib/components/campaign-reach-sections.d.ts +137 -0
  32. package/src/lib/components/campaign-reach-sections.js +694 -0
  33. package/src/lib/components/campaign-reach-sections.js.map +1 -0
  34. package/src/lib/components/campaign-report-card.d.ts +91 -0
  35. package/src/lib/components/campaign-report-card.js +667 -0
  36. package/src/lib/components/campaign-report-card.js.map +1 -0
  37. package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
  38. package/src/lib/components/campaign-test-send-drawer.js +238 -0
  39. package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
  40. package/src/lib/components/campaigns-card.d.ts +31 -0
  41. package/src/lib/components/campaigns-card.js +667 -0
  42. package/src/lib/components/campaigns-card.js.map +1 -0
  43. package/src/lib/components/conversion-attribution.component.d.ts +62 -0
  44. package/src/lib/components/conversion-attribution.component.js +127 -0
  45. package/src/lib/components/conversion-attribution.component.js.map +1 -0
  46. package/src/lib/components/email-compose-card.d.ts +12 -0
  47. package/src/lib/components/email-compose-card.js +157 -0
  48. package/src/lib/components/email-compose-card.js.map +1 -0
  49. package/src/lib/components/email-detail.d.ts +40 -0
  50. package/src/lib/components/email-detail.js +1180 -0
  51. package/src/lib/components/email-detail.js.map +1 -0
  52. package/src/lib/components/email-edit-drawer.d.ts +54 -0
  53. package/src/lib/components/email-edit-drawer.js +172 -0
  54. package/src/lib/components/email-edit-drawer.js.map +1 -0
  55. package/src/lib/components/email-messages-widget.d.ts +22 -0
  56. package/src/lib/components/email-messages-widget.js +49 -0
  57. package/src/lib/components/email-messages-widget.js.map +1 -0
  58. package/src/lib/components/email-recipients-card.d.ts +50 -0
  59. package/src/lib/components/email-recipients-card.js +338 -0
  60. package/src/lib/components/email-recipients-card.js.map +1 -0
  61. package/src/lib/components/emails-list-card.d.ts +36 -0
  62. package/src/lib/components/emails-list-card.js +544 -0
  63. package/src/lib/components/emails-list-card.js.map +1 -0
  64. package/src/lib/components/experiment-zones.d.ts +78 -0
  65. package/src/lib/components/experiment-zones.js +65 -0
  66. package/src/lib/components/experiment-zones.js.map +1 -0
  67. package/src/lib/components/host-experiments-card.component.d.ts +17 -0
  68. package/src/lib/components/host-experiments-card.component.js +887 -0
  69. package/src/lib/components/host-experiments-card.component.js.map +1 -0
  70. package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
  71. package/src/lib/components/host-marketing-summary-card.component.js +161 -0
  72. package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
  73. package/src/lib/components/host-overlays-card.component.d.ts +19 -0
  74. package/src/lib/components/host-overlays-card.component.js +783 -0
  75. package/src/lib/components/host-overlays-card.component.js.map +1 -0
  76. package/src/lib/components/marketing-console-page.d.ts +28 -0
  77. package/src/lib/components/marketing-console-page.js +166 -0
  78. package/src/lib/components/marketing-console-page.js.map +1 -0
  79. package/src/lib/components/marketing-console-sections.d.ts +34 -0
  80. package/src/lib/components/marketing-console-sections.js +77 -0
  81. package/src/lib/components/marketing-console-sections.js.map +1 -0
  82. package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
  83. package/src/lib/components/overlay-stats-row.component.js +78 -0
  84. package/src/lib/components/overlay-stats-row.component.js.map +1 -0
  85. package/src/lib/components/popup-card.component.d.ts +18 -0
  86. package/src/lib/components/popup-card.component.js +462 -0
  87. package/src/lib/components/popup-card.component.js.map +1 -0
  88. package/src/lib/components/record-attribution-widget.d.ts +35 -0
  89. package/src/lib/components/record-attribution-widget.js +33 -0
  90. package/src/lib/components/record-attribution-widget.js.map +1 -0
  91. package/src/lib/components/site-runtime.d.ts +9 -0
  92. package/src/lib/components/site-runtime.js +1081 -0
  93. package/src/lib/components/site-runtime.js.map +1 -0
  94. package/src/lib/components/use-campaign-send-api.d.ts +35 -0
  95. package/src/lib/components/use-campaign-send-api.js +99 -0
  96. package/src/lib/components/use-campaign-send-api.js.map +1 -0
  97. package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
  98. package/src/lib/components/use-campaign-topic-options.js +44 -0
  99. package/src/lib/components/use-campaign-topic-options.js.map +1 -0
  100. package/src/lib/components/use-emails-hub-path.d.ts +39 -0
  101. package/src/lib/components/use-emails-hub-path.js +70 -0
  102. package/src/lib/components/use-emails-hub-path.js.map +1 -0
  103. package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
  104. package/src/lib/components/use-marketing-hub-path.js +50 -0
  105. package/src/lib/components/use-marketing-hub-path.js.map +1 -0
  106. package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
  107. package/src/lib/components/use-overlay-copy-editor.js +110 -0
  108. package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
  109. package/src/lib/constants/bundle-common.d.ts +8 -0
  110. package/src/lib/constants/bundle-common.js +9 -0
  111. package/src/lib/constants/bundle-common.js.map +1 -0
  112. package/src/lib/model/campaign-membership-figures.d.ts +109 -0
  113. package/src/lib/model/campaign-membership-figures.js +113 -0
  114. package/src/lib/model/campaign-membership-figures.js.map +1 -0
  115. package/src/lib/model/compile-client-automations.d.ts +65 -0
  116. package/src/lib/model/compile-client-automations.js +95 -0
  117. package/src/lib/model/compile-client-automations.js.map +1 -0
  118. package/src/lib/model/experiments.d.ts +130 -0
  119. package/src/lib/model/experiments.js +186 -0
  120. package/src/lib/model/experiments.js.map +1 -0
  121. package/src/lib/model/index.d.ts +26 -0
  122. package/src/lib/model/index.js +26 -0
  123. package/src/lib/model/index.js.map +1 -0
  124. package/src/lib/model/marketing-record-routes.d.ts +6 -0
  125. package/src/lib/model/marketing-record-routes.js +46 -0
  126. package/src/lib/model/marketing-record-routes.js.map +1 -0
  127. package/src/lib/model/overlay-copy.d.ts +62 -0
  128. package/src/lib/model/overlay-copy.js +78 -0
  129. package/src/lib/model/overlay-copy.js.map +1 -0
  130. package/src/lib/model/overlays.d.ts +113 -0
  131. package/src/lib/model/overlays.js +93 -0
  132. package/src/lib/model/overlays.js.map +1 -0
  133. package/src/lib/model/site-contract.d.ts +100 -0
  134. package/src/lib/model/site-contract.js +23 -0
  135. package/src/lib/model/site-contract.js.map +1 -0
  136. package/src/lib/plugin.d.ts +27 -0
  137. package/src/lib/plugin.js +168 -0
  138. package/src/lib/plugin.js.map +1 -0
  139. package/src/lib/preview-props.d.ts +31 -0
  140. package/src/lib/preview-props.js +54 -0
  141. package/src/lib/preview-props.js.map +1 -0
  142. package/src/lib/server/campaign-manage.d.ts +62 -0
  143. package/src/lib/server/campaign-manage.js +829 -0
  144. package/src/lib/server/campaign-manage.js.map +1 -0
  145. package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
  146. package/src/lib/server/campaign-process-scheduled.js +198 -0
  147. package/src/lib/server/campaign-process-scheduled.js.map +1 -0
  148. package/src/lib/server/campaign-recipients.d.ts +55 -0
  149. package/src/lib/server/campaign-recipients.js +219 -0
  150. package/src/lib/server/campaign-recipients.js.map +1 -0
  151. package/src/lib/server/campaign-send.d.ts +487 -0
  152. package/src/lib/server/campaign-send.js +3157 -0
  153. package/src/lib/server/campaign-send.js.map +1 -0
  154. package/src/lib/server/email-campaign-reach.d.ts +174 -0
  155. package/src/lib/server/email-campaign-reach.js +254 -0
  156. package/src/lib/server/email-campaign-reach.js.map +1 -0
  157. package/src/lib/server/email-events.d.ts +27 -0
  158. package/src/lib/server/email-events.js +714 -0
  159. package/src/lib/server/email-events.js.map +1 -0
  160. package/src/lib/server/get-client-automations.d.ts +60 -0
  161. package/src/lib/server/get-client-automations.js +87 -0
  162. package/src/lib/server/get-client-automations.js.map +1 -0
  163. package/src/lib/server/get-overlays.d.ts +28 -0
  164. package/src/lib/server/get-overlays.js +65 -0
  165. package/src/lib/server/get-overlays.js.map +1 -0
  166. package/src/lib/server/get-screen-experiments.d.ts +42 -0
  167. package/src/lib/server/get-screen-experiments.js +82 -0
  168. package/src/lib/server/get-screen-experiments.js.map +1 -0
  169. package/src/lib/server/lists-materialize.d.ts +18 -0
  170. package/src/lib/server/lists-materialize.js +130 -0
  171. package/src/lib/server/lists-materialize.js.map +1 -0
  172. package/src/lib/server/marketing-figures.d.ts +37 -0
  173. package/src/lib/server/marketing-figures.js +248 -0
  174. package/src/lib/server/marketing-figures.js.map +1 -0
  175. package/src/lib/server/site-page-enricher.d.ts +26 -0
  176. package/src/lib/server/site-page-enricher.js +210 -0
  177. package/src/lib/server/site-page-enricher.js.map +1 -0
  178. package/src/lib/server.d.ts +20 -0
  179. package/src/lib/server.js +152 -0
  180. package/src/lib/server.js.map +1 -0
  181. package/src/lib/site.d.ts +22 -0
  182. package/src/lib/site.js +48 -0
  183. package/src/lib/site.js.map +1 -0
@@ -0,0 +1,1081 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import * as Aglyn from "@aglyn/aglyn";
20
+ import { sendAnalyticsBeacon } from "@aglyn/aglyn/app-utils/analytics-beacon";
21
+ import { trackAuthoredEvent, trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
22
+ import { campaignTouchField } from "@aglyn/aglyn/app-utils/campaign-touch";
23
+ import { useEffect, useRef, useState } from "react";
24
+ import * as MarketingModel from "../model/index.js";
25
+ /**
26
+ * Overlay metrics beacon (AGL-200): fire-and-forget, never blocks UX.
27
+ * With an `overlayId` (marketing-hub docs, AGL-271) the collector also
28
+ * increments that overlay's own stats counters.
29
+ */ function sendOverlayBeacon(hostId, overlay, overlayId) {
30
+ if (!hostId) return;
31
+ // Best-effort, and refused outright from a non-production build or a browser
32
+ // marked ours — the same gate the pageview beacon passes. The GA mirror
33
+ // below is NOT gated here: `trackEvent` carries the taxonomy's own
34
+ // environment check and stamps `traffic_type` rather than dropping the hit.
35
+ sendAnalyticsBeacon(_extends({
36
+ hostId,
37
+ overlay
38
+ }, overlayId ? {
39
+ overlayId
40
+ } : {}));
41
+ // GA mirror (wave v8): sites with Analytics configured see overlay
42
+ // engagement in their own property; no-op without gtag.
43
+ //
44
+ // Through the taxonomy since AGL-1591 rather than raw gtag. `trackEvent`
45
+ // already swallows everything and never throws, so the try/catch this used
46
+ // to need is gone rather than kept as decoration.
47
+ trackEvent('aglyn_overlay', _extends({
48
+ overlay_action: overlay
49
+ }, overlayId ? {
50
+ overlay_id: overlayId
51
+ } : {}));
52
+ }
53
+ /**
54
+ * Site-wide announcement bar (AGL-195). Dismissal is per content-hash in
55
+ * localStorage (no cookies): editing the text re-shows the bar for
56
+ * everyone who hid the old one.
57
+ */ /**
58
+ * Experiments runner (AGL-253): deterministic per-visitor assignment for
59
+ * the screen's experiments. The first active experiment wins (one per
60
+ * screen); its variant tree — composed server-side per variant version —
61
+ * swaps in via canvas.setNodes on hydration. Exposures beacon on
62
+ * assignment; the conversion listener matches the experiment's goal and
63
+ * beacons once per pageview.
64
+ */ function ExperimentsRunner(props) {
65
+ var _experiments_;
66
+ const { hostId, experiments, consentHost } = props;
67
+ useEffect(()=>{
68
+ var _ref;
69
+ var _experiment_goal;
70
+ if (!hostId || !experiments.length) return undefined;
71
+ const experiment = experiments[0];
72
+ // Visitor-id persistence honors the consent gate (AGL-1498): without an
73
+ // analytics yes on a consent-gated site, the id lives in sessionStorage —
74
+ // variants stay stable within the visit, and the indefinite identifier
75
+ // is not kept. The id never leaves the browser either way (the beacons
76
+ // below carry no visitor id), which is why this is a persistence
77
+ // downgrade rather than a kill switch on experiments.
78
+ const persistence = Aglyn.resolveVisitorIdPersistence(consentHost, Aglyn.readStoredVisitorConsent(hostId));
79
+ let visitorId;
80
+ try {
81
+ var _ref1, _ref2, _storage_getItem;
82
+ const storage = persistence === 'local' ? localStorage : sessionStorage;
83
+ // Read across BOTH stores so the assignment survives a consent move:
84
+ // a session id promoted to local (consent granted mid-visit) and a
85
+ // legacy local id migrating down to session both keep their variant.
86
+ visitorId = (_ref1 = (_ref2 = (_storage_getItem = storage.getItem(Aglyn.VISITOR_ID_STORAGE_KEY)) != null ? _storage_getItem : sessionStorage.getItem(Aglyn.VISITOR_ID_STORAGE_KEY)) != null ? _ref2 : localStorage.getItem(Aglyn.VISITOR_ID_STORAGE_KEY)) != null ? _ref1 : '';
87
+ if (!visitorId) {
88
+ visitorId = `v-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
89
+ }
90
+ storage.setItem(Aglyn.VISITOR_ID_STORAGE_KEY, visitorId);
91
+ // A pre-consent-tool id may still sit in localStorage from before the
92
+ // gate existed; while un-consented it must not persist.
93
+ if (persistence === 'session') {
94
+ localStorage.removeItem(Aglyn.VISITOR_ID_STORAGE_KEY);
95
+ }
96
+ } catch (unused) {
97
+ return undefined // No storage (privacy mode) — serve the default.
98
+ ;
99
+ }
100
+ const variant = MarketingModel.assignExperimentVariant(// Wire-typed props (AGL-418); the plugin narrows its own enum.
101
+ _extends({
102
+ status: experiment.status,
103
+ variants: experiment.variants
104
+ }, experiment.winnerVariantId ? {
105
+ winnerVariantId: experiment.winnerVariantId
106
+ } : {}, experiment.endAtMs ? {
107
+ endAtMs: experiment.endAtMs
108
+ } : {}), experiment.id, visitorId);
109
+ if (!variant) return undefined;
110
+ const payload = experiment.payloads[variant.id];
111
+ if (payload) Aglyn.canvas.setNodes(payload);
112
+ const beacon = (kind)=>{
113
+ void fetch('/api/experiments/track', {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json'
117
+ },
118
+ body: JSON.stringify({
119
+ hostId,
120
+ experimentId: experiment.id,
121
+ variantId: variant.id,
122
+ kind
123
+ }),
124
+ keepalive: true
125
+ }).catch(()=>undefined);
126
+ // GA mirror (wave v8): exposure/conversion events land in the
127
+ // site's own Analytics property when configured. Through the taxonomy
128
+ // since AGL-1591 — which also reserves the name against an authored
129
+ // `trackGaEvent` step, so nothing hand-written can add itself to the
130
+ // counts that decide which variant wins.
131
+ trackEvent('aglyn_experiment', {
132
+ experiment_id: experiment.id,
133
+ variant_id: variant.id,
134
+ experiment_action: kind
135
+ });
136
+ };
137
+ // Finished experiments serve the winner without counting stats.
138
+ if (experiment.status !== 'running') return undefined;
139
+ beacon('exposure');
140
+ // Goal listener — client-observable goals only; server-side goals
141
+ // (bookings etc.) count through their own pipelines in a follow-up.
142
+ const cleanups = [];
143
+ let converted = false;
144
+ const convert = ()=>{
145
+ if (converted) return;
146
+ converted = true;
147
+ beacon('conversion');
148
+ };
149
+ const goal = (_ref = (_experiment_goal = experiment.goal) == null ? void 0 : _experiment_goal.event) != null ? _ref : 'formSubmission';
150
+ if (goal === 'formSubmission') {
151
+ const onSubmit = ()=>convert();
152
+ document.addEventListener('submit', onSubmit, true);
153
+ cleanups.push(()=>document.removeEventListener('submit', onSubmit, true));
154
+ } else if (goal === 'elementClick') {
155
+ var _experiment_goal_filter, _experiment_goal1;
156
+ const selector = ((_experiment_goal1 = experiment.goal) == null ? void 0 : (_experiment_goal_filter = _experiment_goal1.filter) == null ? void 0 : _experiment_goal_filter.trim()) || 'a, button';
157
+ const onClick = (event)=>{
158
+ var _target_closest;
159
+ const target = event.target;
160
+ if (target == null ? void 0 : (_target_closest = target.closest) == null ? void 0 : _target_closest.call(target, selector)) convert();
161
+ };
162
+ document.addEventListener('click', onClick, true);
163
+ cleanups.push(()=>document.removeEventListener('click', onClick, true));
164
+ } else if (goal === 'scrollDepth') {
165
+ const onScroll = ()=>{
166
+ const doc = document.documentElement;
167
+ const max = doc.scrollHeight - window.innerHeight;
168
+ if (max <= 0 || window.scrollY / max * 100 >= 50) {
169
+ convert();
170
+ window.removeEventListener('scroll', onScroll);
171
+ }
172
+ };
173
+ window.addEventListener('scroll', onScroll, {
174
+ passive: true
175
+ });
176
+ cleanups.push(()=>window.removeEventListener('scroll', onScroll));
177
+ } else if (goal === 'timeOnPage') {
178
+ const timer = setTimeout(convert, 30000);
179
+ cleanups.push(()=>clearTimeout(timer));
180
+ }
181
+ return ()=>{
182
+ for (const cleanup of cleanups)cleanup();
183
+ };
184
+ // Experiments are static per pageview (ISR props).
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [
187
+ hostId,
188
+ experiments.length && ((_experiments_ = experiments[0]) == null ? void 0 : _experiments_.id)
189
+ ]);
190
+ return null;
191
+ }
192
+ /**
193
+ * Site-event automations engine (AGL-256/257): arms the page's triggers
194
+ * (scroll depth, element visibility/clicks, exit intent, dwell time),
195
+ * runs each fired action's CLIENT steps in the browser, and dispatches
196
+ * actions with server steps to /api/events/dispatch. Every trigger fires
197
+ * at most once per pageview.
198
+ */ function AutomationsEngine(props) {
199
+ const { hostId, automations, screens, onShowOverlay } = props;
200
+ const showOverlayRef = useRef(onShowOverlay);
201
+ showOverlayRef.current = onShowOverlay;
202
+ useEffect(()=>{
203
+ if (!hostId || !automations.length) return undefined;
204
+ const fired = new Set();
205
+ const cleanups = [];
206
+ const showToast = (message, severity)=>{
207
+ const toast = document.createElement('div');
208
+ toast.textContent = message;
209
+ toast.setAttribute('role', 'status');
210
+ Object.assign(toast.style, {
211
+ position: 'fixed',
212
+ bottom: '16px',
213
+ left: '50%',
214
+ transform: 'translateX(-50%)',
215
+ zIndex: '2147483000',
216
+ padding: '10px 16px',
217
+ borderRadius: '8px',
218
+ fontSize: '14px',
219
+ fontFamily: 'system-ui, sans-serif',
220
+ color: '#fff',
221
+ backgroundColor: severity === 'error' ? '#c62828' : severity === 'warning' ? '#e65100' : severity === 'success' ? '#2e7d32' : 'rgba(0, 0, 0, 0.85)',
222
+ maxWidth: '90vw'
223
+ });
224
+ document.body.appendChild(toast);
225
+ setTimeout(()=>toast.remove(), 6000);
226
+ };
227
+ const runClientSteps = (automation)=>{
228
+ for (const step of automation.steps){
229
+ try {
230
+ if (step.type === 'addClass' || step.type === 'removeClass' || step.type === 'toggleClass') {
231
+ // Expand the raw-id selector to also match the layout-composed
232
+ // live id (AGL-573); non-leaf selectors are unchanged.
233
+ const targets = document.querySelectorAll(Aglyn.expandLeafSelector(step.selector));
234
+ targets.forEach((element)=>{
235
+ if (step.type === 'addClass') {
236
+ element.classList.add(step.className);
237
+ } else if (step.type === 'removeClass') {
238
+ element.classList.remove(step.className);
239
+ } else {
240
+ element.classList.toggle(step.className);
241
+ }
242
+ });
243
+ } else if (step.type === 'setAttribute' || step.type === 'removeAttribute') {
244
+ /*
245
+ The semantics half of a hand-built disclosure (AGL-2546).
246
+
247
+ Deliberately in THIS loop rather than behind a guard of its own:
248
+ it then inherits exactly the gating the visibility steps have,
249
+ including which surfaces mount the runtime at all. A separate
250
+ path reaching `document.querySelector` directly is how this
251
+ would come to run somewhere `showElement` does not.
252
+
253
+ The name is re-checked here and not only in the editor. The
254
+ editor is a convenience; a step can reach this runtime from a
255
+ document written before the allowlist existed, or by any route
256
+ that does not pass through the console UI.
257
+
258
+ A rejected name is a NO-OP, not a throw — the `catch` around
259
+ this loop would otherwise let one bad attribute name silently
260
+ drop every step after it in the same automation.
261
+ */ if (Aglyn.isInteractionAttributeAllowed(step.name)) {
262
+ const name = step.name.trim().toLowerCase();
263
+ const targets = document.querySelectorAll(Aglyn.expandLeafSelector(step.selector));
264
+ targets.forEach((element)=>{
265
+ if (step.type === 'setAttribute') {
266
+ var _step_value;
267
+ element.setAttribute(name, (_step_value = step.value) != null ? _step_value : '');
268
+ } else {
269
+ element.removeAttribute(name);
270
+ }
271
+ });
272
+ }
273
+ } else if (step.type === 'scrollTo') {
274
+ // One target, the first one on screen (AGL-2867). A target that
275
+ // is missing, deleted or hidden makes the step a no-op, and the
276
+ // steps after it in this automation still run.
277
+ Aglyn.runScrollToStep(Aglyn.expandLeafSelector(step.selector), {
278
+ behavior: step.behavior,
279
+ offsetPx: step.offsetPx
280
+ });
281
+ } else if (step.type === 'playVideo') {
282
+ // The Video element's own poster press, sent to the element: the
283
+ // player loads only now, exactly as it would for a click on the
284
+ // poster, and under the same consent rules.
285
+ Aglyn.runPlayVideoStep(Aglyn.expandLeafSelector(step.selector));
286
+ } else if (step.type === 'stickyNav') {
287
+ var _step_selector;
288
+ const target = document.querySelector(Aglyn.expandLeafSelector(((_step_selector = step.selector) == null ? void 0 : _step_selector.trim()) || 'header, nav'));
289
+ if (target instanceof HTMLElement) {
290
+ target.style.position = 'sticky';
291
+ target.style.top = '0';
292
+ target.style.zIndex = '1100';
293
+ }
294
+ } else if (step.type === 'showHtml') {
295
+ const container = document.createElement('div');
296
+ // Sanitize (AGL-504): unlike runJs, showHtml is NOT dropped by the
297
+ // JS entitlement gate, so raw innerHTML would let any automations-
298
+ // tier author execute event-handler attributes (<img onerror>).
299
+ //
300
+ // ONE policy for the surface (AGL-2486). AGL-1901 moved rich text
301
+ // and Custom HTML onto `sanitizeAuthorHtml`; this call was the
302
+ // last raw DOMPurify left on a published page, and two sanitizers
303
+ // with different rules on one surface is worse than the weaker of
304
+ // them — a reviewer reads the strict one and assumes it covers
305
+ // both. It did not: DOMPurify filters NO CSS, so every `style`
306
+ // attribute arriving here reached the visitor unexamined while
307
+ // the Custom HTML block beside it went through `sanitizeAuthorCss`.
308
+ //
309
+ // Nothing this used to refuse is now allowed. `sanitizeAuthorHtml`
310
+ // is a strict SUBSET of the DOMPurify config it replaces, and that
311
+ // is measured rather than asserted: `rich-text-hydration.spec.tsx`
312
+ // holds this exact former config as `referenceShowHtmlPurify` and
313
+ // runs the shared corpus through both. So the change is only in
314
+ // what it ADDITIONALLY refuses:
315
+ //
316
+ // - `style` now gets the CSS rule — `http:` and unknown-scheme
317
+ // `url()` neutered to `about:invalid`, and `@import`,
318
+ // `expression()`, `behavior:` and `-moz-binding` dropping the
319
+ // declaration whole. Character references are decoded BEFORE
320
+ // the scheme is judged, which the old config did not do: it
321
+ // emitted `url(&#104;ttp://…)` as a literal `http://`.
322
+ // - `<form>` stops being an element. The former FORBID_TAGS here
323
+ // listed `script, iframe, object, embed, base` and omitted
324
+ // `form` — alone among this repo's author-HTML configs, both of
325
+ // which forbade it — so a password field on the owner's live
326
+ // page was one paste away.
327
+ //
328
+ // What is deliberately NOT refused, on either side of the change:
329
+ // a third-party `https:` `url()`, and `position:fixed` / `z-index`.
330
+ // The first is AGL-1725's actor analysis (the site owner chooses
331
+ // recipients for their own visitors); the second is a capability
332
+ // the Styles panel already gives the same author as a first-class
333
+ // field, so refusing it only here would move nothing.
334
+ container.innerHTML = Aglyn.sanitizeAuthorHtml(step.html);
335
+ document.body.appendChild(container);
336
+ } else if (step.type === 'runJs') {
337
+ // Business-gated server-side (dropped from props otherwise);
338
+ // the site owner's own code running on their own page.
339
+ // eslint-disable-next-line no-new-func
340
+ new Function(step.code)();
341
+ } else if (step.type === 'redirect') {
342
+ // Screen targets resolve through the routing map (AGL-339) so
343
+ // slug renames never break the interaction; url is the manual
344
+ // fallback.
345
+ const screenSlug = step.screenId != null ? screens == null ? void 0 : screens[step.screenId] : undefined;
346
+ const destination = screenSlug != null ? Aglyn.screenRoutePathToUrl(screenSlug) : step.url;
347
+ if (destination) window.location.assign(destination);
348
+ } else if (step.type === 'showElement' || step.type === 'hideElement' || step.type === 'toggleElement') {
349
+ // Element choreography (AGL-562): class-based so "show"
350
+ // reveals targets the author started hidden. The tenant page
351
+ // ships the hidden-class rule in its SSR HTML; ensure is the
352
+ // fallback for other embeddings. Grace delays + Escape/
353
+ // outside-click dismissal ride the same step (AGL-589).
354
+ Aglyn.ensureElementHiddenStyle();
355
+ Aglyn.runElementVisibilityStep(step.type === 'showElement' ? 'show' : step.type === 'hideElement' ? 'hide' : 'toggle', // Expand so a show/hide/toggle authored against the raw
356
+ // canvas id still reaches the layout-composed node (AGL-573).
357
+ Aglyn.expandLeafSelector(step.selector), {
358
+ delayMs: step.delayMs,
359
+ dismissOn: step.dismissOn
360
+ });
361
+ } else if (step.type === 'openDrawer' || step.type === 'closeDrawer' || step.type === 'toggleDrawer') {
362
+ // Drawer commands (AGL-562) ride the window event bus; the
363
+ // muiDrawer element answers (first drawer for broadcasts).
364
+ Aglyn.dispatchDrawerCommand(step.type === 'openDrawer' ? 'open' : step.type === 'closeDrawer' ? 'close' : 'toggle', step.drawerNodeId || undefined);
365
+ } else if (step.type === 'openMenu' || step.type === 'closeMenu' || step.type === 'toggleMenu') {
366
+ // Menu commands (AGL-568) ride their own event bus like
367
+ // drawers; the nav-menu elements answer (first menu for
368
+ // broadcasts). A hover-enter trigger stamps the hover flag
369
+ // so a menu opened this way closes itself when the pointer
370
+ // leaves the trigger + panel surface.
371
+ Aglyn.dispatchMenuCommand(step.type === 'openMenu' ? 'open' : step.type === 'closeMenu' ? 'close' : 'toggle', step.menuNodeId || undefined, {
372
+ hover: automation.event === 'elementHoverEnter'
373
+ });
374
+ } else if (step.type === 'trackGaEvent') {
375
+ // AGL-1587: the name and the params are AUTHORED, which makes
376
+ // this the least trustworthy analytics input in the repo — a
377
+ // param could carry a form field's value. It goes through the
378
+ // shared sanitizer and the reserved-name refusal like everything
379
+ // else, never raw gtag.
380
+ trackAuthoredEvent(step.eventName, step.params);
381
+ } else if (step.type === 'siteAlert') {
382
+ var _step_message, _step_severity;
383
+ showToast(String((_step_message = step.message) != null ? _step_message : '').slice(0, 300), (_step_severity = step.severity) != null ? _step_severity : 'info');
384
+ } else if (step.type === 'showOverlay') {
385
+ if (step.overlayId) showOverlayRef.current(step.overlayId);
386
+ }
387
+ } catch (error) {
388
+ console.error('automation step failed', error);
389
+ }
390
+ }
391
+ };
392
+ const fire = (automation)=>{
393
+ // Repeatable UI choreography (AGL-562): everyTime automations skip
394
+ // the once-per-pageview gate (explicit caps below still apply).
395
+ if (!automation.everyTime) {
396
+ if (fired.has(automation.id)) return;
397
+ fired.add(automation.id);
398
+ }
399
+ // Once per visitor (AGL-266): a persisted flag outlives the pageview.
400
+ if (automation.oncePerVisitor) {
401
+ const key = `aglyn:auto:${automation.id}`;
402
+ try {
403
+ if (localStorage.getItem(key)) return;
404
+ localStorage.setItem(key, '1');
405
+ } catch (unused) {
406
+ // Storage unavailable — degrade to once per pageview.
407
+ }
408
+ } else if (automation.oncePerSession) {
409
+ // Once per session (AGL-274): survives navigation, not the tab.
410
+ const key = `aglyn:auto:${automation.id}`;
411
+ try {
412
+ if (sessionStorage.getItem(key)) return;
413
+ sessionStorage.setItem(key, '1');
414
+ } catch (unused) {
415
+ // Storage unavailable — degrade to once per pageview.
416
+ }
417
+ } else if (automation.cooldownMinutes) {
418
+ // Cooldown (AGL-274): a timestamp gates re-fires for the visitor.
419
+ const key = `aglyn:auto:${automation.id}:at`;
420
+ try {
421
+ var _localStorage_getItem;
422
+ const last = Number((_localStorage_getItem = localStorage.getItem(key)) != null ? _localStorage_getItem : 0);
423
+ if (last && Date.now() - last < automation.cooldownMinutes * 60000) {
424
+ return;
425
+ }
426
+ localStorage.setItem(key, String(Date.now()));
427
+ } catch (unused) {
428
+ // Storage unavailable — degrade to once per pageview.
429
+ }
430
+ }
431
+ runClientSteps(automation);
432
+ if (automation.hasServerSteps) {
433
+ void fetch('/api/events/dispatch', {
434
+ method: 'POST',
435
+ headers: {
436
+ 'Content-Type': 'application/json'
437
+ },
438
+ body: JSON.stringify({
439
+ hostId,
440
+ actionId: automation.id,
441
+ event: automation.event,
442
+ payload: {
443
+ path: window.location.pathname
444
+ }
445
+ }),
446
+ keepalive: true
447
+ }).catch(()=>undefined);
448
+ }
449
+ };
450
+ /**
451
+ * Elements that arrive AFTER this effect binds (AGL-2512).
452
+ *
453
+ * `elementVisible` and `scrollToElement` are the only two triggers that
454
+ * resolve their targets by querying the DOM — every other one listens on
455
+ * `document` and so needs no element to exist yet. A single query at bind
456
+ * time made them dead on the two pages where the tree lands late: a
457
+ * deferred lazy panel patched back in on interaction (AGL-1285), and a
458
+ * gated screen's tree swapped in once the visitor is past the gate
459
+ * (AGL-2510).
460
+ *
461
+ * One MutationObserver for all of them, not one each, and it stops the
462
+ * moment nothing is left watching — these triggers fire once per pageview
463
+ * by construction, so a page whose targets have all been seen pays
464
+ * nothing for the rest of the visit. A page with no such automation never
465
+ * starts one at all.
466
+ */ const pendingObservers = [];
467
+ let domObserver = null;
468
+ const rescan = ()=>{
469
+ for (const attach of [
470
+ ...pendingObservers
471
+ ])attach();
472
+ if (!pendingObservers.length) {
473
+ domObserver == null ? void 0 : domObserver.disconnect();
474
+ domObserver = null;
475
+ }
476
+ };
477
+ const watchLateElements = (attach)=>{
478
+ pendingObservers.push(attach);
479
+ if (domObserver || typeof MutationObserver === 'undefined') return;
480
+ domObserver = new MutationObserver(rescan);
481
+ domObserver.observe(document.body, {
482
+ childList: true,
483
+ subtree: true
484
+ });
485
+ };
486
+ const stopWatching = (attach)=>{
487
+ const index = pendingObservers.indexOf(attach);
488
+ if (index >= 0) pendingObservers.splice(index, 1);
489
+ if (!pendingObservers.length) {
490
+ domObserver == null ? void 0 : domObserver.disconnect();
491
+ domObserver = null;
492
+ }
493
+ };
494
+ cleanups.push(()=>{
495
+ pendingObservers.length = 0;
496
+ domObserver == null ? void 0 : domObserver.disconnect();
497
+ domObserver = null;
498
+ });
499
+ for (const automation of automations){
500
+ const { event, threshold } = automation;
501
+ // Composition namespaces a node's live `data-aglyn` id, but the
502
+ // interaction builder records the RAW canvas id in the selector.
503
+ // Expand it so the trigger still matches the live element — layouts
504
+ // (AGL-573) and reusable-component grafts (AGL-1229), the latter being
505
+ // why every interaction authored inside the Site nav bound to nothing.
506
+ // Hand-typed CSS selectors pass through untouched.
507
+ const selector = automation.selector ? Aglyn.expandLeafSelector(automation.selector) : automation.selector;
508
+ if (event === 'pageVisit') {
509
+ fire(automation);
510
+ } else if (event === 'timeOnPage') {
511
+ const timer = setTimeout(()=>fire(automation), Math.max(1, threshold != null ? threshold : 5) * 1000);
512
+ cleanups.push(()=>clearTimeout(timer));
513
+ } else if (event === 'scrollDepth') {
514
+ const onScroll = ()=>{
515
+ const doc = document.documentElement;
516
+ const max = doc.scrollHeight - window.innerHeight;
517
+ const percent = max > 0 ? window.scrollY / max * 100 : 100;
518
+ if (percent >= Math.min(100, Math.max(1, threshold != null ? threshold : 50))) {
519
+ fire(automation);
520
+ window.removeEventListener('scroll', onScroll);
521
+ }
522
+ };
523
+ window.addEventListener('scroll', onScroll, {
524
+ passive: true
525
+ });
526
+ cleanups.push(()=>window.removeEventListener('scroll', onScroll));
527
+ } else if (event === 'elementVisible' || event === 'scrollToElement') {
528
+ if (!selector) continue;
529
+ let fired = false;
530
+ const observer = new IntersectionObserver((entries)=>{
531
+ if (fired || !entries.some((entry)=>entry.isIntersecting)) return;
532
+ fired = true;
533
+ fire(automation);
534
+ observer.disconnect();
535
+ stopWatching(attachTargets);
536
+ });
537
+ // Re-entrant: called again on every DOM change until the trigger
538
+ // fires, and observing an element twice is a no-op, so a rescan costs
539
+ // one `querySelectorAll` and nothing else.
540
+ const attachTargets = ()=>{
541
+ if (fired) return stopWatching(attachTargets);
542
+ document.querySelectorAll(selector).forEach((target)=>observer.observe(target));
543
+ };
544
+ attachTargets();
545
+ watchLateElements(attachTargets);
546
+ cleanups.push(()=>observer.disconnect());
547
+ } else if (event === 'elementClick') {
548
+ if (!selector) continue;
549
+ const onClick = (mouseEvent)=>{
550
+ var _target_closest;
551
+ const target = mouseEvent.target;
552
+ if (target == null ? void 0 : (_target_closest = target.closest) == null ? void 0 : _target_closest.call(target, selector)) fire(automation);
553
+ };
554
+ document.addEventListener('click', onClick, true);
555
+ cleanups.push(()=>document.removeEventListener('click', onClick, true));
556
+ } else if (event === 'elementHoverEnter' || event === 'elementHoverLeave') {
557
+ // Hover choreography (AGL-562): delegated mouseover/mouseout with
558
+ // enter/leave semantics — moves WITHIN the matched element (its
559
+ // own children) never re-fire.
560
+ if (!selector) continue;
561
+ const onHover = (mouseEvent)=>{
562
+ var _target_closest;
563
+ const target = mouseEvent.target;
564
+ const match = target == null ? void 0 : (_target_closest = target.closest) == null ? void 0 : _target_closest.call(target, selector);
565
+ if (!match) return;
566
+ const related = mouseEvent.relatedTarget;
567
+ if (related && match.contains(related)) return;
568
+ fire(automation);
569
+ };
570
+ const type = event === 'elementHoverEnter' ? 'mouseover' : 'mouseout';
571
+ document.addEventListener(type, onHover, true);
572
+ cleanups.push(()=>document.removeEventListener(type, onHover, true));
573
+ } else if (event === 'exitIntent') {
574
+ const onLeave = (mouseEvent)=>{
575
+ if (mouseEvent.clientY <= 0) fire(automation);
576
+ };
577
+ document.addEventListener('mouseleave', onLeave);
578
+ cleanups.push(()=>document.removeEventListener('mouseleave', onLeave));
579
+ }
580
+ }
581
+ return ()=>{
582
+ for (const cleanup of cleanups)cleanup();
583
+ };
584
+ // Automations are static per pageview (ISR props).
585
+ // eslint-disable-next-line react-hooks/exhaustive-deps
586
+ // eslint-disable-next-line react-hooks/exhaustive-deps
587
+ }, [
588
+ hostId,
589
+ screens,
590
+ // eslint-disable-next-line react-hooks/exhaustive-deps
591
+ JSON.stringify(automations.map((automation)=>automation.id))
592
+ ]);
593
+ return null;
594
+ }
595
+ function AnnouncementBar(props) {
596
+ const { bar, hostId } = props;
597
+ const storageKey = `aglyn-abar-${bar.contentHash}`;
598
+ const [hidden, setHidden] = useState(true);
599
+ useEffect(()=>{
600
+ try {
601
+ setHidden(Boolean(window.localStorage.getItem(storageKey)));
602
+ } catch (unused) {
603
+ setHidden(false);
604
+ }
605
+ }, [
606
+ storageKey
607
+ ]);
608
+ // Impression (AGL-271): once per pageview, only when actually shown.
609
+ useEffect(()=>{
610
+ if (!hidden) sendOverlayBeacon(hostId, 'barImpression', bar.overlayId);
611
+ // eslint-disable-next-line react-hooks/exhaustive-deps
612
+ }, [
613
+ hidden
614
+ ]);
615
+ if (hidden) return null;
616
+ const style = {
617
+ position: 'relative',
618
+ zIndex: 1300,
619
+ display: 'flex',
620
+ alignItems: 'center',
621
+ justifyContent: 'center',
622
+ gap: 8,
623
+ padding: '8px 40px 8px 16px',
624
+ fontSize: 14,
625
+ fontFamily: 'system-ui, sans-serif',
626
+ textAlign: 'center',
627
+ color: bar.textColor || '#fff',
628
+ backgroundColor: bar.backgroundColor || '#111827'
629
+ };
630
+ const text = bar.href ? /*#__PURE__*/ _jsx("a", {
631
+ href: bar.href,
632
+ onClick: ()=>sendOverlayBeacon(hostId, 'barClick', bar.overlayId),
633
+ style: {
634
+ color: 'inherit',
635
+ textDecoration: 'underline'
636
+ },
637
+ children: bar.text
638
+ }) : /*#__PURE__*/ _jsx("span", {
639
+ children: bar.text
640
+ });
641
+ return /*#__PURE__*/ _jsxs("div", {
642
+ role: "region",
643
+ "aria-label": "Announcement",
644
+ style: style,
645
+ children: [
646
+ text,
647
+ bar.dismissible ? /*#__PURE__*/ _jsx("button", {
648
+ type: "button",
649
+ "aria-label": "Dismiss announcement",
650
+ onClick: ()=>{
651
+ try {
652
+ window.localStorage.setItem(storageKey, '1');
653
+ } catch (unused) {
654
+ // Storage may be unavailable (private mode); hide anyway.
655
+ }
656
+ sendOverlayBeacon(hostId, 'barDismiss', bar.overlayId);
657
+ setHidden(true);
658
+ },
659
+ style: {
660
+ position: 'absolute',
661
+ right: 8,
662
+ top: '50%',
663
+ transform: 'translateY(-50%)',
664
+ border: 'none',
665
+ background: 'transparent',
666
+ color: 'inherit',
667
+ fontSize: 16,
668
+ lineHeight: 1,
669
+ cursor: 'pointer',
670
+ padding: 6
671
+ },
672
+ children: '✕'
673
+ }) : null
674
+ ]
675
+ });
676
+ }
677
+ /**
678
+ * Promotional popup (AGL-196). Client owns: schedule-window re-check (the
679
+ * page is ISR-cached), trigger timing (delay/scroll/exit), and frequency
680
+ * capping via a per-content-hash localStorage stamp (no cookies). ESC and
681
+ * backdrop close; reduced-motion preferences skip the entrance ease.
682
+ */ function PopupOverlay(props) {
683
+ var _popup_imageAlt;
684
+ const { popup, hostId } = props;
685
+ const storageKey = `aglyn-popup-${popup.contentHash}`;
686
+ const [open, setOpen] = useState(false);
687
+ // Email capture (AGL-200): submits into the forms pipeline (inbox +
688
+ // contacts); success suppresses the popup for ~10 years.
689
+ const [email, setEmail] = useState('');
690
+ // The honeypot's value, held in state so whatever a bot typed into the
691
+ // hidden input below is what gets POSTed (AGL-1665). This used to be a
692
+ // hardcoded `website: ''` in the request body: the route drops a
693
+ // submission only when the field is NON-empty, so a constant empty string
694
+ // never failed the check — it removed it. There was no field on the page
695
+ // to fill and no value a script could set, which made the popup the one
696
+ // form surface on the site with no honeypot at all.
697
+ const [website, setWebsite] = useState('');
698
+ const [submitted, setSubmitted] = useState(false);
699
+ useEffect(()=>{
700
+ if (open) sendOverlayBeacon(hostId, 'popupImpression', popup.overlayId);
701
+ // eslint-disable-next-line react-hooks/exhaustive-deps
702
+ }, [
703
+ open,
704
+ hostId
705
+ ]);
706
+ useEffect(()=>{
707
+ const now = Date.now();
708
+ if (popup.startAtMs && now < popup.startAtMs) return;
709
+ if (popup.endAtMs && now > popup.endAtMs) return;
710
+ try {
711
+ var _window_localStorage_getItem;
712
+ // The decision itself is a pure function in the model (AGL-2174) —
713
+ // which store to read is the whole feature, and it has to agree with
714
+ // what `close` writes.
715
+ const suppressed = MarketingModel.popupSuppressed(popup, MarketingModel.popupCapStore(popup) === 'session' ? {
716
+ sessionFlag: window.sessionStorage.getItem(storageKey)
717
+ } : {
718
+ lastShownMs: Number((_window_localStorage_getItem = window.localStorage.getItem(storageKey)) != null ? _window_localStorage_getItem : 0)
719
+ }, now);
720
+ if (suppressed) return;
721
+ } catch (unused) {
722
+ // Storage unavailable: show at most per-pageview.
723
+ }
724
+ let cleanup;
725
+ if (popup.trigger === 'scroll') {
726
+ const threshold = Math.min(100, Math.max(0, popup.triggerValue)) / 100;
727
+ const onScroll = ()=>{
728
+ const height = document.documentElement.scrollHeight - window.innerHeight;
729
+ if (height <= 0 || window.scrollY / height >= threshold) {
730
+ setOpen(true);
731
+ }
732
+ };
733
+ window.addEventListener('scroll', onScroll, {
734
+ passive: true
735
+ });
736
+ cleanup = ()=>window.removeEventListener('scroll', onScroll);
737
+ } else if (popup.trigger === 'exit') {
738
+ const onLeave = (event)=>{
739
+ if (event.clientY <= 0) setOpen(true);
740
+ };
741
+ document.addEventListener('mouseout', onLeave);
742
+ cleanup = ()=>document.removeEventListener('mouseout', onLeave);
743
+ } else {
744
+ const timer = window.setTimeout(()=>setOpen(true), Math.max(0, popup.triggerValue) * 1000);
745
+ cleanup = ()=>window.clearTimeout(timer);
746
+ }
747
+ return cleanup;
748
+ }, [
749
+ popup,
750
+ storageKey
751
+ ]);
752
+ const close = (kind = 'popupDismiss')=>{
753
+ try {
754
+ // Written to whichever store the cap READS (AGL-2174). Stamping
755
+ // `localStorage` for a session-capped popup would leave a timestamp
756
+ // nothing reads, outliving the cap it was meant to record.
757
+ if (MarketingModel.popupCapStore(popup) === 'session') {
758
+ window.sessionStorage.setItem(storageKey, '1');
759
+ } else {
760
+ window.localStorage.setItem(storageKey, String(Date.now()));
761
+ }
762
+ } catch (unused) {
763
+ // Private mode etc. — closing still works for this pageview.
764
+ }
765
+ sendOverlayBeacon(hostId, kind, popup.overlayId);
766
+ setOpen(false);
767
+ };
768
+ const handleEmailSubmit = async ()=>{
769
+ const value = email.trim();
770
+ if (!value || submitted) return;
771
+ try {
772
+ const response = await fetch('/api/forms/submit', {
773
+ method: 'POST',
774
+ headers: {
775
+ 'Content-Type': 'application/json'
776
+ },
777
+ body: JSON.stringify(_extends({
778
+ hostId,
779
+ // The door this capture comes through (AGL-3029): the popup is a
780
+ // Marketing element, so a site that switched Forms off still
781
+ // collects the address it shows the popup for.
782
+ door: 'popup',
783
+ formName: 'Popup',
784
+ fields: {
785
+ email: value
786
+ },
787
+ path: window.location.pathname,
788
+ // Submitted AS-IS (AGL-1665) — the point of a honeypot is that the
789
+ // client never judges it. `libs/plugins/mui/.../form.tsx` reads its
790
+ // own hidden `website` input off the FormData and forwards it the
791
+ // same way; `/api/forms/submit` is the single place that decides.
792
+ website
793
+ }, campaignTouchField()))
794
+ });
795
+ // The popup's email capture is a lead, and it reported none: an
796
+ // announcement popup is often a site's highest-converting capture, and
797
+ // the merchant saw the impression and the dismiss (`aglyn_overlay`) with
798
+ // nothing in between. The generic form block has sent `generate_lead`
799
+ // since AGL-1561; this door was simply never wired to it.
800
+ //
801
+ // Gated on `response.ok`, unlike the visitor-facing thank-you below.
802
+ // The two disagree deliberately: a spam-refused or rate-limited capture
803
+ // is still shown the thank-you — telling a real visitor to try again
804
+ // after a honeypot verdict would leak the honeypot — but it is NOT a
805
+ // lead, and counting it would inflate the only number the merchant uses
806
+ // to judge whether the popup is worth interrupting people with.
807
+ //
808
+ // `form_name` is the block's own name, matching the `formName` on the
809
+ // wire; the address the visitor just typed never leaves this function.
810
+ if (response.ok) {
811
+ trackEvent('generate_lead', {
812
+ form_name: 'Popup',
813
+ form_location: window.location.pathname
814
+ });
815
+ }
816
+ } catch (unused) {
817
+ // Best-effort — still thank the visitor.
818
+ }
819
+ setSubmitted(true);
820
+ try {
821
+ // Far-future stamp: a successful capture never re-prompts.
822
+ window.localStorage.setItem(storageKey, String(Date.now() + 315360000000));
823
+ } catch (unused) {
824
+ // Ignore.
825
+ }
826
+ };
827
+ useEffect(()=>{
828
+ if (!open) return;
829
+ const onKey = (event)=>{
830
+ if (event.key === 'Escape') close();
831
+ };
832
+ document.addEventListener('keydown', onKey);
833
+ return ()=>document.removeEventListener('keydown', onKey);
834
+ // eslint-disable-next-line react-hooks/exhaustive-deps
835
+ }, [
836
+ open
837
+ ]);
838
+ if (!open) return null;
839
+ return /*#__PURE__*/ _jsx("div", {
840
+ onClick: ()=>close(),
841
+ style: {
842
+ position: 'fixed',
843
+ inset: 0,
844
+ zIndex: 2147483200,
845
+ display: 'flex',
846
+ alignItems: 'center',
847
+ justifyContent: 'center',
848
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
849
+ padding: 16
850
+ },
851
+ children: /*#__PURE__*/ _jsxs("div", {
852
+ role: "dialog",
853
+ "aria-modal": "true",
854
+ "aria-label": popup.headline || 'Announcement',
855
+ onClick: (event)=>event.stopPropagation(),
856
+ style: {
857
+ maxWidth: 420,
858
+ width: '100%',
859
+ background: '#fff',
860
+ color: '#111827',
861
+ borderRadius: 12,
862
+ overflow: 'hidden',
863
+ boxShadow: '0 24px 64px rgba(0, 0, 0, 0.35)',
864
+ fontFamily: 'system-ui, sans-serif',
865
+ position: 'relative'
866
+ },
867
+ children: [
868
+ /*#__PURE__*/ _jsx("button", {
869
+ type: "button",
870
+ "aria-label": "Close",
871
+ onClick: ()=>close(),
872
+ style: {
873
+ position: 'absolute',
874
+ top: 8,
875
+ right: 8,
876
+ border: 'none',
877
+ background: 'rgba(255, 255, 255, 0.85)',
878
+ borderRadius: '50%',
879
+ width: 28,
880
+ height: 28,
881
+ cursor: 'pointer',
882
+ fontSize: 14,
883
+ lineHeight: 1
884
+ },
885
+ children: '✕'
886
+ }),
887
+ popup.imageUrl && !Aglyn.isRefusedAuthorImageSrc(popup.imageUrl) ? // eslint-disable-next-line @next/next/no-img-element
888
+ /*#__PURE__*/ _jsx("img", {
889
+ src: popup.imageUrl,
890
+ // AGL-1896. This was a hardcoded `alt=""` with no field behind
891
+ // it. Still empty when the author left it empty — a decorative
892
+ // banner beside its own headline is the case `alt=""` exists for
893
+ // — but it is now their choice rather than ours, and picking the
894
+ // image from the library fills it in from the asset.
895
+ alt: (_popup_imageAlt = popup.imageAlt) != null ? _popup_imageAlt : '',
896
+ style: {
897
+ width: '100%',
898
+ maxHeight: 200,
899
+ objectFit: 'cover',
900
+ display: 'block'
901
+ }
902
+ }) : null,
903
+ /*#__PURE__*/ _jsxs("div", {
904
+ style: {
905
+ padding: 24,
906
+ textAlign: 'center'
907
+ },
908
+ children: [
909
+ popup.headline ? /*#__PURE__*/ _jsx("h2", {
910
+ style: {
911
+ margin: '0 0 8px',
912
+ fontSize: 20
913
+ },
914
+ children: popup.headline
915
+ }) : null,
916
+ /*#__PURE__*/ _jsx("p", {
917
+ style: {
918
+ margin: 0,
919
+ fontSize: 14,
920
+ whiteSpace: 'pre-wrap'
921
+ },
922
+ children: popup.body
923
+ }),
924
+ popup.collectEmail ? /*#__PURE__*/ _jsx("div", {
925
+ style: {
926
+ marginTop: 16
927
+ },
928
+ children: submitted ? /*#__PURE__*/ _jsx("p", {
929
+ style: {
930
+ margin: 0,
931
+ fontSize: 14,
932
+ fontWeight: 600
933
+ },
934
+ children: 'Thanks — you are on the list!'
935
+ }) : /*#__PURE__*/ _jsxs("form", {
936
+ onSubmit: (event)=>{
937
+ event.preventDefault();
938
+ void handleEmailSubmit();
939
+ },
940
+ style: {
941
+ display: 'flex',
942
+ gap: 8,
943
+ justifyContent: 'center'
944
+ },
945
+ children: [
946
+ /*#__PURE__*/ _jsx("input", {
947
+ type: "email",
948
+ required: true,
949
+ placeholder: "you@example.com",
950
+ // Same nameless-input defect as the newsletter block
951
+ // (AGL-2392). A popup is modal-ish and arrives unbidden,
952
+ // so a screen-reader user meets this field with no
953
+ // surrounding context at all.
954
+ "aria-label": "Email address",
955
+ value: email,
956
+ onChange: (event)=>setEmail(event.target.value),
957
+ style: {
958
+ flex: 1,
959
+ maxWidth: 240,
960
+ padding: '8px 12px',
961
+ borderRadius: 8,
962
+ border: '1px solid #d1d5db',
963
+ fontSize: 14
964
+ }
965
+ }),
966
+ /*#__PURE__*/ _jsx("input", {
967
+ type: "text",
968
+ name: "website",
969
+ tabIndex: -1,
970
+ autoComplete: "off",
971
+ "aria-hidden": "true",
972
+ value: website,
973
+ onChange: (event)=>setWebsite(event.target.value),
974
+ style: {
975
+ position: 'absolute',
976
+ left: '-5000px',
977
+ height: 0,
978
+ width: 0
979
+ }
980
+ }),
981
+ /*#__PURE__*/ _jsx("button", {
982
+ type: "submit",
983
+ style: {
984
+ padding: '8px 16px',
985
+ borderRadius: 8,
986
+ border: 'none',
987
+ background: '#111827',
988
+ color: '#fff',
989
+ fontSize: 14,
990
+ cursor: 'pointer'
991
+ },
992
+ children: 'Sign up'
993
+ })
994
+ ]
995
+ })
996
+ }) : null,
997
+ popup.ctaHref && popup.ctaLabel ? /*#__PURE__*/ _jsx("a", {
998
+ href: popup.ctaHref,
999
+ onClick: ()=>close('popupClick'),
1000
+ style: {
1001
+ display: 'inline-block',
1002
+ marginTop: 16,
1003
+ padding: '10px 24px',
1004
+ borderRadius: 8,
1005
+ background: '#111827',
1006
+ color: '#fff',
1007
+ textDecoration: 'none',
1008
+ fontSize: 14
1009
+ },
1010
+ children: popup.ctaLabel
1011
+ }) : null
1012
+ ]
1013
+ })
1014
+ ]
1015
+ })
1016
+ });
1017
+ }
1018
+ /**
1019
+ * The marketing plugin's site runtime (AGL-419), relocated verbatim from
1020
+ * the tenant catch-all: announcement bar + popup rendering (AGL-195/200),
1021
+ * the experiments runner (AGL-253), the site-event automations engine
1022
+ * (AGL-256/257), and automation-triggered overlays. Reads back exactly the
1023
+ * page-props slices its server enricher wrote (site-contract shapes).
1024
+ */ export function MarketingSiteRuntime({ hostId, screens, page }) {
1025
+ var _page_experiments, _page_clientAutomations, _automationOverlay_popup_frequencyDays;
1026
+ var _page_data, _automationOverlay_bar, _automationOverlay_popup;
1027
+ const [automationOverlay, setAutomationOverlay] = useState(null);
1028
+ const announcementBar = page['announcementBar'];
1029
+ const popup = page['popup'];
1030
+ const experiments = (_page_experiments = page['experiments']) != null ? _page_experiments : [];
1031
+ const clientAutomations = (_page_clientAutomations = page['clientAutomations']) != null ? _page_clientAutomations : [];
1032
+ const automationOverlays = page['automationOverlays'];
1033
+ return /*#__PURE__*/ _jsxs(_Fragment, {
1034
+ children: [
1035
+ announcementBar ? /*#__PURE__*/ _jsx(AnnouncementBar, {
1036
+ bar: announcementBar,
1037
+ hostId: hostId
1038
+ }) : null,
1039
+ popup ? /*#__PURE__*/ _jsx(PopupOverlay, {
1040
+ popup: popup,
1041
+ hostId: hostId
1042
+ }) : null,
1043
+ experiments.length ? /*#__PURE__*/ _jsx(ExperimentsRunner, {
1044
+ hostId: hostId,
1045
+ experiments: experiments,
1046
+ consentHost: (_page_data = page['data']) == null ? void 0 : _page_data.host
1047
+ }) : null,
1048
+ clientAutomations.length ? /*#__PURE__*/ _jsx(AutomationsEngine, {
1049
+ hostId: hostId,
1050
+ automations: clientAutomations,
1051
+ screens: screens,
1052
+ onShowOverlay: (overlayId)=>{
1053
+ const overlay = automationOverlays == null ? void 0 : automationOverlays[overlayId];
1054
+ if (overlay) setAutomationOverlay(_extends({
1055
+ id: overlayId
1056
+ }, overlay));
1057
+ }
1058
+ }) : null,
1059
+ (automationOverlay == null ? void 0 : automationOverlay.kind) === 'bar' && ((_automationOverlay_bar = automationOverlay.bar) == null ? void 0 : _automationOverlay_bar.text) ? /*#__PURE__*/ _jsx(AnnouncementBar, {
1060
+ bar: _extends({}, automationOverlay.bar, {
1061
+ text: String(automationOverlay.bar.text),
1062
+ dismissible: automationOverlay.bar.dismissible !== false,
1063
+ contentHash: `automation-${automationOverlay.id}`
1064
+ }),
1065
+ hostId: hostId
1066
+ }) : null,
1067
+ (automationOverlay == null ? void 0 : automationOverlay.kind) === 'popup' && ((_automationOverlay_popup = automationOverlay.popup) == null ? void 0 : _automationOverlay_popup.body) ? /*#__PURE__*/ _jsx(PopupOverlay, {
1068
+ popup: _extends({}, automationOverlay.popup, {
1069
+ body: String(automationOverlay.popup.body),
1070
+ trigger: 'delay',
1071
+ triggerValue: 0,
1072
+ frequencyDays: Math.max(1, Number((_automationOverlay_popup_frequencyDays = automationOverlay.popup.frequencyDays) != null ? _automationOverlay_popup_frequencyDays : 1)),
1073
+ contentHash: `automation-${automationOverlay.id}`
1074
+ }),
1075
+ hostId: hostId
1076
+ }) : null
1077
+ ]
1078
+ });
1079
+ }
1080
+
1081
+ //# sourceMappingURL=site-runtime.js.map