@eazo/sdk 0.13.0 → 0.15.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.
Files changed (43) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/internal/banner-ui/app-info.d.ts +78 -0
  4. package/dist/internal/banner-ui/app-info.d.ts.map +1 -0
  5. package/dist/internal/banner-ui/app-info.js +62 -0
  6. package/dist/internal/banner-ui/app-info.js.map +1 -0
  7. package/dist/internal/banner-ui/icons.d.ts +18 -4
  8. package/dist/internal/banner-ui/icons.d.ts.map +1 -1
  9. package/dist/internal/banner-ui/icons.js +58 -4
  10. package/dist/internal/banner-ui/icons.js.map +1 -1
  11. package/dist/internal/banner-ui/index.d.ts +16 -3
  12. package/dist/internal/banner-ui/index.d.ts.map +1 -1
  13. package/dist/internal/banner-ui/index.js +377 -41
  14. package/dist/internal/banner-ui/index.js.map +1 -1
  15. package/dist/internal/banner-ui/initial-info.d.ts +4 -0
  16. package/dist/internal/banner-ui/initial-info.d.ts.map +1 -0
  17. package/dist/internal/banner-ui/initial-info.js +22 -0
  18. package/dist/internal/banner-ui/initial-info.js.map +1 -0
  19. package/dist/internal/banner-ui/qr.d.ts +22 -0
  20. package/dist/internal/banner-ui/qr.d.ts.map +1 -0
  21. package/dist/internal/banner-ui/qr.js +95 -0
  22. package/dist/internal/banner-ui/qr.js.map +1 -0
  23. package/dist/internal/banner-ui/store-links.d.ts +38 -0
  24. package/dist/internal/banner-ui/store-links.d.ts.map +1 -1
  25. package/dist/internal/banner-ui/store-links.js +49 -0
  26. package/dist/internal/banner-ui/store-links.js.map +1 -1
  27. package/dist/internal/banner-ui/styles.d.ts +4 -2
  28. package/dist/internal/banner-ui/styles.d.ts.map +1 -1
  29. package/dist/internal/banner-ui/styles.js +709 -66
  30. package/dist/internal/banner-ui/styles.js.map +1 -1
  31. package/dist/react.d.ts +26 -1
  32. package/dist/react.d.ts.map +1 -1
  33. package/dist/react.js +42 -2
  34. package/dist/react.js.map +1 -1
  35. package/dist/react.server.d.ts +22 -0
  36. package/dist/react.server.d.ts.map +1 -0
  37. package/dist/react.server.js +118 -0
  38. package/dist/react.server.js.map +1 -0
  39. package/dist/server.d.ts +2 -0
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +13 -1
  42. package/dist/server.js.map +1 -1
  43. package/package.json +7 -2
@@ -1,92 +1,662 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BANNER_UI_CSS = exports.BANNER_HEIGHT_MOBILE = exports.BANNER_HEIGHT_DESKTOP = void 0;
3
+ exports.BANNER_UI_CSS = exports.BOTTOM_HEIGHT_MOBILE = exports.BOTTOM_HEIGHT_DESKTOP = exports.BANNER_HEIGHT_MOBILE = exports.BANNER_HEIGHT_DESKTOP = void 0;
4
4
  exports.ensureBannerStylesInjected = ensureBannerStylesInjected;
5
+ const env_1 = require("../env");
5
6
  const STYLE_ID = "eazo-sdk-banner-ui";
6
7
  exports.BANNER_HEIGHT_DESKTOP = 52;
