@pushwoosh/websdk-common 0.0.0 → 6.17.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/package.json +30 -5
- package/subscription-widget/SubscriptionWidgetReader.d.ts +23 -0
- package/subscription-widget/SubscriptionWidgetReader.js +307 -0
- package/subscription-widget/constants.d.ts +19 -0
- package/subscription-widget/constants.js +61 -0
- package/subscription-widget/helpers.d.ts +16 -0
- package/subscription-widget/helpers.js +110 -0
- package/subscription-widget/index.d.ts +4 -0
- package/subscription-widget/index.js +3 -0
- package/subscription-widget/types.d.ts +90 -0
- package/subscription-widget/types.js +1 -0
- package/web-popups/WebPopupReader.d.ts +4 -0
- package/web-popups/WebPopupReader.js +1394 -0
- package/web-popups/colorScheme.d.ts +2 -0
- package/web-popups/colorScheme.js +22 -0
- package/web-popups/constants.d.ts +71 -0
- package/web-popups/constants.js +216 -0
- package/web-popups/glyphs.d.ts +8 -0
- package/web-popups/glyphs.js +30 -0
- package/web-popups/helpers.d.ts +84 -0
- package/web-popups/helpers.js +687 -0
- package/web-popups/index.d.ts +9 -0
- package/web-popups/index.js +8 -0
- package/web-popups/layout.d.ts +39 -0
- package/web-popups/layout.js +290 -0
- package/web-popups/palette.d.ts +34 -0
- package/web-popups/palette.js +157 -0
- package/web-popups/templates.d.ts +3 -0
- package/web-popups/templates.js +125 -0
- package/web-popups/timer.d.ts +16 -0
- package/web-popups/timer.js +194 -0
- package/web-popups/types.d.ts +660 -0
- package/web-popups/types.js +10 -0
|
@@ -0,0 +1,1394 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useColorScheme } from './colorScheme';
|
|
4
|
+
import { webPopupButtonPillStyle, webPopupFormInputStyle, webPopupRatingScores, webPopupTextColor, webPopupSlotGap, webPopupSlotRole, WEB_POPUP_BG_SECTION_FALLBACK, WEB_POPUP_BUTTON_SLOT_PADDING, WEB_POPUP_FONT_STACK, WEB_POPUP_KEYFRAMES, WEB_POPUP_PAGE_TRANSITION_MS, WEB_POPUP_RETRY_ERROR, WEB_POPUP_TEXT_SLOT_PADDING, WEB_POPUP_TEXT_VARIANT_STYLES } from './constants';
|
|
5
|
+
import { WEB_POPUP_GLYPH_PATHS, WEB_POPUP_THUMB_UP_PATH } from './glyphs';
|
|
6
|
+
import { placementMaxSize, resolveWebPopupPages, safeUrl, sanitizeWebPopupHtml, startPageOf, webPopupLinkTargetAttrs } from './helpers';
|
|
7
|
+
import { webPopupBackgroundImageStyle, webPopupContainerStyle, webPopupScrimGradient } from './layout';
|
|
8
|
+
import { resolveWebPopupBoxChrome, resolveWebPopupColors, webPopupColorAlpha, webPopupDarkEnabled, webPopupImageSrc } from './palette';
|
|
9
|
+
import { webPopupTimerLabels, webPopupTimerRemaining } from './timer';
|
|
10
|
+
// Renders a popup from its model by interpreting each page's embedded layout
|
|
11
|
+
// tree — the same node vocabulary the editor's canvas builds from: container
|
|
12
|
+
// nodes become inline-styled grid divs, `Content` leaves render a named
|
|
13
|
+
// zone's slot stack, `Image` leaves an image param. 'live' mode adds the
|
|
14
|
+
// viewport chrome (overlay, placement, animation, close), 'preview' renders
|
|
15
|
+
// just the popup box inline. Only one page renders at a time; buttons with a
|
|
16
|
+
// `page` action switch pages. All styling is inline (shadow-DOM friendly, no
|
|
17
|
+
// CSS files).
|
|
18
|
+
const ANIMATION_STYLE = {
|
|
19
|
+
none: undefined,
|
|
20
|
+
fade: 'pw-vpopup-fade 0.35s ease-out backwards',
|
|
21
|
+
slide: 'pw-vpopup-slide 0.35s ease-out backwards'
|
|
22
|
+
};
|
|
23
|
+
function TextSlotView({
|
|
24
|
+
slot,
|
|
25
|
+
align,
|
|
26
|
+
inverse,
|
|
27
|
+
colors
|
|
28
|
+
}) {
|
|
29
|
+
const style = {
|
|
30
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES[slot.variant],
|
|
31
|
+
textAlign: slot.align ?? align
|
|
32
|
+
};
|
|
33
|
+
const color = webPopupTextColor(slot.variant, colors, inverse);
|
|
34
|
+
if (color) style.color = color;
|
|
35
|
+
// The html comes from the editor's rich-text slots, but the json is
|
|
36
|
+
// API-writable — sanitize on render (inline tags only, safe link hrefs).
|
|
37
|
+
return _jsx("div", {
|
|
38
|
+
style: style,
|
|
39
|
+
dangerouslySetInnerHTML: {
|
|
40
|
+
__html: sanitizeWebPopupHtml(slot.html)
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function ButtonSlotView({
|
|
45
|
+
slot,
|
|
46
|
+
colors,
|
|
47
|
+
onRequestClose,
|
|
48
|
+
onPageChange,
|
|
49
|
+
onRequestSubscribe
|
|
50
|
+
}) {
|
|
51
|
+
const [askPhase, setAskPhase] = useState('idle');
|
|
52
|
+
const pill = webPopupButtonPillStyle(slot.variant, colors);
|
|
53
|
+
const wrapper = {};
|
|
54
|
+
if (slot.action.type === 'close') {
|
|
55
|
+
return _jsx("div", {
|
|
56
|
+
style: wrapper,
|
|
57
|
+
children: _jsx("button", {
|
|
58
|
+
type: "button",
|
|
59
|
+
style: pill,
|
|
60
|
+
onClick: onRequestClose,
|
|
61
|
+
children: slot.label
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (slot.action.type === 'page') {
|
|
66
|
+
const target = slot.action.page;
|
|
67
|
+
return _jsx("div", {
|
|
68
|
+
style: wrapper,
|
|
69
|
+
children: _jsx("button", {
|
|
70
|
+
type: "button",
|
|
71
|
+
style: pill,
|
|
72
|
+
onClick: () => onPageChange(target),
|
|
73
|
+
children: slot.label
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (slot.action.type === 'subscribe') {
|
|
78
|
+
// 'asking' guards the double click (one prompt, one registerDevice);
|
|
79
|
+
// 'denied' stays disabled because the browser grants no second prompt.
|
|
80
|
+
const spent = askPhase !== 'idle';
|
|
81
|
+
const ask = () => {
|
|
82
|
+
if (!onRequestSubscribe || spent) return;
|
|
83
|
+
setAskPhase('asking');
|
|
84
|
+
onRequestSubscribe().then(result => {
|
|
85
|
+
// Granted: the CTA is spent, so the popup goes away — that IS the
|
|
86
|
+
// feedback. Dismissed ('default') or failed: the pill comes back.
|
|
87
|
+
if (result === 'granted') {
|
|
88
|
+
onRequestClose?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setAskPhase(result === 'denied' ? 'denied' : 'idle');
|
|
92
|
+
}).catch(() => setAskPhase('idle'));
|
|
93
|
+
};
|
|
94
|
+
return _jsx("div", {
|
|
95
|
+
style: wrapper,
|
|
96
|
+
children: _jsx("button", {
|
|
97
|
+
type: "button",
|
|
98
|
+
disabled: spent,
|
|
99
|
+
style: {
|
|
100
|
+
...pill,
|
|
101
|
+
...(spent ? {
|
|
102
|
+
opacity: 0.7
|
|
103
|
+
} : {})
|
|
104
|
+
},
|
|
105
|
+
onClick: ask,
|
|
106
|
+
children: slot.label
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const href = safeUrl(slot.action.url);
|
|
111
|
+
// No (safe) URL yet: an inert pill — not an '#' anchor opening a blank tab.
|
|
112
|
+
if (!href) {
|
|
113
|
+
return _jsx("div", {
|
|
114
|
+
style: wrapper,
|
|
115
|
+
children: _jsx("button", {
|
|
116
|
+
type: "button",
|
|
117
|
+
style: pill,
|
|
118
|
+
children: slot.label
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return _jsx("div", {
|
|
123
|
+
style: wrapper,
|
|
124
|
+
children: _jsx("a", {
|
|
125
|
+
href: href,
|
|
126
|
+
...webPopupLinkTargetAttrs(slot.action.target),
|
|
127
|
+
style: pill,
|
|
128
|
+
children: slot.label
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// The copy a submitted slot leaves behind, in the zone's own body style.
|
|
133
|
+
function SlotMessageView({
|
|
134
|
+
message,
|
|
135
|
+
align,
|
|
136
|
+
inverse,
|
|
137
|
+
colors
|
|
138
|
+
}) {
|
|
139
|
+
const color = webPopupTextColor('body', colors, inverse);
|
|
140
|
+
return _jsx("div", {
|
|
141
|
+
style: {
|
|
142
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.body,
|
|
143
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING,
|
|
144
|
+
textAlign: align,
|
|
145
|
+
...(color ? {
|
|
146
|
+
color
|
|
147
|
+
} : {})
|
|
148
|
+
},
|
|
149
|
+
children: message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const FORM_EMAIL_PATTERN = /^\S+@\S+\.\S+$/;
|
|
153
|
+
const FORM_INPUT_TYPES = {
|
|
154
|
+
email: 'email',
|
|
155
|
+
phone: 'tel'
|
|
156
|
+
};
|
|
157
|
+
const FORM_AUTOCOMPLETE = {
|
|
158
|
+
email: 'email',
|
|
159
|
+
firstName: 'given-name',
|
|
160
|
+
lastName: 'family-name',
|
|
161
|
+
phone: 'tel'
|
|
162
|
+
};
|
|
163
|
+
// The privacy-check error renders under the checkbox row; a field key can
|
|
164
|
+
// never collide with it (normalized keys are non-empty slot-local ids).
|
|
165
|
+
const PRIVACY_ERROR_KEY = '__privacy';
|
|
166
|
+
/**
|
|
167
|
+
* An email-subscription form slot: the fields, the consent row and the
|
|
168
|
+
* submit pill, all popup-styled. Local state machine: idle → submitting →
|
|
169
|
+
* success/confirm (the form swaps to the slot's message) or error (the form
|
|
170
|
+
* stays, a generic retryable note appears). Without an onSubmitForm handler
|
|
171
|
+
* or a bound formCode the form is inert — inputs type, submit does nothing —
|
|
172
|
+
* which is exactly what previews and the editor canvas want.
|
|
173
|
+
*/
|
|
174
|
+
function FormSlotView({
|
|
175
|
+
slot,
|
|
176
|
+
align,
|
|
177
|
+
inverse,
|
|
178
|
+
colors,
|
|
179
|
+
onSubmitForm
|
|
180
|
+
}) {
|
|
181
|
+
const [values, setValues] = useState({});
|
|
182
|
+
const [policyAccepted, setPolicyAccepted] = useState(false);
|
|
183
|
+
const [focusedKey, setFocusedKey] = useState(null);
|
|
184
|
+
const [errors, setErrors] = useState({});
|
|
185
|
+
const [phase, setPhase] = useState('idle');
|
|
186
|
+
const captionColor = webPopupTextColor('caption', colors, inverse);
|
|
187
|
+
const captionStyle = {
|
|
188
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.caption,
|
|
189
|
+
...(captionColor ? {
|
|
190
|
+
color: captionColor
|
|
191
|
+
} : {})
|
|
192
|
+
};
|
|
193
|
+
const errorStyle = {
|
|
194
|
+
fontSize: 12,
|
|
195
|
+
lineHeight: 1.4,
|
|
196
|
+
color: colors.error,
|
|
197
|
+
paddingTop: 2
|
|
198
|
+
};
|
|
199
|
+
if (phase === 'success' || phase === 'confirm') {
|
|
200
|
+
return _jsx(SlotMessageView, {
|
|
201
|
+
message: phase === 'confirm' ? slot.confirmEmailMessage : slot.successMessage,
|
|
202
|
+
align: align,
|
|
203
|
+
inverse: inverse,
|
|
204
|
+
colors: colors
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const setFieldValue = (key, value) => {
|
|
208
|
+
setValues(prev => ({
|
|
209
|
+
...prev,
|
|
210
|
+
[key]: value
|
|
211
|
+
}));
|
|
212
|
+
setErrors(prev => prev[key] ? Object.fromEntries(Object.entries(prev).filter(([k]) => k !== key)) : prev);
|
|
213
|
+
};
|
|
214
|
+
const submit = () => {
|
|
215
|
+
if (!onSubmitForm || !slot.formCode || phase === 'submitting') return;
|
|
216
|
+
const found = {};
|
|
217
|
+
for (const field of slot.fields) {
|
|
218
|
+
const value = (values[field.key] ?? '').trim();
|
|
219
|
+
if (field.required && !value) found[field.key] = 'Required';else if (field.kind === 'email' && value && !FORM_EMAIL_PATTERN.test(value)) {
|
|
220
|
+
found[field.key] = 'Enter a valid email';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (slot.privacy && !policyAccepted) found[PRIVACY_ERROR_KEY] = 'Please accept the privacy policy';
|
|
224
|
+
setErrors(found);
|
|
225
|
+
if (Object.keys(found).length > 0) return;
|
|
226
|
+
const payload = {
|
|
227
|
+
email: '',
|
|
228
|
+
policyAccepted: slot.privacy ? policyAccepted : true
|
|
229
|
+
};
|
|
230
|
+
const customFields = {};
|
|
231
|
+
for (const field of slot.fields) {
|
|
232
|
+
const value = (values[field.key] ?? '').trim();
|
|
233
|
+
if (!value) continue;
|
|
234
|
+
if (field.kind === 'email') payload.email = payload.email || value;else if (field.kind === 'firstName') payload.firstName = value;else if (field.kind === 'lastName') payload.lastName = value;else if (field.kind === 'phone') payload.phone = value;else customFields[field.tag || field.key] = value;
|
|
235
|
+
}
|
|
236
|
+
if (Object.keys(customFields).length > 0) payload.customFields = customFields;
|
|
237
|
+
setPhase('submitting');
|
|
238
|
+
// Promise.resolve wrapper: a host callback that throws SYNCHRONOUSLY
|
|
239
|
+
// (before returning its promise) must land in the error phase too — a
|
|
240
|
+
// bare .then(ok, fail) would let the throw escape and strand the form
|
|
241
|
+
// in 'submitting' with no way back.
|
|
242
|
+
Promise.resolve().then(() => onSubmitForm(slot.formCode, payload)).then(result => setPhase(result === 'confirm-email' ? 'confirm' : 'success'), () => setPhase('error'));
|
|
243
|
+
};
|
|
244
|
+
const submitting = phase === 'submitting';
|
|
245
|
+
const privacyHref = slot.privacy ? safeUrl(slot.privacy.url) : undefined;
|
|
246
|
+
const pill = {
|
|
247
|
+
...webPopupButtonPillStyle(slot.submitVariant, colors),
|
|
248
|
+
...(submitting ? {
|
|
249
|
+
opacity: 0.6,
|
|
250
|
+
cursor: 'default'
|
|
251
|
+
} : {})
|
|
252
|
+
};
|
|
253
|
+
return _jsxs("form", {
|
|
254
|
+
noValidate: true,
|
|
255
|
+
style: {
|
|
256
|
+
margin: 0,
|
|
257
|
+
textAlign: 'left'
|
|
258
|
+
},
|
|
259
|
+
onSubmit: event => {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
submit();
|
|
262
|
+
},
|
|
263
|
+
children: [slot.fields.map(field => {
|
|
264
|
+
const error = errors[field.key];
|
|
265
|
+
let borderColor;
|
|
266
|
+
if (error) borderColor = colors.error;else if (focusedKey === field.key) borderColor = colors.accent;
|
|
267
|
+
return _jsxs("div", {
|
|
268
|
+
style: {
|
|
269
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING
|
|
270
|
+
},
|
|
271
|
+
children: [field.label && _jsx("div", {
|
|
272
|
+
style: {
|
|
273
|
+
...captionStyle,
|
|
274
|
+
paddingBottom: 4
|
|
275
|
+
},
|
|
276
|
+
children: field.label
|
|
277
|
+
}), _jsx("input", {
|
|
278
|
+
type: FORM_INPUT_TYPES[field.kind] ?? 'text',
|
|
279
|
+
autoComplete: FORM_AUTOCOMPLETE[field.kind],
|
|
280
|
+
placeholder: field.placeholder || undefined,
|
|
281
|
+
value: values[field.key] ?? '',
|
|
282
|
+
disabled: submitting,
|
|
283
|
+
style: webPopupFormInputStyle(colors, borderColor),
|
|
284
|
+
onFocus: () => setFocusedKey(field.key),
|
|
285
|
+
onBlur: () => setFocusedKey(key => key === field.key ? null : key),
|
|
286
|
+
onChange: event => setFieldValue(field.key, event.currentTarget.value)
|
|
287
|
+
}), error && _jsx("div", {
|
|
288
|
+
style: errorStyle,
|
|
289
|
+
children: error
|
|
290
|
+
})]
|
|
291
|
+
}, field.key);
|
|
292
|
+
}), slot.privacy && _jsxs("div", {
|
|
293
|
+
style: {
|
|
294
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING
|
|
295
|
+
},
|
|
296
|
+
children: [_jsxs("label", {
|
|
297
|
+
style: {
|
|
298
|
+
...captionStyle,
|
|
299
|
+
display: 'flex',
|
|
300
|
+
alignItems: 'flex-start',
|
|
301
|
+
gap: 8,
|
|
302
|
+
cursor: 'pointer'
|
|
303
|
+
},
|
|
304
|
+
children: [_jsx("input", {
|
|
305
|
+
type: "checkbox",
|
|
306
|
+
checked: policyAccepted,
|
|
307
|
+
disabled: submitting,
|
|
308
|
+
style: {
|
|
309
|
+
margin: '2px 0 0',
|
|
310
|
+
flexShrink: 0
|
|
311
|
+
},
|
|
312
|
+
onChange: event => {
|
|
313
|
+
setPolicyAccepted(event.currentTarget.checked);
|
|
314
|
+
setErrors(prev => prev[PRIVACY_ERROR_KEY] ? Object.fromEntries(Object.entries(prev).filter(([k]) => k !== PRIVACY_ERROR_KEY)) : prev);
|
|
315
|
+
}
|
|
316
|
+
}), _jsxs("span", {
|
|
317
|
+
children: [slot.privacy.checkboxLabel, privacyHref && _jsxs(_Fragment, {
|
|
318
|
+
children: [' ', _jsx("a", {
|
|
319
|
+
href: privacyHref,
|
|
320
|
+
...webPopupLinkTargetAttrs(undefined),
|
|
321
|
+
style: {
|
|
322
|
+
color: colors.accent
|
|
323
|
+
},
|
|
324
|
+
children: slot.privacy.linkLabel || slot.privacy.url
|
|
325
|
+
})]
|
|
326
|
+
})]
|
|
327
|
+
})]
|
|
328
|
+
}), errors[PRIVACY_ERROR_KEY] && _jsx("div", {
|
|
329
|
+
style: errorStyle,
|
|
330
|
+
children: errors[PRIVACY_ERROR_KEY]
|
|
331
|
+
})]
|
|
332
|
+
}), _jsx("div", {
|
|
333
|
+
style: {
|
|
334
|
+
padding: WEB_POPUP_BUTTON_SLOT_PADDING
|
|
335
|
+
},
|
|
336
|
+
children: _jsx("button", {
|
|
337
|
+
type: "submit",
|
|
338
|
+
disabled: submitting,
|
|
339
|
+
style: pill,
|
|
340
|
+
children: slot.submitLabel
|
|
341
|
+
})
|
|
342
|
+
}), phase === 'error' && _jsx("div", {
|
|
343
|
+
style: {
|
|
344
|
+
...errorStyle,
|
|
345
|
+
textAlign: align
|
|
346
|
+
},
|
|
347
|
+
children: WEB_POPUP_RETRY_ERROR
|
|
348
|
+
})]
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// The phase-2 slots. The two that submit follow the form slot's rules exactly:
|
|
352
|
+
// inert without a callback, idle → submitting → success/error, then its copy.
|
|
353
|
+
// One cell of the countdown: the digits over their unit label.
|
|
354
|
+
function TimerCell({
|
|
355
|
+
value,
|
|
356
|
+
label,
|
|
357
|
+
digitStyle,
|
|
358
|
+
labelStyle
|
|
359
|
+
}) {
|
|
360
|
+
return _jsxs("div", {
|
|
361
|
+
style: {
|
|
362
|
+
minWidth: 44
|
|
363
|
+
},
|
|
364
|
+
children: [_jsx("div", {
|
|
365
|
+
style: digitStyle,
|
|
366
|
+
children: String(value).padStart(2, '0')
|
|
367
|
+
}), label ? _jsx("div", {
|
|
368
|
+
style: labelStyle,
|
|
369
|
+
children: label
|
|
370
|
+
}) : null]
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function TimerSlotView({
|
|
374
|
+
slot,
|
|
375
|
+
align,
|
|
376
|
+
inverse,
|
|
377
|
+
colors,
|
|
378
|
+
mode,
|
|
379
|
+
onRequestClose,
|
|
380
|
+
onPageChange
|
|
381
|
+
}) {
|
|
382
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
383
|
+
const remaining = webPopupTimerRemaining(slot.endTime, slot.timezone, nowMs);
|
|
384
|
+
const expired = remaining.total === 0;
|
|
385
|
+
// 'preview' draws a still frame: a gallery is many popups at once, and every
|
|
386
|
+
// one of them would tick. A finished countdown stops on its own.
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (mode !== 'live' || expired) return undefined;
|
|
389
|
+
const id = setInterval(() => setNowMs(Date.now()), 1000);
|
|
390
|
+
return () => clearInterval(id);
|
|
391
|
+
}, [mode, expired, slot.endTime, slot.timezone]);
|
|
392
|
+
// Past the deadline, the slot does what it was told. 'hide-popup' gets here
|
|
393
|
+
// only for a popup left open ACROSS its deadline (the host gates the rest).
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (mode !== 'live' || !expired) return;
|
|
396
|
+
if (slot.onExpire === 'hide-popup') onRequestClose?.();else if (slot.onExpire === 'page' && slot.expirePage) onPageChange(slot.expirePage);
|
|
397
|
+
}, [mode, expired, slot.onExpire, slot.expirePage, onRequestClose, onPageChange]);
|
|
398
|
+
const digitColor = webPopupTextColor('h2', colors, inverse);
|
|
399
|
+
const labelColor = webPopupTextColor('caption', colors, inverse);
|
|
400
|
+
const digitStyle = {
|
|
401
|
+
fontSize: 28,
|
|
402
|
+
fontWeight: 700,
|
|
403
|
+
lineHeight: 1.2,
|
|
404
|
+
// Tabular figures: without them every second reflows the row.
|
|
405
|
+
fontVariantNumeric: 'tabular-nums',
|
|
406
|
+
...(digitColor ? {
|
|
407
|
+
color: digitColor
|
|
408
|
+
} : {})
|
|
409
|
+
};
|
|
410
|
+
const labelStyle = {
|
|
411
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.caption,
|
|
412
|
+
...(labelColor ? {
|
|
413
|
+
color: labelColor
|
|
414
|
+
} : {})
|
|
415
|
+
};
|
|
416
|
+
const labels = webPopupTimerLabels(slot.language);
|
|
417
|
+
const cells = [{
|
|
418
|
+
value: remaining.days,
|
|
419
|
+
label: labels.days
|
|
420
|
+
}, {
|
|
421
|
+
value: remaining.hours,
|
|
422
|
+
label: labels.hours
|
|
423
|
+
}, {
|
|
424
|
+
value: remaining.minutes,
|
|
425
|
+
label: labels.minutes
|
|
426
|
+
}, {
|
|
427
|
+
value: remaining.seconds,
|
|
428
|
+
label: labels.seconds
|
|
429
|
+
}];
|
|
430
|
+
return _jsx("div", {
|
|
431
|
+
style: {
|
|
432
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING,
|
|
433
|
+
textAlign: align
|
|
434
|
+
},
|
|
435
|
+
children: _jsx("div", {
|
|
436
|
+
style: {
|
|
437
|
+
display: 'inline-flex',
|
|
438
|
+
gap: 12,
|
|
439
|
+
textAlign: 'center'
|
|
440
|
+
},
|
|
441
|
+
children: cells.map(cell => _jsx(TimerCell, {
|
|
442
|
+
value: cell.value,
|
|
443
|
+
label: slot.showLabels ? cell.label : '',
|
|
444
|
+
digitStyle: digitStyle,
|
|
445
|
+
labelStyle: labelStyle
|
|
446
|
+
}, cell.label || cell.value))
|
|
447
|
+
})
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function ChoiceSlotView({
|
|
451
|
+
slot,
|
|
452
|
+
align,
|
|
453
|
+
inverse,
|
|
454
|
+
colors,
|
|
455
|
+
onSubmitChoice
|
|
456
|
+
}) {
|
|
457
|
+
const [picked, setPicked] = useState([]);
|
|
458
|
+
const [phase, setPhase] = useState('idle');
|
|
459
|
+
const [notice, setNotice] = useState('');
|
|
460
|
+
if (phase === 'success') {
|
|
461
|
+
return _jsx(SlotMessageView, {
|
|
462
|
+
message: slot.successMessage,
|
|
463
|
+
align: align,
|
|
464
|
+
inverse: inverse,
|
|
465
|
+
colors: colors
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const toggle = value => {
|
|
469
|
+
setNotice('');
|
|
470
|
+
setPicked(prev => {
|
|
471
|
+
if (prev.includes(value)) return prev.filter(entry => entry !== value);
|
|
472
|
+
// At the ceiling the extra tap is refused with a reason rather than
|
|
473
|
+
// silently dropping the oldest pick.
|
|
474
|
+
if (slot.max > 0 && prev.length >= slot.max) {
|
|
475
|
+
setNotice(`Pick up to ${slot.max}`);
|
|
476
|
+
return prev;
|
|
477
|
+
}
|
|
478
|
+
return [...prev, value];
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
const submit = () => {
|
|
482
|
+
if (!onSubmitChoice || !slot.tagName || phase === 'submitting') return;
|
|
483
|
+
// An empty submit would CLEAR the tag — never what a "save" button means.
|
|
484
|
+
const floor = Math.max(slot.min, 1);
|
|
485
|
+
if (picked.length < floor) {
|
|
486
|
+
setNotice(floor === 1 ? 'Pick at least one' : `Pick at least ${floor}`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const values = slot.options.filter(option => picked.includes(option.value)).map(option => option.tagValue || option.value);
|
|
490
|
+
setNotice('');
|
|
491
|
+
setPhase('submitting');
|
|
492
|
+
// Promise.resolve wrapper: a host callback that throws SYNCHRONOUSLY must
|
|
493
|
+
// land in the error phase too, not escape and strand the slot.
|
|
494
|
+
Promise.resolve().then(() => onSubmitChoice({
|
|
495
|
+
slotKey: slot.key,
|
|
496
|
+
tagName: slot.tagName,
|
|
497
|
+
values
|
|
498
|
+
})).then(() => setPhase('success'), () => setPhase('error'));
|
|
499
|
+
};
|
|
500
|
+
const submitting = phase === 'submitting';
|
|
501
|
+
const textColor = webPopupTextColor('body', colors, inverse);
|
|
502
|
+
const optionStyle = {
|
|
503
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.body,
|
|
504
|
+
...(textColor ? {
|
|
505
|
+
color: textColor
|
|
506
|
+
} : {})
|
|
507
|
+
};
|
|
508
|
+
const chip = selected => ({
|
|
509
|
+
padding: '6px 14px',
|
|
510
|
+
borderRadius: 999,
|
|
511
|
+
border: `1px solid ${colors.accent}`,
|
|
512
|
+
backgroundColor: selected ? colors.accent : 'transparent',
|
|
513
|
+
color: selected ? colors.onAccent : colors.accent,
|
|
514
|
+
fontFamily: 'inherit',
|
|
515
|
+
fontSize: 14,
|
|
516
|
+
lineHeight: 1.4,
|
|
517
|
+
cursor: 'pointer'
|
|
518
|
+
});
|
|
519
|
+
return _jsxs("div", {
|
|
520
|
+
style: {
|
|
521
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING
|
|
522
|
+
},
|
|
523
|
+
children: [slot.control === 'chips' ? _jsx("div", {
|
|
524
|
+
style: {
|
|
525
|
+
display: 'flex',
|
|
526
|
+
flexWrap: 'wrap',
|
|
527
|
+
gap: 8,
|
|
528
|
+
justifyContent: align === 'center' ? 'center' : 'flex-start'
|
|
529
|
+
},
|
|
530
|
+
children: slot.options.map(option => _jsx("button", {
|
|
531
|
+
type: "button",
|
|
532
|
+
"aria-pressed": picked.includes(option.value),
|
|
533
|
+
style: chip(picked.includes(option.value)),
|
|
534
|
+
onClick: () => toggle(option.value),
|
|
535
|
+
children: option.label
|
|
536
|
+
}, option.value))
|
|
537
|
+
}) : _jsx("div", {
|
|
538
|
+
style: {
|
|
539
|
+
display: 'grid',
|
|
540
|
+
gap: 6
|
|
541
|
+
},
|
|
542
|
+
children: slot.options.map(option => _jsxs("label", {
|
|
543
|
+
style: {
|
|
544
|
+
...optionStyle,
|
|
545
|
+
display: 'flex',
|
|
546
|
+
alignItems: 'center',
|
|
547
|
+
gap: 8,
|
|
548
|
+
cursor: 'pointer'
|
|
549
|
+
},
|
|
550
|
+
children: [_jsx("input", {
|
|
551
|
+
type: "checkbox",
|
|
552
|
+
checked: picked.includes(option.value),
|
|
553
|
+
onChange: () => toggle(option.value),
|
|
554
|
+
style: {
|
|
555
|
+
accentColor: colors.accent,
|
|
556
|
+
width: 16,
|
|
557
|
+
height: 16,
|
|
558
|
+
margin: 0
|
|
559
|
+
}
|
|
560
|
+
}), option.label]
|
|
561
|
+
}, option.value))
|
|
562
|
+
}), notice ? _jsx("div", {
|
|
563
|
+
style: {
|
|
564
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.caption,
|
|
565
|
+
color: colors.error,
|
|
566
|
+
paddingTop: 4
|
|
567
|
+
},
|
|
568
|
+
children: notice
|
|
569
|
+
}) : null, phase === 'error' ? _jsx("div", {
|
|
570
|
+
style: {
|
|
571
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.caption,
|
|
572
|
+
color: colors.error,
|
|
573
|
+
paddingTop: 4
|
|
574
|
+
},
|
|
575
|
+
children: WEB_POPUP_RETRY_ERROR
|
|
576
|
+
}) : null, _jsx("div", {
|
|
577
|
+
style: {
|
|
578
|
+
padding: WEB_POPUP_BUTTON_SLOT_PADDING
|
|
579
|
+
},
|
|
580
|
+
children: _jsx("button", {
|
|
581
|
+
type: "button",
|
|
582
|
+
style: {
|
|
583
|
+
...webPopupButtonPillStyle(slot.submitVariant, colors),
|
|
584
|
+
...(submitting ? {
|
|
585
|
+
opacity: 0.7
|
|
586
|
+
} : {})
|
|
587
|
+
},
|
|
588
|
+
onClick: submit,
|
|
589
|
+
children: slot.submitLabel
|
|
590
|
+
})
|
|
591
|
+
})]
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
// One rating button: a number for nps, a star for stars, a thumb for thumbs.
|
|
595
|
+
function RatingButton({
|
|
596
|
+
score,
|
|
597
|
+
scale,
|
|
598
|
+
selected,
|
|
599
|
+
colors,
|
|
600
|
+
onPick
|
|
601
|
+
}) {
|
|
602
|
+
const base = {
|
|
603
|
+
display: 'inline-flex',
|
|
604
|
+
alignItems: 'center',
|
|
605
|
+
justifyContent: 'center',
|
|
606
|
+
padding: 0,
|
|
607
|
+
border: `1px solid ${colors.accent}`,
|
|
608
|
+
backgroundColor: selected ? colors.accent : 'transparent',
|
|
609
|
+
color: selected ? colors.onAccent : colors.accent,
|
|
610
|
+
fontFamily: 'inherit',
|
|
611
|
+
fontSize: 14,
|
|
612
|
+
lineHeight: 1,
|
|
613
|
+
cursor: 'pointer'
|
|
614
|
+
};
|
|
615
|
+
if (scale === 'nps') {
|
|
616
|
+
return _jsx("button", {
|
|
617
|
+
type: "button",
|
|
618
|
+
"aria-label": `Rate ${score}`,
|
|
619
|
+
"aria-pressed": selected,
|
|
620
|
+
style: {
|
|
621
|
+
...base,
|
|
622
|
+
width: 30,
|
|
623
|
+
height: 32,
|
|
624
|
+
borderRadius: 6
|
|
625
|
+
},
|
|
626
|
+
onClick: onPick,
|
|
627
|
+
children: score
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const thumbs = scale === 'thumbs';
|
|
631
|
+
// Icon-only buttons need a name of their own; the nps row says it in text.
|
|
632
|
+
let label = `Rate ${score}`;
|
|
633
|
+
if (thumbs) label = score === 1 ? 'Thumbs up' : 'Thumbs down';
|
|
634
|
+
return _jsx("button", {
|
|
635
|
+
type: "button",
|
|
636
|
+
"aria-label": label,
|
|
637
|
+
"aria-pressed": selected,
|
|
638
|
+
style: {
|
|
639
|
+
...base,
|
|
640
|
+
width: thumbs ? 44 : 34,
|
|
641
|
+
height: thumbs ? 40 : 34,
|
|
642
|
+
borderRadius: thumbs ? 8 : 6
|
|
643
|
+
},
|
|
644
|
+
onClick: onPick,
|
|
645
|
+
children: _jsx("svg", {
|
|
646
|
+
width: thumbs ? 22 : 20,
|
|
647
|
+
height: thumbs ? 22 : 20,
|
|
648
|
+
viewBox: "0 0 24 24",
|
|
649
|
+
fill: "none",
|
|
650
|
+
stroke: "currentColor",
|
|
651
|
+
strokeWidth: 2,
|
|
652
|
+
strokeLinecap: "round",
|
|
653
|
+
strokeLinejoin: "round",
|
|
654
|
+
"aria-hidden": "true",
|
|
655
|
+
children: _jsx("path", {
|
|
656
|
+
d: thumbs ? WEB_POPUP_THUMB_UP_PATH : WEB_POPUP_GLYPH_PATHS.star,
|
|
657
|
+
...(thumbs && score === 0 ? {
|
|
658
|
+
transform: 'rotate(180 12 12)'
|
|
659
|
+
} : {})
|
|
660
|
+
})
|
|
661
|
+
})
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function RatingSlotView({
|
|
665
|
+
slot,
|
|
666
|
+
align,
|
|
667
|
+
inverse,
|
|
668
|
+
colors,
|
|
669
|
+
onSubmitRating
|
|
670
|
+
}) {
|
|
671
|
+
const [score, setScore] = useState(null);
|
|
672
|
+
const [phase, setPhase] = useState('idle');
|
|
673
|
+
if (phase === 'success') {
|
|
674
|
+
return _jsx(SlotMessageView, {
|
|
675
|
+
message: slot.successMessage,
|
|
676
|
+
align: align,
|
|
677
|
+
inverse: inverse,
|
|
678
|
+
colors: colors
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const send = value => {
|
|
682
|
+
if (!onSubmitRating || !slot.eventName || phase === 'submitting') return;
|
|
683
|
+
setPhase('submitting');
|
|
684
|
+
// See ChoiceSlotView: a synchronous throw must land in 'error' too.
|
|
685
|
+
Promise.resolve().then(() => onSubmitRating({
|
|
686
|
+
slotKey: slot.key,
|
|
687
|
+
eventName: slot.eventName,
|
|
688
|
+
score: value,
|
|
689
|
+
scale: slot.scale
|
|
690
|
+
})).then(() => setPhase('success'), () => setPhase('error'));
|
|
691
|
+
};
|
|
692
|
+
const pick = value => {
|
|
693
|
+
// Same guard as `send`: otherwise a tap during an in-flight submit moves the
|
|
694
|
+
// highlight while the host keeps the score that was actually sent.
|
|
695
|
+
if (phase === 'submitting') return;
|
|
696
|
+
setScore(value);
|
|
697
|
+
if (slot.submitMode === 'instant') send(value);
|
|
698
|
+
};
|
|
699
|
+
const scores = webPopupRatingScores(slot.scale);
|
|
700
|
+
const labelColor = webPopupTextColor('caption', colors, inverse);
|
|
701
|
+
const edgeLabelStyle = {
|
|
702
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.caption,
|
|
703
|
+
...(labelColor ? {
|
|
704
|
+
color: labelColor
|
|
705
|
+
} : {})
|
|
706
|
+
};
|
|
707
|
+
// A star scale fills every button up to the pick — the row IS the score.
|
|
708
|
+
const isSelected = value => slot.scale === 'stars' ? score !== null && value <= score : score === value;
|
|
709
|
+
return _jsxs("div", {
|
|
710
|
+
style: {
|
|
711
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING
|
|
712
|
+
},
|
|
713
|
+
children: [_jsx("div", {
|
|
714
|
+
style: {
|
|
715
|
+
display: 'flex',
|
|
716
|
+
flexWrap: 'wrap',
|
|
717
|
+
gap: 6,
|
|
718
|
+
justifyContent: align === 'center' ? 'center' : 'flex-start'
|
|
719
|
+
},
|
|
720
|
+
children: scores.map(value => _jsx(RatingButton, {
|
|
721
|
+
score: value,
|
|
722
|
+
scale: slot.scale,
|
|
723
|
+
selected: isSelected(value),
|
|
724
|
+
colors: colors,
|
|
725
|
+
onPick: () => pick(value)
|
|
726
|
+
}, value))
|
|
727
|
+
}), slot.labels.low || slot.labels.high ? _jsxs("div", {
|
|
728
|
+
style: {
|
|
729
|
+
display: 'flex',
|
|
730
|
+
justifyContent: 'space-between',
|
|
731
|
+
gap: 8,
|
|
732
|
+
paddingTop: 4
|
|
733
|
+
},
|
|
734
|
+
children: [_jsx("span", {
|
|
735
|
+
style: edgeLabelStyle,
|
|
736
|
+
children: slot.labels.low
|
|
737
|
+
}), _jsx("span", {
|
|
738
|
+
style: edgeLabelStyle,
|
|
739
|
+
children: slot.labels.high
|
|
740
|
+
})]
|
|
741
|
+
}) : null, phase === 'error' ? _jsx("div", {
|
|
742
|
+
style: {
|
|
743
|
+
...WEB_POPUP_TEXT_VARIANT_STYLES.caption,
|
|
744
|
+
color: colors.error,
|
|
745
|
+
paddingTop: 4
|
|
746
|
+
},
|
|
747
|
+
children: WEB_POPUP_RETRY_ERROR
|
|
748
|
+
}) : null, slot.submitMode === 'button' ? _jsx("div", {
|
|
749
|
+
style: {
|
|
750
|
+
padding: WEB_POPUP_BUTTON_SLOT_PADDING
|
|
751
|
+
},
|
|
752
|
+
children: _jsx("button", {
|
|
753
|
+
type: "button",
|
|
754
|
+
style: {
|
|
755
|
+
...webPopupButtonPillStyle('primary', colors),
|
|
756
|
+
...(phase === 'submitting' ? {
|
|
757
|
+
opacity: 0.7
|
|
758
|
+
} : {})
|
|
759
|
+
},
|
|
760
|
+
onClick: () => score === null ? undefined : send(score),
|
|
761
|
+
children: slot.submitLabel
|
|
762
|
+
})
|
|
763
|
+
}) : null]
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function GlyphSlotView({
|
|
767
|
+
slot,
|
|
768
|
+
align,
|
|
769
|
+
inverse,
|
|
770
|
+
colors
|
|
771
|
+
}) {
|
|
772
|
+
// No authored color: the accent, or the inverse copy color over a dark image
|
|
773
|
+
// (where the accent can be the one thing that doesn't read).
|
|
774
|
+
const color = slot.color || (inverse ? colors.textInverse : colors.accent);
|
|
775
|
+
return _jsx("div", {
|
|
776
|
+
style: {
|
|
777
|
+
padding: WEB_POPUP_TEXT_SLOT_PADDING,
|
|
778
|
+
textAlign: align,
|
|
779
|
+
lineHeight: 0
|
|
780
|
+
},
|
|
781
|
+
children: _jsx("svg", {
|
|
782
|
+
width: slot.size,
|
|
783
|
+
height: slot.size,
|
|
784
|
+
viewBox: "0 0 24 24",
|
|
785
|
+
fill: "none",
|
|
786
|
+
stroke: color,
|
|
787
|
+
strokeWidth: 2,
|
|
788
|
+
strokeLinecap: "round",
|
|
789
|
+
strokeLinejoin: "round",
|
|
790
|
+
"aria-hidden": "true",
|
|
791
|
+
style: {
|
|
792
|
+
display: 'inline-block'
|
|
793
|
+
},
|
|
794
|
+
children: _jsx("path", {
|
|
795
|
+
d: WEB_POPUP_GLYPH_PATHS[slot.name]
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
// A switch, not a chain of `&&`: the compiler is what notices a slot type with
|
|
801
|
+
// no view. Exported so the editor canvas draws these slots with it, not a copy.
|
|
802
|
+
export function WebPopupSlotView({
|
|
803
|
+
slot,
|
|
804
|
+
align,
|
|
805
|
+
inverse,
|
|
806
|
+
colors,
|
|
807
|
+
mode = 'live',
|
|
808
|
+
onRequestClose,
|
|
809
|
+
onPageChange,
|
|
810
|
+
onSubmitForm,
|
|
811
|
+
onSubmitChoice,
|
|
812
|
+
onSubmitRating,
|
|
813
|
+
onRequestSubscribe
|
|
814
|
+
}) {
|
|
815
|
+
// A page jump nobody handles is a no-op, not a crash — the editor canvas
|
|
816
|
+
// and the previews mount this without any navigation of their own.
|
|
817
|
+
const pageChange = onPageChange ?? (() => {});
|
|
818
|
+
switch (slot.type) {
|
|
819
|
+
case 'text':
|
|
820
|
+
return _jsx(TextSlotView, {
|
|
821
|
+
slot: slot,
|
|
822
|
+
align: align,
|
|
823
|
+
inverse: inverse,
|
|
824
|
+
colors: colors
|
|
825
|
+
});
|
|
826
|
+
case 'button':
|
|
827
|
+
return _jsx(ButtonSlotView, {
|
|
828
|
+
slot: slot,
|
|
829
|
+
colors: colors,
|
|
830
|
+
onRequestClose: onRequestClose,
|
|
831
|
+
onPageChange: pageChange,
|
|
832
|
+
onRequestSubscribe: onRequestSubscribe
|
|
833
|
+
});
|
|
834
|
+
case 'email-subscription-form':
|
|
835
|
+
return _jsx(FormSlotView, {
|
|
836
|
+
slot: slot,
|
|
837
|
+
align: align,
|
|
838
|
+
inverse: inverse,
|
|
839
|
+
colors: colors,
|
|
840
|
+
onSubmitForm: onSubmitForm
|
|
841
|
+
});
|
|
842
|
+
case 'timer':
|
|
843
|
+
return _jsx(TimerSlotView, {
|
|
844
|
+
slot: slot,
|
|
845
|
+
align: align,
|
|
846
|
+
inverse: inverse,
|
|
847
|
+
colors: colors,
|
|
848
|
+
mode: mode,
|
|
849
|
+
onRequestClose: onRequestClose,
|
|
850
|
+
onPageChange: pageChange
|
|
851
|
+
});
|
|
852
|
+
case 'choice':
|
|
853
|
+
return _jsx(ChoiceSlotView, {
|
|
854
|
+
slot: slot,
|
|
855
|
+
align: align,
|
|
856
|
+
inverse: inverse,
|
|
857
|
+
colors: colors,
|
|
858
|
+
onSubmitChoice: onSubmitChoice
|
|
859
|
+
});
|
|
860
|
+
case 'rating':
|
|
861
|
+
return _jsx(RatingSlotView, {
|
|
862
|
+
slot: slot,
|
|
863
|
+
align: align,
|
|
864
|
+
inverse: inverse,
|
|
865
|
+
colors: colors,
|
|
866
|
+
onSubmitRating: onSubmitRating
|
|
867
|
+
});
|
|
868
|
+
case 'glyph':
|
|
869
|
+
return _jsx(GlyphSlotView, {
|
|
870
|
+
slot: slot,
|
|
871
|
+
align: align,
|
|
872
|
+
inverse: inverse,
|
|
873
|
+
colors: colors
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// A Content leaf: the named zone's slots (texts, buttons and forms in order)
|
|
878
|
+
// as a grid stack. An unknown zone key renders as an empty spot, not a crash.
|
|
879
|
+
function ContentNodeView({
|
|
880
|
+
node,
|
|
881
|
+
page,
|
|
882
|
+
colors,
|
|
883
|
+
mode,
|
|
884
|
+
leaf,
|
|
885
|
+
onRequestClose,
|
|
886
|
+
onPageChange,
|
|
887
|
+
onSubmitForm,
|
|
888
|
+
onSubmitChoice,
|
|
889
|
+
onSubmitRating,
|
|
890
|
+
onRequestSubscribe
|
|
891
|
+
}) {
|
|
892
|
+
const zone = page.contents.find(z => z.key === node.props.content);
|
|
893
|
+
if (!zone) return null;
|
|
894
|
+
if (leaf?.renderZone) {
|
|
895
|
+
return _jsx("div", {
|
|
896
|
+
style: {
|
|
897
|
+
minWidth: 0
|
|
898
|
+
},
|
|
899
|
+
children: leaf.renderZone(zone, node)
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const align = node.props.align ?? 'left';
|
|
903
|
+
const inverse = node.props.tone === 'inverse';
|
|
904
|
+
return _jsx("div", {
|
|
905
|
+
style: {
|
|
906
|
+
minWidth: 0
|
|
907
|
+
},
|
|
908
|
+
children: zone.slots.map((slot, index) => {
|
|
909
|
+
const previous = zone.slots[index - 1];
|
|
910
|
+
const marginTop = previous ? webPopupSlotGap(webPopupSlotRole(previous), webPopupSlotRole(slot)) : 0;
|
|
911
|
+
return _jsx("div", {
|
|
912
|
+
style: marginTop ? {
|
|
913
|
+
marginTop
|
|
914
|
+
} : undefined,
|
|
915
|
+
children: _jsx(WebPopupSlotView, {
|
|
916
|
+
slot: slot,
|
|
917
|
+
align: align,
|
|
918
|
+
inverse: inverse,
|
|
919
|
+
colors: colors,
|
|
920
|
+
mode: mode,
|
|
921
|
+
onRequestClose: onRequestClose,
|
|
922
|
+
onPageChange: onPageChange,
|
|
923
|
+
onSubmitForm: onSubmitForm,
|
|
924
|
+
onSubmitChoice: onSubmitChoice,
|
|
925
|
+
onSubmitRating: onSubmitRating,
|
|
926
|
+
onRequestSubscribe: onRequestSubscribe
|
|
927
|
+
})
|
|
928
|
+
}, slot.key);
|
|
929
|
+
})
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
// The playable url of a media param, or undefined when it has no video.
|
|
933
|
+
function mediaVideoSrc(image) {
|
|
934
|
+
return image?.video ? safeUrl(image.video.src) : undefined;
|
|
935
|
+
}
|
|
936
|
+
// A media param's video: autoplay only muted, playsInline to keep iOS out of
|
|
937
|
+
// fullscreen, and in 'preview' it holds on the poster instead of streaming.
|
|
938
|
+
function MediaVideo({
|
|
939
|
+
image,
|
|
940
|
+
poster,
|
|
941
|
+
src,
|
|
942
|
+
style,
|
|
943
|
+
mode
|
|
944
|
+
}) {
|
|
945
|
+
return _jsx("video", {
|
|
946
|
+
src: src,
|
|
947
|
+
poster: poster || undefined,
|
|
948
|
+
style: style,
|
|
949
|
+
autoPlay: mode === 'live',
|
|
950
|
+
muted: true,
|
|
951
|
+
playsInline: true,
|
|
952
|
+
preload: "metadata",
|
|
953
|
+
loop: image.video?.loop !== false,
|
|
954
|
+
controls: image.video?.controls === true
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
function ImageNodeView({
|
|
958
|
+
node,
|
|
959
|
+
page,
|
|
960
|
+
leaf,
|
|
961
|
+
mode,
|
|
962
|
+
scheme
|
|
963
|
+
}) {
|
|
964
|
+
const image = page.images.find(i => i.key === node.props.image);
|
|
965
|
+
if (!image) return null;
|
|
966
|
+
// The override sees srcless params too — an editor wants a placeholder
|
|
967
|
+
// where the runtime shows nothing.
|
|
968
|
+
if (leaf?.renderImage) return _jsx(_Fragment, {
|
|
969
|
+
children: leaf.renderImage(image, node)
|
|
970
|
+
});
|
|
971
|
+
const video = mediaVideoSrc(image);
|
|
972
|
+
const src = webPopupImageSrc(image, scheme);
|
|
973
|
+
if (!src && !video) return null;
|
|
974
|
+
const style = {
|
|
975
|
+
display: 'block',
|
|
976
|
+
width: '100%',
|
|
977
|
+
minWidth: 0
|
|
978
|
+
};
|
|
979
|
+
if (image.fit) {
|
|
980
|
+
style.height = '100%';
|
|
981
|
+
style.objectFit = image.fit;
|
|
982
|
+
} else {
|
|
983
|
+
style.height = 'auto';
|
|
984
|
+
}
|
|
985
|
+
if (image.blur) style.filter = `blur(${image.blur}px)`;
|
|
986
|
+
if (video) {
|
|
987
|
+
return _jsx(MediaVideo, {
|
|
988
|
+
image: image,
|
|
989
|
+
poster: src,
|
|
990
|
+
src: video,
|
|
991
|
+
style: style,
|
|
992
|
+
mode: mode
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
return _jsx("img", {
|
|
996
|
+
src: src,
|
|
997
|
+
alt: "",
|
|
998
|
+
style: style
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
// Both layered backgrounds (blur, video) share this shell: the color/poster
|
|
1002
|
+
// paints on an outer grid box, the content rides an inner layer covering it.
|
|
1003
|
+
function LayeredBackgroundBox({
|
|
1004
|
+
style,
|
|
1005
|
+
outer,
|
|
1006
|
+
inner,
|
|
1007
|
+
layer,
|
|
1008
|
+
children
|
|
1009
|
+
}) {
|
|
1010
|
+
const {
|
|
1011
|
+
backgroundColor,
|
|
1012
|
+
...rest
|
|
1013
|
+
} = style;
|
|
1014
|
+
return _jsxs("div", {
|
|
1015
|
+
style: {
|
|
1016
|
+
display: 'grid',
|
|
1017
|
+
minWidth: 0,
|
|
1018
|
+
backgroundColor,
|
|
1019
|
+
...outer
|
|
1020
|
+
},
|
|
1021
|
+
children: [layer, _jsx("div", {
|
|
1022
|
+
style: {
|
|
1023
|
+
...rest,
|
|
1024
|
+
...inner
|
|
1025
|
+
},
|
|
1026
|
+
children: children
|
|
1027
|
+
})]
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
// The tree interpreter: containers → grid divs, leaves → zone/image views.
|
|
1031
|
+
function LayoutNodeView({
|
|
1032
|
+
node,
|
|
1033
|
+
page,
|
|
1034
|
+
colors,
|
|
1035
|
+
mode,
|
|
1036
|
+
scheme,
|
|
1037
|
+
leaf,
|
|
1038
|
+
onRequestClose,
|
|
1039
|
+
onPageChange,
|
|
1040
|
+
onSubmitForm,
|
|
1041
|
+
onSubmitChoice,
|
|
1042
|
+
onSubmitRating,
|
|
1043
|
+
onRequestSubscribe
|
|
1044
|
+
}) {
|
|
1045
|
+
if (node.type === 'Content') {
|
|
1046
|
+
return _jsx(ContentNodeView, {
|
|
1047
|
+
node: node,
|
|
1048
|
+
page: page,
|
|
1049
|
+
colors: colors,
|
|
1050
|
+
mode: mode,
|
|
1051
|
+
scheme: scheme,
|
|
1052
|
+
leaf: leaf,
|
|
1053
|
+
onRequestClose: onRequestClose,
|
|
1054
|
+
onPageChange: onPageChange,
|
|
1055
|
+
onSubmitForm: onSubmitForm,
|
|
1056
|
+
onSubmitChoice: onSubmitChoice,
|
|
1057
|
+
onSubmitRating: onSubmitRating,
|
|
1058
|
+
onRequestSubscribe: onRequestSubscribe
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if (node.type === 'Image') {
|
|
1062
|
+
return _jsx(ImageNodeView, {
|
|
1063
|
+
node: node,
|
|
1064
|
+
page: page,
|
|
1065
|
+
leaf: leaf,
|
|
1066
|
+
mode: mode,
|
|
1067
|
+
scheme: scheme
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
const bgKey = node.props?.backgroundImage;
|
|
1071
|
+
const background = bgKey ? page.images.find(i => i.key === bgKey) : undefined;
|
|
1072
|
+
const blur = background?.blur ?? 0;
|
|
1073
|
+
const dim = background?.dim ?? 0;
|
|
1074
|
+
const bgVideo = mediaVideoSrc(background);
|
|
1075
|
+
const bgSrc = background ? webPopupImageSrc(background, scheme) : undefined;
|
|
1076
|
+
const style = webPopupContainerStyle(node, blur > 0 || bgVideo ? undefined : bgSrc || undefined, dim);
|
|
1077
|
+
if (bgKey) style.backgroundColor = WEB_POPUP_BG_SECTION_FALLBACK;
|
|
1078
|
+
const children = node.children.map((child, index) => _jsx(LayoutNodeView
|
|
1079
|
+
// Layout nodes are static per page render — positional keys are fine.
|
|
1080
|
+
, {
|
|
1081
|
+
node: child,
|
|
1082
|
+
page: page,
|
|
1083
|
+
colors: colors,
|
|
1084
|
+
mode: mode,
|
|
1085
|
+
scheme: scheme,
|
|
1086
|
+
leaf: leaf,
|
|
1087
|
+
onRequestClose: onRequestClose,
|
|
1088
|
+
onPageChange: onPageChange,
|
|
1089
|
+
onSubmitForm: onSubmitForm,
|
|
1090
|
+
onSubmitChoice: onSubmitChoice,
|
|
1091
|
+
onSubmitRating: onSubmitRating,
|
|
1092
|
+
onRequestSubscribe: onRequestSubscribe
|
|
1093
|
+
}, index));
|
|
1094
|
+
if (bgVideo && background) {
|
|
1095
|
+
// A background video can't ride the css background property, so it paints
|
|
1096
|
+
// as its own layer; the scrim moves onto the content layer above it.
|
|
1097
|
+
const scrim = webPopupScrimGradient(dim);
|
|
1098
|
+
return _jsx(LayeredBackgroundBox, {
|
|
1099
|
+
style: style,
|
|
1100
|
+
outer: {
|
|
1101
|
+
position: 'relative',
|
|
1102
|
+
overflow: 'hidden',
|
|
1103
|
+
...(bgSrc ? webPopupBackgroundImageStyle(bgSrc) : {})
|
|
1104
|
+
},
|
|
1105
|
+
inner: {
|
|
1106
|
+
position: 'relative',
|
|
1107
|
+
...(scrim ? {
|
|
1108
|
+
backgroundImage: scrim
|
|
1109
|
+
} : {})
|
|
1110
|
+
},
|
|
1111
|
+
layer: _jsx(MediaVideo, {
|
|
1112
|
+
image: background,
|
|
1113
|
+
poster: bgSrc ?? '',
|
|
1114
|
+
src: bgVideo,
|
|
1115
|
+
mode: mode,
|
|
1116
|
+
style: {
|
|
1117
|
+
position: 'absolute',
|
|
1118
|
+
inset: 0,
|
|
1119
|
+
width: '100%',
|
|
1120
|
+
height: '100%',
|
|
1121
|
+
objectFit: 'cover',
|
|
1122
|
+
...(blur > 0 ? {
|
|
1123
|
+
filter: `blur(${blur}px)`
|
|
1124
|
+
} : {})
|
|
1125
|
+
}
|
|
1126
|
+
}),
|
|
1127
|
+
children: children
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
if (blur > 0) {
|
|
1131
|
+
// backdrop-filter blurs what's painted BEHIND its element, never the
|
|
1132
|
+
// element's own background — so the image paints on an outer box and the
|
|
1133
|
+
// laid-out content rides an inner blurring layer covering it edge to
|
|
1134
|
+
// edge (grid/padding move onto that layer). The outer box is a grid so
|
|
1135
|
+
// a stretched height (a 'stretch' page's root) passes through to the
|
|
1136
|
+
// layer actually carrying alignContent.
|
|
1137
|
+
return _jsx(LayeredBackgroundBox, {
|
|
1138
|
+
style: style,
|
|
1139
|
+
outer: bgSrc ? webPopupBackgroundImageStyle(bgSrc, dim) : undefined,
|
|
1140
|
+
inner: {
|
|
1141
|
+
backdropFilter: `blur(${blur}px)`,
|
|
1142
|
+
WebkitBackdropFilter: `blur(${blur}px)`
|
|
1143
|
+
},
|
|
1144
|
+
children: children
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
return _jsx("div", {
|
|
1148
|
+
style: style,
|
|
1149
|
+
children: children
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
// The popup box: the shared chrome (background, radius, shadow) around one
|
|
1153
|
+
// resolved page's layout tree. Exported for the editor — its canvas is this
|
|
1154
|
+
// very interpreter with `leafRenderers` swapping the Content/Image leaves
|
|
1155
|
+
// for editable views (the container interpretation stays this component's,
|
|
1156
|
+
// which is what keeps the canvas true to the runtime); the viewport chrome
|
|
1157
|
+
// (placement, overlay, enter animation) is the caller's business there.
|
|
1158
|
+
// After a page switch (`animatePage`) the incoming content fades in — keyed
|
|
1159
|
+
// by page so the animation restarts per switch — and an explicit px height
|
|
1160
|
+
// glides via the transition (auto heights just snap).
|
|
1161
|
+
export function WebPopupBox({
|
|
1162
|
+
page,
|
|
1163
|
+
params,
|
|
1164
|
+
mode = 'live',
|
|
1165
|
+
colorScheme = 'light',
|
|
1166
|
+
leafRenderers,
|
|
1167
|
+
animatePage = false,
|
|
1168
|
+
onRequestClose,
|
|
1169
|
+
onPageChange,
|
|
1170
|
+
onSubmitForm,
|
|
1171
|
+
onSubmitChoice,
|
|
1172
|
+
onSubmitRating,
|
|
1173
|
+
onRequestSubscribe
|
|
1174
|
+
}) {
|
|
1175
|
+
const colors = resolveWebPopupColors(params, colorScheme);
|
|
1176
|
+
const box = resolveWebPopupBoxChrome(params, colorScheme);
|
|
1177
|
+
const style = {
|
|
1178
|
+
position: 'relative',
|
|
1179
|
+
boxSizing: 'border-box',
|
|
1180
|
+
overflow: 'hidden',
|
|
1181
|
+
backgroundColor: colors.background,
|
|
1182
|
+
colorScheme: colors.scheme,
|
|
1183
|
+
fontFamily: WEB_POPUP_FONT_STACK,
|
|
1184
|
+
color: colors.text,
|
|
1185
|
+
borderRadius: params.borderRadius > 0 ? params.borderRadius : undefined,
|
|
1186
|
+
boxShadow: box.boxShadow && box.boxShadow !== 'none' ? box.boxShadow : undefined,
|
|
1187
|
+
transition: `height ${WEB_POPUP_PAGE_TRANSITION_MS}ms ease`
|
|
1188
|
+
};
|
|
1189
|
+
if (page.height) {
|
|
1190
|
+
// 'stretch' fills whatever box the caller gives (the fixed container pins
|
|
1191
|
+
// both vertical edges in live mode); a px height is the box's own.
|
|
1192
|
+
style.height = page.height === 'stretch' ? '100%' : page.height;
|
|
1193
|
+
style.overflowY = 'auto';
|
|
1194
|
+
}
|
|
1195
|
+
// A stretch page's tree gets the full box height to lay out in: the
|
|
1196
|
+
// min-height'd single-track grid stretches the root container, so its
|
|
1197
|
+
// alignContent can place the content vertically (a px height keeps the
|
|
1198
|
+
// pre-stretch behavior — content flows from the top).
|
|
1199
|
+
const pageStyle = {
|
|
1200
|
+
...(animatePage ? {
|
|
1201
|
+
animation: `pw-vpopup-page ${WEB_POPUP_PAGE_TRANSITION_MS}ms ease-out backwards`
|
|
1202
|
+
} : {}),
|
|
1203
|
+
...(page.height === 'stretch' ? {
|
|
1204
|
+
display: 'grid',
|
|
1205
|
+
minHeight: '100%'
|
|
1206
|
+
} : {})
|
|
1207
|
+
};
|
|
1208
|
+
return _jsx("div", {
|
|
1209
|
+
style: style,
|
|
1210
|
+
children: _jsx("div", {
|
|
1211
|
+
style: pageStyle,
|
|
1212
|
+
children: _jsx(LayoutNodeView, {
|
|
1213
|
+
node: page.layout,
|
|
1214
|
+
page: page,
|
|
1215
|
+
colors: colors,
|
|
1216
|
+
mode: mode,
|
|
1217
|
+
scheme: colors.scheme,
|
|
1218
|
+
leaf: leafRenderers,
|
|
1219
|
+
onRequestClose: onRequestClose,
|
|
1220
|
+
onPageChange: onPageChange ?? (() => {}),
|
|
1221
|
+
onSubmitForm: onSubmitForm,
|
|
1222
|
+
onSubmitChoice: onSubmitChoice,
|
|
1223
|
+
onSubmitRating: onSubmitRating,
|
|
1224
|
+
onRequestSubscribe: onRequestSubscribe
|
|
1225
|
+
})
|
|
1226
|
+
}, page.key)
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function CloseButton({
|
|
1230
|
+
colors,
|
|
1231
|
+
onRequestClose
|
|
1232
|
+
}) {
|
|
1233
|
+
return _jsx("button", {
|
|
1234
|
+
type: "button",
|
|
1235
|
+
"aria-label": "Close",
|
|
1236
|
+
onClick: onRequestClose,
|
|
1237
|
+
style: {
|
|
1238
|
+
position: 'absolute',
|
|
1239
|
+
top: 10,
|
|
1240
|
+
right: 10,
|
|
1241
|
+
width: 24,
|
|
1242
|
+
height: 24,
|
|
1243
|
+
padding: 0,
|
|
1244
|
+
border: 'none',
|
|
1245
|
+
borderRadius: '50%',
|
|
1246
|
+
background: webPopupColorAlpha(colors.surface, 0.92),
|
|
1247
|
+
boxShadow: '0 1px 3px rgba(16, 24, 40, 0.25)',
|
|
1248
|
+
color: colors.chromeIcon,
|
|
1249
|
+
fontSize: 13,
|
|
1250
|
+
lineHeight: 1,
|
|
1251
|
+
cursor: 'pointer'
|
|
1252
|
+
},
|
|
1253
|
+
children: "\u2715"
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
// Fixed viewport placement from the placement param: anchored edges get the
|
|
1257
|
+
// offset, unanchored axes center via translate. No width = stretch between
|
|
1258
|
+
// the side gutters; height 'stretch' = pinned to both vertical edges the same
|
|
1259
|
+
// way. The size is the ACTIVE page's (resolved page ?? chrome), so the box
|
|
1260
|
+
// re-anchors itself when a page switch changes it.
|
|
1261
|
+
function fixedContainerStyle(params, width, height) {
|
|
1262
|
+
const {
|
|
1263
|
+
placement
|
|
1264
|
+
} = params;
|
|
1265
|
+
const style = {
|
|
1266
|
+
position: 'fixed',
|
|
1267
|
+
boxSizing: 'border-box',
|
|
1268
|
+
maxWidth: placementMaxSize(placement),
|
|
1269
|
+
maxHeight: placementMaxSize(placement),
|
|
1270
|
+
// The widget host is pointer-transparent (a corner banner must not block
|
|
1271
|
+
// the page); the popup itself takes clicks back.
|
|
1272
|
+
pointerEvents: 'auto'
|
|
1273
|
+
};
|
|
1274
|
+
if (width) {
|
|
1275
|
+
style.width = width;
|
|
1276
|
+
const h = placement.h;
|
|
1277
|
+
if (h === 'left') style.left = placement.offset;else if (h === 'right') style.right = placement.offset;else style.left = '50%';
|
|
1278
|
+
} else {
|
|
1279
|
+
// Stretch: pin both side edges regardless of the horizontal anchor.
|
|
1280
|
+
style.left = placement.offset;
|
|
1281
|
+
style.right = placement.offset;
|
|
1282
|
+
}
|
|
1283
|
+
if (height === 'stretch') {
|
|
1284
|
+
// Stretch: pin both vertical edges; the v anchor has nothing to place.
|
|
1285
|
+
style.top = placement.offset;
|
|
1286
|
+
style.bottom = placement.offset;
|
|
1287
|
+
} else if (placement.v === 'top') style.top = placement.offset;else if (placement.v === 'bottom') style.bottom = placement.offset;else style.top = '50%';
|
|
1288
|
+
const tx = width && placement.h === 'center' ? '-50%' : '0';
|
|
1289
|
+
const ty = placement.v === 'middle' && height !== 'stretch' ? '-50%' : '0';
|
|
1290
|
+
if (tx !== '0' || ty !== '0') style.transform = `translate(${tx}, ${ty})`;
|
|
1291
|
+
const anim = ANIMATION_STYLE[params.animation];
|
|
1292
|
+
if (anim) style.animation = anim;
|
|
1293
|
+
// A page switch that changes the (explicit) width glides instead of
|
|
1294
|
+
// jumping; the anchored edges keep the box pinned while it resizes.
|
|
1295
|
+
style.transition = `width ${WEB_POPUP_PAGE_TRANSITION_MS}ms ease`;
|
|
1296
|
+
return style;
|
|
1297
|
+
}
|
|
1298
|
+
export function WebPopupReader({
|
|
1299
|
+
params,
|
|
1300
|
+
mode = 'live',
|
|
1301
|
+
colorScheme = 'auto',
|
|
1302
|
+
onRequestClose,
|
|
1303
|
+
onSubmitForm,
|
|
1304
|
+
onSubmitChoice,
|
|
1305
|
+
onSubmitRating,
|
|
1306
|
+
onRequestSubscribe,
|
|
1307
|
+
leafRenderers
|
|
1308
|
+
}) {
|
|
1309
|
+
// null = never navigated: show the start page. Guarded against keys that
|
|
1310
|
+
// don't exist (anymore) — an edited/older document falls back gracefully.
|
|
1311
|
+
const [pageKey, setPageKey] = useState(null);
|
|
1312
|
+
// Only the active page's images are in the DOM — the rest would start
|
|
1313
|
+
// downloading on the page switch, leaving the previous picture on screen
|
|
1314
|
+
// until the new one arrives. Warm the browser cache for every page up front.
|
|
1315
|
+
useEffect(() => {
|
|
1316
|
+
resolveWebPopupPages(params).forEach(p => p.images.forEach(({
|
|
1317
|
+
src,
|
|
1318
|
+
srcDark
|
|
1319
|
+
}) => {
|
|
1320
|
+
[src, srcDark].forEach(url => {
|
|
1321
|
+
if (url) new Image().src = url;
|
|
1322
|
+
});
|
|
1323
|
+
}));
|
|
1324
|
+
}, [params]);
|
|
1325
|
+
const scheme = useColorScheme(colorScheme, webPopupDarkEnabled(params));
|
|
1326
|
+
const pages = resolveWebPopupPages(params);
|
|
1327
|
+
if (pages.length === 0) return null;
|
|
1328
|
+
const page = pages.find(p => p.key === pageKey) ?? startPageOf(params, pages);
|
|
1329
|
+
// null = the initial page: entry is the chrome's business (the enter
|
|
1330
|
+
// animation in live mode); the page fade only plays on actual switches.
|
|
1331
|
+
const animatePage = pageKey !== null;
|
|
1332
|
+
if (mode === 'preview') {
|
|
1333
|
+
return _jsxs("div", {
|
|
1334
|
+
style: {
|
|
1335
|
+
position: 'relative',
|
|
1336
|
+
width: page.width ?? '100%',
|
|
1337
|
+
transition: `width ${WEB_POPUP_PAGE_TRANSITION_MS}ms ease`
|
|
1338
|
+
},
|
|
1339
|
+
children: [_jsx("style", {
|
|
1340
|
+
children: WEB_POPUP_KEYFRAMES
|
|
1341
|
+
}), _jsx(WebPopupBox, {
|
|
1342
|
+
page: page,
|
|
1343
|
+
params: params,
|
|
1344
|
+
mode: mode,
|
|
1345
|
+
colorScheme: scheme,
|
|
1346
|
+
leafRenderers: leafRenderers,
|
|
1347
|
+
animatePage: animatePage,
|
|
1348
|
+
onRequestClose: onRequestClose,
|
|
1349
|
+
onPageChange: setPageKey,
|
|
1350
|
+
onSubmitForm: onSubmitForm,
|
|
1351
|
+
onSubmitChoice: onSubmitChoice,
|
|
1352
|
+
onSubmitRating: onSubmitRating,
|
|
1353
|
+
onRequestSubscribe: onRequestSubscribe
|
|
1354
|
+
})]
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
const {
|
|
1358
|
+
overlay
|
|
1359
|
+
} = resolveWebPopupBoxChrome(params, scheme);
|
|
1360
|
+
return _jsxs(_Fragment, {
|
|
1361
|
+
children: [_jsx("style", {
|
|
1362
|
+
children: WEB_POPUP_KEYFRAMES
|
|
1363
|
+
}), overlay && _jsx("div", {
|
|
1364
|
+
style: {
|
|
1365
|
+
position: 'fixed',
|
|
1366
|
+
inset: 0,
|
|
1367
|
+
backgroundColor: overlay.color,
|
|
1368
|
+
opacity: overlay.opacity,
|
|
1369
|
+
animation: 'pw-vpopup-overlay 0.2s ease-out backwards',
|
|
1370
|
+
pointerEvents: 'auto'
|
|
1371
|
+
},
|
|
1372
|
+
onClick: onRequestClose
|
|
1373
|
+
}), _jsxs("div", {
|
|
1374
|
+
style: fixedContainerStyle(params, page.width, page.height),
|
|
1375
|
+
children: [_jsx(WebPopupBox, {
|
|
1376
|
+
page: page,
|
|
1377
|
+
params: params,
|
|
1378
|
+
mode: mode,
|
|
1379
|
+
colorScheme: scheme,
|
|
1380
|
+
leafRenderers: leafRenderers,
|
|
1381
|
+
animatePage: animatePage,
|
|
1382
|
+
onRequestClose: onRequestClose,
|
|
1383
|
+
onPageChange: setPageKey,
|
|
1384
|
+
onSubmitForm: onSubmitForm,
|
|
1385
|
+
onSubmitChoice: onSubmitChoice,
|
|
1386
|
+
onSubmitRating: onSubmitRating,
|
|
1387
|
+
onRequestSubscribe: onRequestSubscribe
|
|
1388
|
+
}), _jsx(CloseButton, {
|
|
1389
|
+
colors: resolveWebPopupColors(params, scheme),
|
|
1390
|
+
onRequestClose: onRequestClose
|
|
1391
|
+
})]
|
|
1392
|
+
})]
|
|
1393
|
+
});
|
|
1394
|
+
}
|