7
- exports.BANNER_HEIGHT_MOBILE = 64;
8
+ exports.BANNER_HEIGHT_MOBILE = 56;
9
+ /* Sized so the V5 / M5 bottom banner fits a 44px coral pill plus the
10
+ * design's 14 / 22-px breathing pad. Bumping these here also bumps the
11
+ * `<html>` padding-bottom the SDK reserves (see banner-ui/index.tsx),
12
+ * so the host page never tucks under the banner. */
13
+ exports.BOTTOM_HEIGHT_DESKTOP = 72;
14
+ exports.BOTTOM_HEIGHT_MOBILE = 78;
15
+ const TOKENS = `
16
+ --eazo-cream: #f1ebe0;
17
+ --eazo-paper: #faf6ee;
18
+ --eazo-ink: #11130f;
19
+ --eazo-ink-soft: rgba(17,19,15,0.62);
20
+ --eazo-ink-faint: rgba(17,19,15,0.32);
21
+ --eazo-hair: rgba(17,19,15,0.10);
22
+ --eazo-coral: #d4614a;
23
+ --eazo-coral-gradient: linear-gradient(180deg, #F47A42 0%, #EE5C2A 100%);
24
+ --eazo-glow: rgba(212,97,74,0.36);
25
+ --eazo-sans: "Inter", "Helvetica Neue", system-ui, sans-serif;
26
+ --eazo-serif: "Source Serif 4", "GT Sectra", "Tiempos", Georgia, serif;
27
+ --eazo-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, Menlo, monospace;
28
+ `;
8
29
  exports.BANNER_UI_CSS = `
9
- .eazo-banner-root {
30
+ /* ════════════════════════════════════════════════════════════════════════
31
+ * Host content safe area — two-layer wrapper
32
+ *
33
+ * Two nested elements wrap the host's children at the EazoProvider level:
34
+ *
35
+ * <div class="eazo-app-area"> ← outer: containing block
36
+ * <div class="eazo-app-area-scroller"> ← inner: scroll container
37
+ * {host children}
38
+ * </div>
39
+ * </div>
40
+ *
41
+ * Both elements are ALWAYS rendered (SSR + CSR markup is static, no
42
+ * hydration mismatch). The effective styles activate only when the host
43
+ * is a plain web browser AND the handoff banners are mounted — gated on
44
+ * the \`eazo-host-web\` class on \`<html>\`, set/cleared by the banner-ui
45
+ * effect. In a mobile WebView or iframe both elements are inert \`<div>\`s.
46
+ *
47
+ * Why two layers (this is the whole point):
48
+ *
49
+ * A single-element wrapper that combines \`transform: translateZ(0)\` AND
50
+ * \`overflow: auto\` LOOKS like it should keep host's \`position: fixed;
51
+ * bottom: 0\` elements pinned above the SDK banner — the transform
52
+ * reparents the containing block to the wrapper, after all. But under
53
+ * that combination, browsers (per the CSS positioning + scrolling spec
54
+ * interaction) demote the fixed descendant to absolute-like behavior
55
+ * AND translate it by the wrapper's scroll offset. The "sticky CTA"
56
+ * ends up scrolling with content rather than staying pinned.
57
+ *
58
+ * Splitting them fixes this:
59
+ * - \`.eazo-app-area\` (outer): \`position: fixed\` between the banners
60
+ * + \`transform: translateZ(0)\`. It establishes the containing block
61
+ * but is NOT a scroll container — its own padding box never moves.
62
+ * - \`.eazo-app-area-scroller\` (inner): \`position: absolute; inset: 0\`
63
+ * to fill the outer + \`overflow-y: auto\` to scroll host content. It
64
+ * is NOT a containing block for fixed descendants (no transform,
65
+ * static position contexts don't qualify), so host's
66
+ * \`position: fixed; bottom: 0\` still resolves all the way up to the
67
+ * outer. The bar is painted in the outer's coordinate space, OUTSIDE
68
+ * the inner's scroll layer, so scrolling host content does NOT
69
+ * translate it. Result: bar stays visually pinned to the outer's
70
+ * bottom edge — which sits exactly above the SDK banner.
71
+ *
72
+ * Why scope the styles to \`html.eazo-host-web\`:
73
+ * The wrapper only exists to keep host content clear of the SDK's
74
+ * handoff banners. In a mobile WebView or iframe the banners don't
75
+ * render — the wrapper has no job, and activating the fixed-position
76
+ * + overflow + containing-block semantics there would silently break
77
+ * \`window.scrollY\`, \`window\` scroll listeners, body-overflow scroll
78
+ * locks, and host modals at \`position: fixed; inset: 0\` for zero
79
+ * product benefit. So both layers stay inert outside plain web.
80
+ *
81
+ * Known trade-offs on web (called out in CHANGELOG):
82
+ * - Scrolling happens inside \`.eazo-app-area-scroller\`, not on
83
+ * \`window\`. Code reading \`window.scrollY\` or attaching \`scroll\`
84
+ * listeners to \`window\` must migrate to the scroller element.
85
+ * - \`document.body { overflow: hidden }\` no longer locks scroll;
86
+ * body-scroll-lock libraries must target the scroller.
87
+ * - Host modals at \`position: fixed; inset: 0\` are contained to
88
+ * the outer wrapper rather than covering the full viewport —
89
+ * visually equivalent (the wrapper IS the safe-area box) but
90
+ * \`inset: 0\` no longer covers the banner area.
91
+ */
92
+ /* Default state for the two wrapper layers: \`display: contents\` makes
93
+ * the wrapper boxes disappear from layout entirely. Their children
94
+ * participate in the GRANDPARENT's layout context (i.e. directly in the
95
+ * \`<body>\`'s flex column) as if the wrapper elements didn't exist.
96
+ * With no generated box there's also no containing block for fixed
97
+ * descendants — host's \`position: fixed; bottom: 0\` resolves all the
98
+ * way up to the viewport, exactly as it would without the SDK present.
99
+ *
100
+ * This is the ONLY state the wrapper takes in mobile WebView / iframe
101
+ * hosts: the banners aren't mounted, the wrapper has no job, so it
102
+ * collapses to a layout no-op. \`html.eazo-host-web\` (added by banner-ui
103
+ * on mount, only in plain web) overrides BOTH layers below to their
104
+ * full active styles. */
105
+ .eazo-app-area,
106
+ .eazo-app-area-scroller {
107
+ display: contents;
108
+ }
109
+
110
+ html.eazo-host-web .eazo-app-area {
111
+ display: block;
10
112
  position: fixed;
11
- top: 0;
12
- left: 0;
13
- right: 0;
14
- z-index: 2147483550;
113
+ inset: var(--eazo-handoff-top, 0px) 0 var(--eazo-handoff-bottom, 0px) 0;
114
+ /* Containing block for fixed-positioned descendants — this is what
115
+ * lets host's \`position: fixed; bottom: 0\` anchor to the wrapper
116
+ * (between the banners) instead of to the viewport (under our banner).
117
+ *
118
+ * IMPORTANT: do not move \`overflow: auto\` onto this element. The
119
+ * combination of transform + overflow makes fixed descendants scroll
120
+ * with content. The scroll lives on \`.eazo-app-area-scroller\` below. */
121
+ transform: translateZ(0);
122
+ }
123
+ html.eazo-host-web .eazo-app-area-scroller {
124
+ display: block;
125
+ /* Fill the outer wrapper exactly. \`position: absolute\` is the
126
+ * cheapest way to do this — \`width/height: 100%\` plus margin/padding
127
+ * inheritance can leak; \`inset: 0\` against the outer's padding box
128
+ * is unambiguous. */
129
+ position: absolute;
130
+ inset: 0;
131
+ /* This is the actual scroll container for host content. Crucially,
132
+ * it does NOT have \`transform\` — so it is NOT a containing block
133
+ * for host's \`position: fixed\` descendants. Those still resolve up
134
+ * to \`.eazo-app-area\` and stay pinned to its edges, ignoring scroll. */
135
+ overflow-x: hidden;
136
+ overflow-y: auto;
137
+ /* Disable rubber-band overscroll. The wrapper sits between two fixed
138
+ * banners on its own compositor layer (via the outer's translateZ);
139
+ * during native overscroll bounce the scroller's content briefly
140
+ * translates beyond its padding box and the compositor briefly
141
+ * reveals adjacent layers — the cream top banner above, the white
142
+ * bottom banner below, the body's UA-default background everywhere
143
+ * else — as a flash of "other colors" at the top/bottom edges.
144
+ * \`overscroll-behavior: none\` keeps the scroll fully contained and
145
+ * eliminates that visual seam, at the cost of the native bounce
146
+ * gesture inside the wrapper (acceptable trade for the SDK's promo
147
+ * surface, which is already constrained by the banner sandwich). */
148
+ overscroll-behavior: none;
149
+ }
150
+
151
+ /* The whole handoff UI lives inside ONE fixed-positioned container that
152
+ * fills the viewport and flex-columns its three children: top banner +
153
+ * overlay (which holds the modal) + bottom banner. This replaces the
154
+ * earlier design where each piece was independently position:fixed
155
+ * with hand-tuned top:52px / bottom:60px insets — that scheme broke
156
+ * any time an ancestor of the SDK mount established a containing block
157
+ * (transform / filter / backdrop-filter / contain on <body>, a wrapper,
158
+ * etc.), at which point position:fixed becomes relative to that
159
+ * ancestor and the math goes wrong. Flex layout makes the overlay
160
+ * genuinely between the banners by structure, not by pixel math.
161
+ *
162
+ * The root is pointer-events:none so the user's page underneath stays
163
+ * interactive in transparent regions (there shouldn't be any when the
164
+ * overlay's modal is up, but it's the right default). Each visual child
165
+ * (banners + overlay dim) opts back in with pointer-events:auto. */
166
+ .eazo-handoff-root {
167
+ ${TOKENS}
168
+ position: fixed;
169
+ inset: 0;
170
+ z-index: 2147483540;
15
171
  display: flex;
16
- align-items: center;
17
- gap: 12px;
18
- height: ${exports.BANNER_HEIGHT_DESKTOP}px;
19
- padding: 0 14px 0 18px;
20
- background: #f1ebe0;
21
- color: #11130f;
22
- font-family: inherit;
172
+ flex-direction: column;
173
+ /* justify-content:space-between keeps the bottom banner pinned even
174
+ * when the user dismisses the modal (the overlay child unmounts) —
175
+ * without it the flex-column would collapse the bottom banner up to
176
+ * sit right under the top one. */
177
+ justify-content: space-between;
178
+ color: var(--eazo-ink);
179
+ font-family: var(--eazo-sans);
180
+ box-sizing: border-box;
181
+ pointer-events: none;
182
+ }
183
+ .eazo-handoff-root *, .eazo-handoff-root *::before, .eazo-handoff-root *::after {
23
184
  box-sizing: border-box;
24
- animation: eazo-banner-slide-down 220ms cubic-bezier(0.16, 1, 0.3, 1);
25
185
  }
26
186
 
27
- @keyframes eazo-banner-slide-down {
187
+ @keyframes eazo-handoff-slide-down {
28
188
  from { transform: translateY(-100%); opacity: 0; }
29
189
  to { transform: translateY(0); opacity: 1; }
30
190
  }
191
+ @keyframes eazo-handoff-slide-up {
192
+ from { transform: translateY(100%); opacity: 0; }
193
+ to { transform: translateY(0); opacity: 1; }
194
+ }
195
+ @keyframes eazo-handoff-orbit { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
196
+ @keyframes eazo-handoff-orbit-rev { from { transform: rotate(360deg); } to { transform: rotate(0deg); } }
197
+ @keyframes eazo-handoff-glow { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
198
+ @keyframes eazo-handoff-fade-in { from { opacity: 0; } to { opacity: 1; } }
199
+ @keyframes eazo-handoff-pop-in {
200
+ from { opacity: 0; transform: translateY(12px) scale(0.97); }
201
+ to { opacity: 1; transform: translateY(0) scale(1); }
202
+ }
31
203
 
32
- .eazo-banner-brand {
33
- display: inline-flex;
204
+ /* ============ TOP BANNER ============
205
+ *
206
+ * Slim three-piece strip: brand mark, single-line copy, CTA. The
207
+ * underlying app's content sits below this. Non-dismissible.
208
+ */
209
+ .eazo-banner-root {
210
+ /* Flex child of .eazo-handoff-root — naturally pinned to the top of
211
+ * the viewport-filling container. No position:fixed needed. */
212
+ flex-shrink: 0;
213
+ display: flex;
34
214
  align-items: center;
215
+ gap: 12px;
216
+ height: ${exports.BANNER_HEIGHT_DESKTOP}px;
217
+ padding: 0 14px 0 18px;
218
+ background: var(--eazo-cream);
219
+ border-bottom: 1px solid var(--eazo-hair);
220
+ pointer-events: auto;
221
+ animation: eazo-handoff-slide-down 240ms cubic-bezier(0.16, 1, 0.3, 1);
222
+ }
223
+ .eazo-banner-brand {
224
+ display: inline-flex; align-items: center;
35
225
  flex-shrink: 0;
36
- color: #11130f;
226
+ color: var(--eazo-ink);
37
227
  }
38
-
39
228
  .eazo-banner-copy {
40
- flex: 1;
41
- min-width: 0;
42
- font-size: 14px;
43
- font-weight: 500;
44
- color: rgba(17, 19, 15, 0.62);
45
- overflow: hidden;
46
- text-overflow: ellipsis;
47
- white-space: nowrap;
229
+ flex: 1; min-width: 0;
230
+ font-size: 14px; font-weight: 500;
231
+ color: var(--eazo-ink-soft);
232
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
48
233
  }
49
-
50
234
  .eazo-banner-cta {
51
235
  flex-shrink: 0;
52
- display: inline-flex;
53
- align-items: center;
54
- height: 34px;
55
- padding: 0 14px;
56
- border-radius: 12px;
57
- background: #d4614a;
58
- color: #ffffff;
59
- font-size: 13px;
60
- font-weight: 600;
236
+ display: inline-flex; align-items: center; gap: 6px;
237
+ height: 30px; padding: 0 14px; border-radius: 10px;
238
+ background: var(--eazo-coral-gradient); color: #fff;
239
+ font-size: 12px; font-weight: 600; border: 0; cursor: pointer;
61
240
  text-decoration: none;
62
- cursor: pointer;
241
+ box-shadow: 0 10px 22px var(--eazo-glow);
63
242
  transition: filter 160ms ease, box-shadow 160ms ease;
64
243
  }
65
- .eazo-banner-cta:hover {
66
- filter: brightness(1.06);
67
- box-shadow: 0 8px 18px rgba(212, 97, 74, 0.36);
68
- }
244
+ .eazo-banner-cta:hover { filter: brightness(1.06); }
69
245
 
70
- .eazo-banner-close {
71
- flex-shrink: 0;
72
- width: 28px;
73
- height: 28px;
246
+ /* CTA wrapper anchors the hover/focus popover. position:relative is the
247
+ * coordinate origin for the absolutely-positioned popover below. */
248
+ .eazo-banner-cta-wrap {
249
+ position: relative;
74
250
  display: inline-flex;
251
+ flex-shrink: 0;
252
+ }
253
+
254
+ /* Hover popover holding the page-URL QR. Matches the v5-stagelight
255
+ * design (project/v5-stagelight.jsx:59-85). The CTA's right edge anchors
256
+ * the right edge of the popover so it never spills off the viewport on
257
+ * a banner where the CTA is hugged to the right padding. */
258
+ .eazo-banner-cta-popover {
259
+ position: absolute;
260
+ top: calc(100% + 10px);
261
+ right: 0;
262
+ z-index: 2147483560;
263
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
264
+ min-width: 168px;
265
+ padding: 14px;
266
+ background: #fff;
267
+ border: 1px solid var(--eazo-hair);
268
+ border-radius: 14px;
269
+ box-shadow:
270
+ 0 24px 50px -20px rgba(17,19,15,0.22),
271
+ 0 0 0 1px rgba(17,19,15,0.03);
272
+ animation: eazo-handoff-fade-in 140ms ease-out;
273
+ }
274
+ /* Triangular tail pointing back up at the CTA. Rotated square so it
275
+ * inherits the card's border + background without an extra SVG. */
276
+ .eazo-banner-cta-popover-arrow {
277
+ position: absolute;
278
+ top: -7px; right: 24px;
279
+ width: 12px; height: 12px;
280
+ background: #fff;
281
+ border-top: 1px solid var(--eazo-hair);
282
+ border-left: 1px solid var(--eazo-hair);
283
+ transform: rotate(45deg);
284
+ }
285
+ .eazo-banner-cta-popover-qr {
286
+ padding: 4px;
287
+ background: #fff;
288
+ line-height: 0;
289
+ }
290
+ .eazo-banner-cta-popover-caption {
291
+ font-family: var(--eazo-mono);
292
+ font-size: 11px;
293
+ line-height: 1.4;
294
+ letter-spacing: 0.04em;
295
+ color: var(--eazo-ink-soft);
296
+ text-align: center;
297
+ }
298
+
299
+ /* ============ OVERLAY (backdrop + spotlight + modal) ============
300
+ *
301
+ * The flex-middle of .eazo-handoff-root. Takes whatever vertical space
302
+ * the top and bottom banners don't claim — i.e. it IS the inter-banner
303
+ * area by structure, not by pixel math. overflow:hidden clips any
304
+ * oversized modal at this seam; the modal's own max-height:100% plus
305
+ * the overlay's flex centering keeps it inside.
306
+ */
307
+ .eazo-handoff-overlay {
308
+ flex: 1;
309
+ min-height: 0; /* allow the flex item to shrink below content size */
310
+ position: relative;
311
+ display: flex;
75
312
  align-items: center;
76
313
  justify-content: center;
77
- border: 0;
314
+ overflow: hidden;
315
+ padding: 16px;
316
+ pointer-events: auto;
317
+ animation: eazo-handoff-fade-in 320ms ease-out;
318
+ }
319
+ .eazo-handoff-overlay-dim {
320
+ position: absolute; inset: 0;
321
+ background: rgba(241,235,224,0.78);
322
+ backdrop-filter: blur(3px);
323
+ -webkit-backdrop-filter: blur(3px);
324
+ }
325
+ .eazo-handoff-overlay-spot {
326
+ position: absolute; inset: 0;
327
+ background: radial-gradient(ellipse at 50% 50%, rgba(212,97,74,0.22) 0%, rgba(212,97,74,0.06) 30%, transparent 58%);
328
+ pointer-events: none;
329
+ }
330
+
331
+ .eazo-modal {
332
+ /* Natural flex centering by the overlay parent — no absolute
333
+ * positioning. This keeps the modal inside the overlay's banner-
334
+ * constrained box even when its content is tall, so it never bleeds
335
+ * into the top or bottom banner area. If the modal is taller than the
336
+ * overlay, the inner content scrolls. */
337
+ position: relative;
338
+ width: min(540px, 100%);
339
+ max-height: 100%;
340
+ overflow-y: auto;
341
+ padding: 32px 32px 28px;
342
+ background: rgba(255,255,255,0.92);
343
+ backdrop-filter: blur(20px);
344
+ -webkit-backdrop-filter: blur(20px);
345
+ border: 1px solid var(--eazo-hair);
346
+ border-radius: 24px;
347
+ color: var(--eazo-ink);
348
+ box-shadow:
349
+ 0 60px 100px -40px rgba(17,19,15,0.28),
350
+ inset 0 1px 0 rgba(255,255,255,0.7),
351
+ 0 0 60px var(--eazo-glow);
352
+ display: flex; flex-direction: column; align-items: center; gap: 18px;
353
+ animation: eazo-handoff-pop-in 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
354
+ }
355
+ /* Close button — sits in the modal's top-right corner. The top + bottom
356
+ * Eazo banners stay visible after the user dismisses the modal; only
357
+ * this center "strong CTA" goes away (per-tab via sessionStorage). */
358
+ .eazo-modal-close {
359
+ position: absolute;
360
+ top: 12px; right: 12px;
361
+ width: 30px; height: 30px;
362
+ display: inline-flex; align-items: center; justify-content: center;
363
+ border: 0; padding: 0;
78
364
  border-radius: 999px;
79
- background: transparent;
80
- color: rgba(17, 19, 15, 0.52);
365
+ background: rgba(17,19,15,0.04);
366
+ color: var(--eazo-ink-soft);
81
367
  cursor: pointer;
82
- padding: 0;
83
- transition: color 120ms ease, background 120ms ease;
368
+ transition: background 140ms ease, color 140ms ease;
84
369
  }
85
- .eazo-banner-close:hover {
86
- color: #11130f;
87
- background: rgba(17, 19, 15, 0.06);
370
+ .eazo-modal-close:hover {
371
+ background: rgba(17,19,15,0.08);
372
+ color: var(--eazo-ink);
373
+ }
374
+ .eazo-modal-close:focus-visible {
375
+ outline: 2px solid var(--eazo-coral);
376
+ outline-offset: 2px;
88
377
  }
89
378
 
379
+ /* ============ ORBITING CAPABILITIES + APP MONOLITH ============
380
+ *
381
+ * Geometry runs in a 280-unit coordinate space (matches the V5 design
382
+ * canvas). The rings SVG uses a viewBox so its content scales to whatever
383
+ * pixel size the .eazo-orbit container is in CSS (280 desktop, 220
384
+ * mobile). The capability nodes position via percentage left/top on
385
+ * the rotating track, then use negative margins to center on that point
386
+ * — margins do not fight the track rotate animation the way a
387
+ * transform: translate(-50%, -50%) would.
388
+ */
389
+ .eazo-orbit {
390
+ position: relative;
391
+ width: 280px; height: 280px;
392
+ display: grid; place-items: center;
393
+ }
394
+ .eazo-orbit-rings {
395
+ position: absolute; inset: 0;
396
+ width: 100%; height: 100%;
397
+ opacity: 0.95;
398
+ }
399
+ .eazo-orbit-track {
400
+ position: absolute; inset: 0; width: 100%; height: 100%;
401
+ animation: eazo-handoff-orbit 30s linear infinite;
402
+ }
403
+ .eazo-orbit-node {
404
+ position: absolute;
405
+ width: 36px; height: 36px;
406
+ margin: -18px 0 0 -18px;
407
+ border-radius: 10px;
408
+ background: #fff; border: 1px solid var(--eazo-hair);
409
+ display: grid; place-items: center;
410
+ box-shadow: 0 10px 22px -10px rgba(17,19,15,0.15);
411
+ animation: eazo-handoff-orbit-rev 30s linear infinite;
412
+ color: var(--eazo-coral);
413
+ }
414
+ .eazo-monolith {
415
+ width: 96px; height: 96px; border-radius: 22px;
416
+ /* Default fallback background — visible behind emoji icons and the
417
+ * typographic initials fallback. URL icons render as a child <img>
418
+ * that covers this completely. Eazo coral gradient (same as primary
419
+ * CTAs) so the empty state reads as a clear Eazo-brand placeholder. */
420
+ background: var(--eazo-coral-gradient);
421
+ display: grid; place-items: center;
422
+ position: relative;
423
+ color: #ffffff;
424
+ font-family: var(--eazo-serif); font-weight: 500;
425
+ font-size: 42px; letter-spacing: -0.02em;
426
+ box-shadow:
427
+ 0 30px 60px -20px var(--eazo-glow),
428
+ inset 0 1px 0 rgba(255,255,255,0.30),
429
+ 0 0 0 1px rgba(255,255,255,0.14);
430
+ overflow: hidden;
431
+ }
432
+ .eazo-monolith img {
433
+ width: 100%; height: 100%; object-fit: cover; display: block;
434
+ }
435
+
436
+ .eazo-modal-eyebrow {
437
+ font-family: var(--eazo-mono); font-size: 10px;
438
+ letter-spacing: 0.18em; text-transform: uppercase;
439
+ color: var(--eazo-ink-faint);
440
+ text-align: center;
441
+ }
442
+ .eazo-modal-title {
443
+ margin: 0; font-family: var(--eazo-serif); font-weight: 500;
444
+ font-size: 32px; line-height: 1.15; letter-spacing: -0.02em;
445
+ text-align: center; max-width: 360px;
446
+ /* Clamp at 2 lines so an unusually long app name doesn't blow up the
447
+ * modal height. Ellipsis takes over for the overflow. */
448
+ display: -webkit-box;
449
+ -webkit-line-clamp: 2;
450
+ -webkit-box-orient: vertical;
451
+ overflow: hidden;
452
+ word-break: break-word;
453
+ }
454
+ .eazo-modal-sub {
455
+ margin: 0; font-size: 13px; line-height: 1.5;
456
+ color: var(--eazo-ink-soft);
457
+ text-align: center; max-width: 360px;
458
+ /* Same idea — long taglines clamp to 3 lines to keep the QR + CTA
459
+ * visible without scrolling. */
460
+ display: -webkit-box;
461
+ -webkit-line-clamp: 3;
462
+ -webkit-box-orient: vertical;
463
+ overflow: hidden;
464
+ word-break: break-word;
465
+ }
466
+
467
+ /* Skeleton blocks shown while public app info is in flight. The modal
468
+ * frame appears immediately so the user sees Eazo's commitment to the
469
+ * handoff; the name / tagline swap in once the fetch resolves. */
470
+ .eazo-skel {
471
+ display: inline-block;
472
+ vertical-align: middle;
473
+ background: linear-gradient(90deg,
474
+ rgba(17,19,15,0.05) 0%,
475
+ rgba(17,19,15,0.12) 50%,
476
+ rgba(17,19,15,0.05) 100%);
477
+ background-size: 200% 100%;
478
+ border-radius: 8px;
479
+ animation: eazo-skel-shimmer 1.4s linear infinite;
480
+ }
481
+ .eazo-skel-title { width: 60%; height: 36px; }
482
+ .eazo-skel-sub-1 { width: 80%; height: 13px; margin-top: 8px; }
483
+ .eazo-skel-sub-2 { width: 55%; height: 13px; margin-top: 6px; }
484
+ .eazo-skel-stat { width: 28px; height: 11px; border-radius: 4px; }
485
+ @keyframes eazo-skel-shimmer {
486
+ from { background-position: 200% 0; }
487
+ to { background-position: -200% 0; }
488
+ }
489
+
490
+ /* Monolith-tuned shimmer — sweeps a brighter band over the dark navy
491
+ * gradient. Used while public app info is still loading, and as the
492
+ * placeholder behind an <img> until it decodes. */
493
+ .eazo-monolith-skel {
494
+ position: absolute;
495
+ inset: 0;
496
+ background: linear-gradient(90deg,
497
+ rgba(255,255,255,0.00) 0%,
498
+ rgba(255,255,255,0.18) 50%,
499
+ rgba(255,255,255,0.00) 100%);
500
+ background-size: 200% 100%;
501
+ animation: eazo-skel-shimmer 1.4s linear infinite;
502
+ pointer-events: none;
503
+ }
504
+ .eazo-monolith-img {
505
+ width: 100%; height: 100%;
506
+ object-fit: cover; display: block;
507
+ opacity: 0;
508
+ transition: opacity 220ms ease-out;
509
+ }
510
+ .eazo-monolith-img.is-loaded { opacity: 1; }
511
+
512
+ /* ============ QR + CTA ROW ============ */
513
+ .eazo-cta-row {
514
+ width: 100%; display: flex; gap: 12px; align-items: stretch; margin-top: 6px;
515
+ }
516
+ .eazo-qr-tile {
517
+ padding: 8px; border-radius: 10px;
518
+ background: #fff; border: 1px solid var(--eazo-hair);
519
+ display: grid; place-items: center;
520
+ }
521
+ .eazo-cta-body {
522
+ flex: 1; min-width: 0;
523
+ display: flex; flex-direction: column; justify-content: space-between; gap: 8px;
524
+ }
525
+ .eazo-cta-headline {
526
+ font-size: 12px; font-weight: 600;
527
+ }
528
+ .eazo-cta-fine {
529
+ font-size: 10px; color: var(--eazo-ink-faint); margin-top: 4px;
530
+ font-family: var(--eazo-mono); letter-spacing: 0.04em; line-height: 1.5;
531
+ }
532
+ .eazo-cta-primary {
533
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
534
+ height: 40px; border-radius: 10px;
535
+ background: var(--eazo-coral-gradient); color: #fff;
536
+ font-size: 13px; font-weight: 600; border: 0; cursor: pointer;
537
+ text-decoration: none;
538
+ box-shadow: 0 14px 26px var(--eazo-glow);
539
+ transition: filter 160ms ease;
540
+ }
541
+ .eazo-cta-primary:hover { filter: brightness(1.06); }
542
+
543
+ /* ============ BOTTOM BANNER ============
544
+ *
545
+ * Per V5 / M5 design: two prominent stats on the left (heart + chat,
546
+ * each rendered as a tinted icon-tile with a stacked value-over-label
547
+ * column) separated by a thin hair-divider, and a coral "Remix" pill
548
+ * on the right that reuses the top-banner CTA handoff. A small
549
+ * "eazo.ai ↗" mark sits to the left of the pill on desktop only —
550
+ * on phone widths (≤480px) it drops out so the Remix pill keeps its
551
+ * thumb-zone weight.
552
+ */
553
+ .eazo-bottom-root {
554
+ /* Flex child of .eazo-handoff-root — naturally pinned to the bottom of
555
+ * the viewport-filling container. No position:fixed needed. */
556
+ flex-shrink: 0;
557
+ display: flex; align-items: center; justify-content: space-between;
558
+ gap: 16px;
559
+ height: ${exports.BOTTOM_HEIGHT_DESKTOP}px;
560
+ padding: 0 22px 0 26px;
561
+ background: #fff;
562
+ border-top: 1px solid var(--eazo-hair);
563
+ pointer-events: auto;
564
+ animation: eazo-handoff-slide-up 240ms cubic-bezier(0.16, 1, 0.3, 1);
565
+ }
566
+ .eazo-bottom-stats {
567
+ display: inline-flex; align-items: center; gap: 22px;
568
+ min-width: 0; color: var(--eazo-ink);
569
+ }
570
+ .eazo-bottom-stat {
571
+ display: inline-flex; align-items: center; gap: 9px;
572
+ font-family: var(--eazo-sans);
573
+ flex-shrink: 0;
574
+ }
575
+ /* Tinted square tile that frames each stat icon — coral-on-cream for
576
+ * filled glyphs (heart), neutral-on-cream for line glyphs (chat). */
577
+ .eazo-bottom-stat-icon {
578
+ display: inline-flex; align-items: center; justify-content: center;
579
+ width: 30px; height: 30px; border-radius: 8px;
580
+ background: rgba(212,97,74,0.10);
581
+ color: var(--eazo-coral);
582
+ flex-shrink: 0;
583
+ }
584
+ .eazo-bottom-stat-icon.is-line {
585
+ background: rgba(17,19,15,0.05);
586
+ color: var(--eazo-ink);
587
+ }
588
+ .eazo-bottom-stat-text {
589
+ display: inline-flex; flex-direction: column; line-height: 1.05;
590
+ }
591
+ .eazo-bottom-stat-value {
592
+ font-family: var(--eazo-sans);
593
+ font-size: 16px; font-weight: 600; letter-spacing: -0.01em;
594
+ }
595
+ .eazo-bottom-stat-label {
596
+ font-family: var(--eazo-sans);
597
+ font-size: 11px; font-weight: 500;
598
+ color: var(--eazo-ink-faint);
599
+ margin-top: 1px;
600
+ }
601
+ .eazo-bottom-stat-divider {
602
+ width: 1px; height: 28px;
603
+ background: var(--eazo-hair);
604
+ flex-shrink: 0;
605
+ }
606
+ .eazo-bottom-skel {
607
+ display: inline-block; vertical-align: middle;
608
+ width: 32px; height: 18px; border-radius: 4px;
609
+ background: linear-gradient(90deg,
610
+ rgba(17,19,15,0.05) 0%,
611
+ rgba(17,19,15,0.12) 50%,
612
+ rgba(17,19,15,0.05) 100%);
613
+ background-size: 200% 100%;
614
+ animation: eazo-skel-shimmer 1.4s linear infinite;
615
+ }
616
+
617
+ .eazo-bottom-actions {
618
+ display: inline-flex; align-items: center; gap: 14px;
619
+ flex-shrink: 0;
620
+ }
621
+ .eazo-bottom-site {
622
+ display: inline-flex; align-items: center; gap: 4px;
623
+ color: var(--eazo-ink-soft);
624
+ text-decoration: none;
625
+ font-family: var(--eazo-sans); font-size: 12px; font-weight: 500;
626
+ white-space: nowrap;
627
+ transition: color 140ms ease;
628
+ }
629
+ .eazo-bottom-site:hover { color: var(--eazo-ink); }
630
+ .eazo-bottom-site b { color: var(--eazo-ink); font-weight: 600; }
631
+
632
+ /* Primary CTA on the bottom banner. Renders as <a> so it picks up the
633
+ * same iOS-timeout fallback handler as the top-banner CTA via the
634
+ * shared bindCtaClick — keeps the Remix tap on the same install /
635
+ * deeplink path as the rest of the handoff UX. */
636
+ .eazo-bottom-remix {
637
+ display: inline-flex; align-items: center; justify-content: center; gap: 9px;
638
+ height: 44px; padding: 0 20px 0 18px;
639
+ border: 0; cursor: pointer;
640
+ border-radius: 999px;
641
+ background: var(--eazo-coral-gradient); color: #fff;
642
+ font-family: var(--eazo-sans);
643
+ font-size: 14px; font-weight: 600; letter-spacing: -0.005em;
644
+ white-space: nowrap;
645
+ box-shadow:
646
+ 0 12px 24px var(--eazo-glow),
647
+ inset 0 1px 0 rgba(255,255,255,0.18);
648
+ text-decoration: none;
649
+ transition: transform 140ms ease, box-shadow 140ms ease;
650
+ }
651
+ .eazo-bottom-remix:hover {
652
+ transform: translateY(-1px);
653
+ box-shadow:
654
+ 0 14px 28px var(--eazo-glow),
655
+ inset 0 1px 0 rgba(255,255,255,0.22);
656
+ }
657
+ .eazo-bottom-remix:active { transform: translateY(0); }
658
+
659
+ /* ============ MOBILE TWEAKS (≤480px) ============ */
90
660
  @media (max-width: 480px) {
91
661
  .eazo-banner-root {
92
662
  height: ${exports.BANNER_HEIGHT_MOBILE}px;
@@ -94,25 +664,98 @@ exports.BANNER_UI_CSS = `
94
664
  gap: 10px;
95
665
  }
96
666
  .eazo-banner-copy {
97
- font-size: 12px;
98
- line-height: 1.25;
99
- white-space: normal;
100
- display: -webkit-box;
101
- -webkit-line-clamp: 2;
102
- -webkit-box-orient: vertical;
667
+ font-size: 12px; line-height: 1.25; white-space: normal;
668
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
103
669
  }
104
- .eazo-banner-cta {
105
- height: 32px;
106
- padding: 0 12px;
107
- font-size: 12px;
670
+ .eazo-banner-cta { height: 28px; padding: 0 10px; font-size: 11px; border-radius: 8px; }
671
+ /* Hover doesn't resolve reliably on touch — the CTA still works as a
672
+ * plain link, no popover needed. Belt-and-suspenders to the JS check
673
+ * (the popover render is also gated on the 'open' state, which never
674
+ * flips without mouseenter / focus). */
675
+ .eazo-banner-cta-popover { display: none; }
676
+
677
+ .eazo-modal {
678
+ width: calc(100vw - 32px);
679
+ padding: 24px 20px 20px;
680
+ border-radius: 20px;
681
+ gap: 14px;
108
682
  }
683
+ .eazo-orbit { width: 220px; height: 220px; }
684
+ .eazo-monolith {
685
+ width: 76px; height: 76px; border-radius: 18px;
686
+ font-size: 32px;
687
+ }
688
+ .eazo-orbit-node {
689
+ width: 28px; height: 28px; border-radius: 8px;
690
+ margin: -14px 0 0 -14px;
691
+ }
692
+ .eazo-modal-title { font-size: 26px; }
693
+ .eazo-modal-sub { font-size: 12px; }
694
+
695
+ /* Mobile: the user is already on a phone — no point showing them a QR
696
+ * to scan with their phone, and the "Scan to open" headline + fine
697
+ * print only made sense paired with the QR. Collapse to the primary
698
+ * CTA alone. */
699
+ .eazo-qr-tile { display: none; }
700
+ .eazo-cta-row { flex-direction: column; gap: 10px; }
701
+ .eazo-cta-primary { height: 44px; width: 100%; font-size: 14px; border-radius: 12px; }
702
+ .eazo-cta-headline { display: none; }
703
+ .eazo-cta-fine { display: none; }
704
+
705
+ .eazo-bottom-root {
706
+ height: ${exports.BOTTOM_HEIGHT_MOBILE}px;
707
+ padding: 0 16px 0 20px;
708
+ gap: 12px;
709
+ }
710
+ /* Tighter cells per the M5 (390px) spec: smaller icon tile, smaller
711
+ * value, smaller divider. Labels stay — they're a key part of the
712
+ * visual rhythm in M5. */
713
+ .eazo-bottom-stats { gap: 12px; }
714
+ .eazo-bottom-stat { gap: 7px; }
715
+ .eazo-bottom-stat-icon { width: 26px; height: 26px; border-radius: 7px; }
716
+ .eazo-bottom-stat-value { font-size: 14px; }
717
+ .eazo-bottom-stat-label { font-size: 10px; }
718
+ .eazo-bottom-stat-divider { height: 24px; }
719
+ .eazo-bottom-skel { width: 28px; height: 15px; }
720
+ /* M5 drops the secondary eazo.ai mark on phone widths so the Remix
721
+ * pill keeps unambiguous thumb-zone weight. */
722
+ .eazo-bottom-site { display: none; }
723
+ .eazo-bottom-remix {
724
+ height: 44px; padding: 0 18px 0 16px;
725
+ gap: 8px; font-size: 13px;
726
+ box-shadow:
727
+ 0 10px 22px var(--eazo-glow),
728
+ inset 0 1px 0 rgba(255,255,255,0.18);
729
+ }
730
+ /* Drop the trailing "this app" wording on phone widths — the icon
731
+ * plus the verb is already unambiguous and the pill stays compact. */
732
+ .eazo-bottom-remix-suffix { display: none; }
109
733
  }
110
734
  `;
111
735
  function ensureBannerStylesInjected() {
112
736
  if (typeof document === "undefined")
113
737
  return;
114
- if (document.getElementById(STYLE_ID))
738
+ // Banner CSS only matters in plain-web hosts where the handoff banners
739
+ // actually render. In a mobile WebView or embedded iframe (where the
740
+ // banner-ui mount-gate bails before any rendering happens), this CSS
741
+ // would just sit inert in `document.head` — every selector either
742
+ // \`.eazo-*\` (matches nothing host-side) or \`html.eazo-host-web ...\`
743
+ // (banner-ui never adds that class outside web). Skip the inject so
744
+ // the SDK leaves no banner-related styles in mobile/iframe documents.
745
+ if ((0, env_1.getHost)() !== "web")
746
+ return;
747
+ // Always overwrite the textContent rather than early-return on
748
+ // existing tag presence. Next.js Fast Refresh re-imports this module
749
+ // with updated `BANNER_UI_CSS`, but the previously-injected <style>
750
+ // tag survives the React tree's hot reload — so an early-return left
751
+ // the page running stale CSS until a hard refresh. Overwriting is
752
+ // O(short string) and idempotent.
753
+ const existing = document.getElementById(STYLE_ID);
754
+ if (existing) {
755
+ if (existing.textContent !== exports.BANNER_UI_CSS)
756
+ existing.textContent = exports.BANNER_UI_CSS;
115
757
  return;
758
+ }
116
759
  const style = document.createElement("style");
117
760
  style.id = STYLE_ID;
118
761
  style.setAttribute("data-eazo-sdk", "banner-ui");