@eazo/sdk 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/banner-ui/app-info.d.ts +78 -0
- package/dist/internal/banner-ui/app-info.d.ts.map +1 -0
- package/dist/internal/banner-ui/app-info.js +62 -0
- package/dist/internal/banner-ui/app-info.js.map +1 -0
- package/dist/internal/banner-ui/icons.d.ts +18 -4
- package/dist/internal/banner-ui/icons.d.ts.map +1 -1
- package/dist/internal/banner-ui/icons.js +57 -4
- package/dist/internal/banner-ui/icons.js.map +1 -1
- package/dist/internal/banner-ui/index.d.ts +16 -3
- package/dist/internal/banner-ui/index.d.ts.map +1 -1
- package/dist/internal/banner-ui/index.js +336 -41
- package/dist/internal/banner-ui/index.js.map +1 -1
- package/dist/internal/banner-ui/initial-info.d.ts +4 -0
- package/dist/internal/banner-ui/initial-info.d.ts.map +1 -0
- package/dist/internal/banner-ui/initial-info.js +22 -0
- package/dist/internal/banner-ui/initial-info.js.map +1 -0
- package/dist/internal/banner-ui/qr.d.ts +22 -0
- package/dist/internal/banner-ui/qr.d.ts.map +1 -0
- package/dist/internal/banner-ui/qr.js +95 -0
- package/dist/internal/banner-ui/qr.js.map +1 -0
- package/dist/internal/banner-ui/store-links.d.ts +38 -0
- package/dist/internal/banner-ui/store-links.d.ts.map +1 -1
- package/dist/internal/banner-ui/store-links.js +49 -0
- package/dist/internal/banner-ui/store-links.js.map +1 -1
- package/dist/internal/banner-ui/styles.d.ts +4 -2
- package/dist/internal/banner-ui/styles.d.ts.map +1 -1
- package/dist/internal/banner-ui/styles.js +578 -66
- package/dist/internal/banner-ui/styles.js.map +1 -1
- package/dist/react.d.ts +26 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +15 -1
- package/dist/react.js.map +1 -1
- package/dist/react.server.d.ts +22 -0
- package/dist/react.server.d.ts.map +1 -0
- package/dist/react.server.js +118 -0
- package/dist/react.server.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -1
- package/dist/server.js.map +1 -1
- package/package.json +7 -2
|
@@ -1,92 +1,540 @@
|
|
|
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
5
|
const STYLE_ID = "eazo-sdk-banner-ui";
|
|
6
6
|
exports.BANNER_HEIGHT_DESKTOP = 52;
|
|
7
|
-
exports.BANNER_HEIGHT_MOBILE =
|
|
7
|
+
exports.BANNER_HEIGHT_MOBILE = 56;
|
|
8
|
+
/* Sized so the V5 / M5 bottom banner fits a 44px coral pill plus the
|
|
9
|
+
* design's 14 / 22-px breathing pad. Bumping these here also bumps the
|
|
10
|
+
* `<html>` padding-bottom the SDK reserves (see banner-ui/index.tsx),
|
|
11
|
+
* so the host page never tucks under the banner. */
|
|
12
|
+
exports.BOTTOM_HEIGHT_DESKTOP = 72;
|
|
13
|
+
exports.BOTTOM_HEIGHT_MOBILE = 78;
|
|
14
|
+
const TOKENS = `
|
|
15
|
+
--eazo-cream: #f1ebe0;
|
|
16
|
+
--eazo-paper: #faf6ee;
|
|
17
|
+
--eazo-ink: #11130f;
|
|
18
|
+
--eazo-ink-soft: rgba(17,19,15,0.62);
|
|
19
|
+
--eazo-ink-faint: rgba(17,19,15,0.32);
|
|
20
|
+
--eazo-hair: rgba(17,19,15,0.10);
|
|
21
|
+
--eazo-coral: #d4614a;
|
|
22
|
+
--eazo-coral-gradient: linear-gradient(180deg, #F47A42 0%, #EE5C2A 100%);
|
|
23
|
+
--eazo-glow: rgba(212,97,74,0.36);
|
|
24
|
+
--eazo-sans: "Inter", "Helvetica Neue", system-ui, sans-serif;
|
|
25
|
+
--eazo-serif: "Source Serif 4", "GT Sectra", "Tiempos", Georgia, serif;
|
|
26
|
+
--eazo-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
27
|
+
`;
|
|
8
28
|
exports.BANNER_UI_CSS = `
|
|
9
|
-
|
|
29
|
+
/* The whole handoff UI lives inside ONE fixed-positioned container that
|
|
30
|
+
* fills the viewport and flex-columns its three children: top banner +
|
|
31
|
+
* overlay (which holds the modal) + bottom banner. This replaces the
|
|
32
|
+
* earlier design where each piece was independently position:fixed
|
|
33
|
+
* with hand-tuned top:52px / bottom:60px insets — that scheme broke
|
|
34
|
+
* any time an ancestor of the SDK mount established a containing block
|
|
35
|
+
* (transform / filter / backdrop-filter / contain on <body>, a wrapper,
|
|
36
|
+
* etc.), at which point position:fixed becomes relative to that
|
|
37
|
+
* ancestor and the math goes wrong. Flex layout makes the overlay
|
|
38
|
+
* genuinely between the banners by structure, not by pixel math.
|
|
39
|
+
*
|
|
40
|
+
* The root is pointer-events:none so the user's page underneath stays
|
|
41
|
+
* interactive in transparent regions (there shouldn't be any when the
|
|
42
|
+
* overlay's modal is up, but it's the right default). Each visual child
|
|
43
|
+
* (banners + overlay dim) opts back in with pointer-events:auto. */
|
|
44
|
+
.eazo-handoff-root {
|
|
45
|
+
${TOKENS}
|
|
10
46
|
position: fixed;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
right: 0;
|
|
14
|
-
z-index: 2147483550;
|
|
47
|
+
inset: 0;
|
|
48
|
+
z-index: 2147483540;
|
|
15
49
|
display: flex;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
/* justify-content:space-between keeps the bottom banner pinned even
|
|
52
|
+
* when the user dismisses the modal (the overlay child unmounts) —
|
|
53
|
+
* without it the flex-column would collapse the bottom banner up to
|
|
54
|
+
* sit right under the top one. */
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
color: var(--eazo-ink);
|
|
57
|
+
font-family: var(--eazo-sans);
|
|
58
|
+
box-sizing: border-box;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
}
|
|
61
|
+
.eazo-handoff-root *, .eazo-handoff-root *::before, .eazo-handoff-root *::after {
|
|
23
62
|
box-sizing: border-box;
|
|
24
|
-
animation: eazo-banner-slide-down 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
25
63
|
}
|
|
26
64
|
|
|
27
|
-
@keyframes eazo-
|
|
65
|
+
@keyframes eazo-handoff-slide-down {
|
|
28
66
|
from { transform: translateY(-100%); opacity: 0; }
|
|
29
67
|
to { transform: translateY(0); opacity: 1; }
|
|
30
68
|
}
|
|
69
|
+
@keyframes eazo-handoff-slide-up {
|
|
70
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
71
|
+
to { transform: translateY(0); opacity: 1; }
|
|
72
|
+
}
|
|
73
|
+
@keyframes eazo-handoff-orbit { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
74
|
+
@keyframes eazo-handoff-orbit-rev { from { transform: rotate(360deg); } to { transform: rotate(0deg); } }
|
|
75
|
+
@keyframes eazo-handoff-glow { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
|
|
76
|
+
@keyframes eazo-handoff-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
77
|
+
@keyframes eazo-handoff-pop-in {
|
|
78
|
+
from { opacity: 0; transform: translateY(12px) scale(0.97); }
|
|
79
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
80
|
+
}
|
|
31
81
|
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
/* ============ TOP BANNER ============
|
|
83
|
+
*
|
|
84
|
+
* Slim three-piece strip: brand mark, single-line copy, CTA. The
|
|
85
|
+
* underlying app's content sits below this. Non-dismissible.
|
|
86
|
+
*/
|
|
87
|
+
.eazo-banner-root {
|
|
88
|
+
/* Flex child of .eazo-handoff-root — naturally pinned to the top of
|
|
89
|
+
* the viewport-filling container. No position:fixed needed. */
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
display: flex;
|
|
34
92
|
align-items: center;
|
|
93
|
+
gap: 12px;
|
|
94
|
+
height: ${exports.BANNER_HEIGHT_DESKTOP}px;
|
|
95
|
+
padding: 0 14px 0 18px;
|
|
96
|
+
background: var(--eazo-cream);
|
|
97
|
+
border-bottom: 1px solid var(--eazo-hair);
|
|
98
|
+
pointer-events: auto;
|
|
99
|
+
animation: eazo-handoff-slide-down 240ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
100
|
+
}
|
|
101
|
+
.eazo-banner-brand {
|
|
102
|
+
display: inline-flex; align-items: center;
|
|
35
103
|
flex-shrink: 0;
|
|
36
|
-
color:
|
|
104
|
+
color: var(--eazo-ink);
|
|
37
105
|
}
|
|
38
|
-
|
|
39
106
|
.eazo-banner-copy {
|
|
40
|
-
flex: 1;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
color: rgba(17, 19, 15, 0.62);
|
|
45
|
-
overflow: hidden;
|
|
46
|
-
text-overflow: ellipsis;
|
|
47
|
-
white-space: nowrap;
|
|
107
|
+
flex: 1; min-width: 0;
|
|
108
|
+
font-size: 14px; font-weight: 500;
|
|
109
|
+
color: var(--eazo-ink-soft);
|
|
110
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
48
111
|
}
|
|
49
|
-
|
|
50
112
|
.eazo-banner-cta {
|
|
51
113
|
flex-shrink: 0;
|
|
52
|
-
display: inline-flex;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
border-radius: 12px;
|
|
57
|
-
background: #d4614a;
|
|
58
|
-
color: #ffffff;
|
|
59
|
-
font-size: 13px;
|
|
60
|
-
font-weight: 600;
|
|
114
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
115
|
+
height: 30px; padding: 0 14px; border-radius: 10px;
|
|
116
|
+
background: var(--eazo-coral-gradient); color: #fff;
|
|
117
|
+
font-size: 12px; font-weight: 600; border: 0; cursor: pointer;
|
|
61
118
|
text-decoration: none;
|
|
62
|
-
|
|
119
|
+
box-shadow: 0 10px 22px var(--eazo-glow);
|
|
63
120
|
transition: filter 160ms ease, box-shadow 160ms ease;
|
|
64
121
|
}
|
|
65
|
-
.eazo-banner-cta:hover {
|
|
66
|
-
filter: brightness(1.06);
|
|
67
|
-
box-shadow: 0 8px 18px rgba(212, 97, 74, 0.36);
|
|
68
|
-
}
|
|
122
|
+
.eazo-banner-cta:hover { filter: brightness(1.06); }
|
|
69
123
|
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
/* CTA wrapper anchors the hover/focus popover. position:relative is the
|
|
125
|
+
* coordinate origin for the absolutely-positioned popover below. */
|
|
126
|
+
.eazo-banner-cta-wrap {
|
|
127
|
+
position: relative;
|
|
74
128
|
display: inline-flex;
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Hover popover holding the page-URL QR. Matches the v5-stagelight
|
|
133
|
+
* design (project/v5-stagelight.jsx:59-85). The CTA's right edge anchors
|
|
134
|
+
* the right edge of the popover so it never spills off the viewport on
|
|
135
|
+
* a banner where the CTA is hugged to the right padding. */
|
|
136
|
+
.eazo-banner-cta-popover {
|
|
137
|
+
position: absolute;
|
|
138
|
+
top: calc(100% + 10px);
|
|
139
|
+
right: 0;
|
|
140
|
+
z-index: 2147483560;
|
|
141
|
+
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
142
|
+
min-width: 168px;
|
|
143
|
+
padding: 14px;
|
|
144
|
+
background: #fff;
|
|
145
|
+
border: 1px solid var(--eazo-hair);
|
|
146
|
+
border-radius: 14px;
|
|
147
|
+
box-shadow:
|
|
148
|
+
0 24px 50px -20px rgba(17,19,15,0.22),
|
|
149
|
+
0 0 0 1px rgba(17,19,15,0.03);
|
|
150
|
+
animation: eazo-handoff-fade-in 140ms ease-out;
|
|
151
|
+
}
|
|
152
|
+
/* Triangular tail pointing back up at the CTA. Rotated square so it
|
|
153
|
+
* inherits the card's border + background without an extra SVG. */
|
|
154
|
+
.eazo-banner-cta-popover-arrow {
|
|
155
|
+
position: absolute;
|
|
156
|
+
top: -7px; right: 24px;
|
|
157
|
+
width: 12px; height: 12px;
|
|
158
|
+
background: #fff;
|
|
159
|
+
border-top: 1px solid var(--eazo-hair);
|
|
160
|
+
border-left: 1px solid var(--eazo-hair);
|
|
161
|
+
transform: rotate(45deg);
|
|
162
|
+
}
|
|
163
|
+
.eazo-banner-cta-popover-qr {
|
|
164
|
+
padding: 4px;
|
|
165
|
+
background: #fff;
|
|
166
|
+
line-height: 0;
|
|
167
|
+
}
|
|
168
|
+
.eazo-banner-cta-popover-caption {
|
|
169
|
+
font-family: var(--eazo-mono);
|
|
170
|
+
font-size: 11px;
|
|
171
|
+
line-height: 1.4;
|
|
172
|
+
letter-spacing: 0.04em;
|
|
173
|
+
color: var(--eazo-ink-soft);
|
|
174
|
+
text-align: center;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ============ OVERLAY (backdrop + spotlight + modal) ============
|
|
178
|
+
*
|
|
179
|
+
* The flex-middle of .eazo-handoff-root. Takes whatever vertical space
|
|
180
|
+
* the top and bottom banners don't claim — i.e. it IS the inter-banner
|
|
181
|
+
* area by structure, not by pixel math. overflow:hidden clips any
|
|
182
|
+
* oversized modal at this seam; the modal's own max-height:100% plus
|
|
183
|
+
* the overlay's flex centering keeps it inside.
|
|
184
|
+
*/
|
|
185
|
+
.eazo-handoff-overlay {
|
|
186
|
+
flex: 1;
|
|
187
|
+
min-height: 0; /* allow the flex item to shrink below content size */
|
|
188
|
+
position: relative;
|
|
189
|
+
display: flex;
|
|
75
190
|
align-items: center;
|
|
76
191
|
justify-content: center;
|
|
77
|
-
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
padding: 16px;
|
|
194
|
+
pointer-events: auto;
|
|
195
|
+
animation: eazo-handoff-fade-in 320ms ease-out;
|
|
196
|
+
}
|
|
197
|
+
.eazo-handoff-overlay-dim {
|
|
198
|
+
position: absolute; inset: 0;
|
|
199
|
+
background: rgba(241,235,224,0.78);
|
|
200
|
+
backdrop-filter: blur(3px);
|
|
201
|
+
-webkit-backdrop-filter: blur(3px);
|
|
202
|
+
}
|
|
203
|
+
.eazo-handoff-overlay-spot {
|
|
204
|
+
position: absolute; inset: 0;
|
|
205
|
+
background: radial-gradient(ellipse at 50% 50%, rgba(212,97,74,0.22) 0%, rgba(212,97,74,0.06) 30%, transparent 58%);
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.eazo-modal {
|
|
210
|
+
/* Natural flex centering by the overlay parent — no absolute
|
|
211
|
+
* positioning. This keeps the modal inside the overlay's banner-
|
|
212
|
+
* constrained box even when its content is tall, so it never bleeds
|
|
213
|
+
* into the top or bottom banner area. If the modal is taller than the
|
|
214
|
+
* overlay, the inner content scrolls. */
|
|
215
|
+
position: relative;
|
|
216
|
+
width: min(540px, 100%);
|
|
217
|
+
max-height: 100%;
|
|
218
|
+
overflow-y: auto;
|
|
219
|
+
padding: 32px 32px 28px;
|
|
220
|
+
background: rgba(255,255,255,0.92);
|
|
221
|
+
backdrop-filter: blur(20px);
|
|
222
|
+
-webkit-backdrop-filter: blur(20px);
|
|
223
|
+
border: 1px solid var(--eazo-hair);
|
|
224
|
+
border-radius: 24px;
|
|
225
|
+
color: var(--eazo-ink);
|
|
226
|
+
box-shadow:
|
|
227
|
+
0 60px 100px -40px rgba(17,19,15,0.28),
|
|
228
|
+
inset 0 1px 0 rgba(255,255,255,0.7),
|
|
229
|
+
0 0 60px var(--eazo-glow);
|
|
230
|
+
display: flex; flex-direction: column; align-items: center; gap: 18px;
|
|
231
|
+
animation: eazo-handoff-pop-in 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
232
|
+
}
|
|
233
|
+
/* Close button — sits in the modal's top-right corner. The top + bottom
|
|
234
|
+
* Eazo banners stay visible after the user dismisses the modal; only
|
|
235
|
+
* this center "strong CTA" goes away (per-tab via sessionStorage). */
|
|
236
|
+
.eazo-modal-close {
|
|
237
|
+
position: absolute;
|
|
238
|
+
top: 12px; right: 12px;
|
|
239
|
+
width: 30px; height: 30px;
|
|
240
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
241
|
+
border: 0; padding: 0;
|
|
78
242
|
border-radius: 999px;
|
|
79
|
-
background:
|
|
80
|
-
color:
|
|
243
|
+
background: rgba(17,19,15,0.04);
|
|
244
|
+
color: var(--eazo-ink-soft);
|
|
81
245
|
cursor: pointer;
|
|
82
|
-
|
|
83
|
-
|
|
246
|
+
transition: background 140ms ease, color 140ms ease;
|
|
247
|
+
}
|
|
248
|
+
.eazo-modal-close:hover {
|
|
249
|
+
background: rgba(17,19,15,0.08);
|
|
250
|
+
color: var(--eazo-ink);
|
|
251
|
+
}
|
|
252
|
+
.eazo-modal-close:focus-visible {
|
|
253
|
+
outline: 2px solid var(--eazo-coral);
|
|
254
|
+
outline-offset: 2px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ============ ORBITING CAPABILITIES + APP MONOLITH ============
|
|
258
|
+
*
|
|
259
|
+
* Geometry runs in a 280-unit coordinate space (matches the V5 design
|
|
260
|
+
* canvas). The rings SVG uses a viewBox so its content scales to whatever
|
|
261
|
+
* pixel size the .eazo-orbit container is in CSS (280 desktop, 220
|
|
262
|
+
* mobile). The capability nodes position via percentage left/top on
|
|
263
|
+
* the rotating track, then use negative margins to center on that point
|
|
264
|
+
* — margins do not fight the track rotate animation the way a
|
|
265
|
+
* transform: translate(-50%, -50%) would.
|
|
266
|
+
*/
|
|
267
|
+
.eazo-orbit {
|
|
268
|
+
position: relative;
|
|
269
|
+
width: 280px; height: 280px;
|
|
270
|
+
display: grid; place-items: center;
|
|
271
|
+
}
|
|
272
|
+
.eazo-orbit-rings {
|
|
273
|
+
position: absolute; inset: 0;
|
|
274
|
+
width: 100%; height: 100%;
|
|
275
|
+
opacity: 0.95;
|
|
276
|
+
}
|
|
277
|
+
.eazo-orbit-track {
|
|
278
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
279
|
+
animation: eazo-handoff-orbit 30s linear infinite;
|
|
280
|
+
}
|
|
281
|
+
.eazo-orbit-node {
|
|
282
|
+
position: absolute;
|
|
283
|
+
width: 36px; height: 36px;
|
|
284
|
+
margin: -18px 0 0 -18px;
|
|
285
|
+
border-radius: 10px;
|
|
286
|
+
background: #fff; border: 1px solid var(--eazo-hair);
|
|
287
|
+
display: grid; place-items: center;
|
|
288
|
+
box-shadow: 0 10px 22px -10px rgba(17,19,15,0.15);
|
|
289
|
+
animation: eazo-handoff-orbit-rev 30s linear infinite;
|
|
290
|
+
color: var(--eazo-coral);
|
|
84
291
|
}
|
|
85
|
-
.eazo-
|
|
86
|
-
|
|
87
|
-
background
|
|
292
|
+
.eazo-monolith {
|
|
293
|
+
width: 96px; height: 96px; border-radius: 22px;
|
|
294
|
+
/* Default fallback background — visible behind emoji icons and the
|
|
295
|
+
* typographic initials fallback. URL icons render as a child <img>
|
|
296
|
+
* that covers this completely. Eazo coral gradient (same as primary
|
|
297
|
+
* CTAs) so the empty state reads as a clear Eazo-brand placeholder. */
|
|
298
|
+
background: var(--eazo-coral-gradient);
|
|
299
|
+
display: grid; place-items: center;
|
|
300
|
+
position: relative;
|
|
301
|
+
color: #ffffff;
|
|
302
|
+
font-family: var(--eazo-serif); font-weight: 500;
|
|
303
|
+
font-size: 42px; letter-spacing: -0.02em;
|
|
304
|
+
box-shadow:
|
|
305
|
+
0 30px 60px -20px var(--eazo-glow),
|
|
306
|
+
inset 0 1px 0 rgba(255,255,255,0.30),
|
|
307
|
+
0 0 0 1px rgba(255,255,255,0.14);
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
}
|
|
310
|
+
.eazo-monolith img {
|
|
311
|
+
width: 100%; height: 100%; object-fit: cover; display: block;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.eazo-modal-eyebrow {
|
|
315
|
+
font-family: var(--eazo-mono); font-size: 10px;
|
|
316
|
+
letter-spacing: 0.18em; text-transform: uppercase;
|
|
317
|
+
color: var(--eazo-ink-faint);
|
|
318
|
+
text-align: center;
|
|
319
|
+
}
|
|
320
|
+
.eazo-modal-title {
|
|
321
|
+
margin: 0; font-family: var(--eazo-serif); font-weight: 500;
|
|
322
|
+
font-size: 32px; line-height: 1.15; letter-spacing: -0.02em;
|
|
323
|
+
text-align: center; max-width: 360px;
|
|
324
|
+
/* Clamp at 2 lines so an unusually long app name doesn't blow up the
|
|
325
|
+
* modal height. Ellipsis takes over for the overflow. */
|
|
326
|
+
display: -webkit-box;
|
|
327
|
+
-webkit-line-clamp: 2;
|
|
328
|
+
-webkit-box-orient: vertical;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
word-break: break-word;
|
|
331
|
+
}
|
|
332
|
+
.eazo-modal-sub {
|
|
333
|
+
margin: 0; font-size: 13px; line-height: 1.5;
|
|
334
|
+
color: var(--eazo-ink-soft);
|
|
335
|
+
text-align: center; max-width: 360px;
|
|
336
|
+
/* Same idea — long taglines clamp to 3 lines to keep the QR + CTA
|
|
337
|
+
* visible without scrolling. */
|
|
338
|
+
display: -webkit-box;
|
|
339
|
+
-webkit-line-clamp: 3;
|
|
340
|
+
-webkit-box-orient: vertical;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
word-break: break-word;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Skeleton blocks shown while public app info is in flight. The modal
|
|
346
|
+
* frame appears immediately so the user sees Eazo's commitment to the
|
|
347
|
+
* handoff; the name / tagline swap in once the fetch resolves. */
|
|
348
|
+
.eazo-skel {
|
|
349
|
+
display: inline-block;
|
|
350
|
+
vertical-align: middle;
|
|
351
|
+
background: linear-gradient(90deg,
|
|
352
|
+
rgba(17,19,15,0.05) 0%,
|
|
353
|
+
rgba(17,19,15,0.12) 50%,
|
|
354
|
+
rgba(17,19,15,0.05) 100%);
|
|
355
|
+
background-size: 200% 100%;
|
|
356
|
+
border-radius: 8px;
|
|
357
|
+
animation: eazo-skel-shimmer 1.4s linear infinite;
|
|
358
|
+
}
|
|
359
|
+
.eazo-skel-title { width: 60%; height: 36px; }
|
|
360
|
+
.eazo-skel-sub-1 { width: 80%; height: 13px; margin-top: 8px; }
|
|
361
|
+
.eazo-skel-sub-2 { width: 55%; height: 13px; margin-top: 6px; }
|
|
362
|
+
.eazo-skel-stat { width: 28px; height: 11px; border-radius: 4px; }
|
|
363
|
+
@keyframes eazo-skel-shimmer {
|
|
364
|
+
from { background-position: 200% 0; }
|
|
365
|
+
to { background-position: -200% 0; }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Monolith-tuned shimmer — sweeps a brighter band over the dark navy
|
|
369
|
+
* gradient. Used while public app info is still loading, and as the
|
|
370
|
+
* placeholder behind an <img> until it decodes. */
|
|
371
|
+
.eazo-monolith-skel {
|
|
372
|
+
position: absolute;
|
|
373
|
+
inset: 0;
|
|
374
|
+
background: linear-gradient(90deg,
|
|
375
|
+
rgba(255,255,255,0.00) 0%,
|
|
376
|
+
rgba(255,255,255,0.18) 50%,
|
|
377
|
+
rgba(255,255,255,0.00) 100%);
|
|
378
|
+
background-size: 200% 100%;
|
|
379
|
+
animation: eazo-skel-shimmer 1.4s linear infinite;
|
|
380
|
+
pointer-events: none;
|
|
381
|
+
}
|
|
382
|
+
.eazo-monolith-img {
|
|
383
|
+
width: 100%; height: 100%;
|
|
384
|
+
object-fit: cover; display: block;
|
|
385
|
+
opacity: 0;
|
|
386
|
+
transition: opacity 220ms ease-out;
|
|
88
387
|
}
|
|
388
|
+
.eazo-monolith-img.is-loaded { opacity: 1; }
|
|
89
389
|
|
|
390
|
+
/* ============ QR + CTA ROW ============ */
|
|
391
|
+
.eazo-cta-row {
|
|
392
|
+
width: 100%; display: flex; gap: 12px; align-items: stretch; margin-top: 6px;
|
|
393
|
+
}
|
|
394
|
+
.eazo-qr-tile {
|
|
395
|
+
padding: 8px; border-radius: 10px;
|
|
396
|
+
background: #fff; border: 1px solid var(--eazo-hair);
|
|
397
|
+
display: grid; place-items: center;
|
|
398
|
+
}
|
|
399
|
+
.eazo-cta-body {
|
|
400
|
+
flex: 1; min-width: 0;
|
|
401
|
+
display: flex; flex-direction: column; justify-content: space-between; gap: 8px;
|
|
402
|
+
}
|
|
403
|
+
.eazo-cta-headline {
|
|
404
|
+
font-size: 12px; font-weight: 600;
|
|
405
|
+
}
|
|
406
|
+
.eazo-cta-fine {
|
|
407
|
+
font-size: 10px; color: var(--eazo-ink-faint); margin-top: 4px;
|
|
408
|
+
font-family: var(--eazo-mono); letter-spacing: 0.04em; line-height: 1.5;
|
|
409
|
+
}
|
|
410
|
+
.eazo-cta-primary {
|
|
411
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
412
|
+
height: 40px; border-radius: 10px;
|
|
413
|
+
background: var(--eazo-coral-gradient); color: #fff;
|
|
414
|
+
font-size: 13px; font-weight: 600; border: 0; cursor: pointer;
|
|
415
|
+
text-decoration: none;
|
|
416
|
+
box-shadow: 0 14px 26px var(--eazo-glow);
|
|
417
|
+
transition: filter 160ms ease;
|
|
418
|
+
}
|
|
419
|
+
.eazo-cta-primary:hover { filter: brightness(1.06); }
|
|
420
|
+
|
|
421
|
+
/* ============ BOTTOM BANNER ============
|
|
422
|
+
*
|
|
423
|
+
* Per V5 / M5 design: two prominent stats on the left (heart + chat,
|
|
424
|
+
* each rendered as a tinted icon-tile with a stacked value-over-label
|
|
425
|
+
* column) separated by a thin hair-divider, and a coral "Remix" pill
|
|
426
|
+
* on the right that reuses the top-banner CTA handoff. A small
|
|
427
|
+
* "eazo.ai ↗" mark sits to the left of the pill on desktop only —
|
|
428
|
+
* on phone widths (≤480px) it drops out so the Remix pill keeps its
|
|
429
|
+
* thumb-zone weight.
|
|
430
|
+
*/
|
|
431
|
+
.eazo-bottom-root {
|
|
432
|
+
/* Flex child of .eazo-handoff-root — naturally pinned to the bottom of
|
|
433
|
+
* the viewport-filling container. No position:fixed needed. */
|
|
434
|
+
flex-shrink: 0;
|
|
435
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
436
|
+
gap: 16px;
|
|
437
|
+
height: ${exports.BOTTOM_HEIGHT_DESKTOP}px;
|
|
438
|
+
padding: 0 22px 0 26px;
|
|
439
|
+
background: #fff;
|
|
440
|
+
border-top: 1px solid var(--eazo-hair);
|
|
441
|
+
pointer-events: auto;
|
|
442
|
+
animation: eazo-handoff-slide-up 240ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
443
|
+
}
|
|
444
|
+
.eazo-bottom-stats {
|
|
445
|
+
display: inline-flex; align-items: center; gap: 22px;
|
|
446
|
+
min-width: 0; color: var(--eazo-ink);
|
|
447
|
+
}
|
|
448
|
+
.eazo-bottom-stat {
|
|
449
|
+
display: inline-flex; align-items: center; gap: 9px;
|
|
450
|
+
font-family: var(--eazo-sans);
|
|
451
|
+
flex-shrink: 0;
|
|
452
|
+
}
|
|
453
|
+
/* Tinted square tile that frames each stat icon — coral-on-cream for
|
|
454
|
+
* filled glyphs (heart), neutral-on-cream for line glyphs (chat). */
|
|
455
|
+
.eazo-bottom-stat-icon {
|
|
456
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
457
|
+
width: 30px; height: 30px; border-radius: 8px;
|
|
458
|
+
background: rgba(212,97,74,0.10);
|
|
459
|
+
color: var(--eazo-coral);
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
.eazo-bottom-stat-icon.is-line {
|
|
463
|
+
background: rgba(17,19,15,0.05);
|
|
464
|
+
color: var(--eazo-ink);
|
|
465
|
+
}
|
|
466
|
+
.eazo-bottom-stat-text {
|
|
467
|
+
display: inline-flex; flex-direction: column; line-height: 1.05;
|
|
468
|
+
}
|
|
469
|
+
.eazo-bottom-stat-value {
|
|
470
|
+
font-family: var(--eazo-sans);
|
|
471
|
+
font-size: 16px; font-weight: 600; letter-spacing: -0.01em;
|
|
472
|
+
}
|
|
473
|
+
.eazo-bottom-stat-label {
|
|
474
|
+
font-family: var(--eazo-sans);
|
|
475
|
+
font-size: 11px; font-weight: 500;
|
|
476
|
+
color: var(--eazo-ink-faint);
|
|
477
|
+
margin-top: 1px;
|
|
478
|
+
}
|
|
479
|
+
.eazo-bottom-stat-divider {
|
|
480
|
+
width: 1px; height: 28px;
|
|
481
|
+
background: var(--eazo-hair);
|
|
482
|
+
flex-shrink: 0;
|
|
483
|
+
}
|
|
484
|
+
.eazo-bottom-skel {
|
|
485
|
+
display: inline-block; vertical-align: middle;
|
|
486
|
+
width: 32px; height: 18px; border-radius: 4px;
|
|
487
|
+
background: linear-gradient(90deg,
|
|
488
|
+
rgba(17,19,15,0.05) 0%,
|
|
489
|
+
rgba(17,19,15,0.12) 50%,
|
|
490
|
+
rgba(17,19,15,0.05) 100%);
|
|
491
|
+
background-size: 200% 100%;
|
|
492
|
+
animation: eazo-skel-shimmer 1.4s linear infinite;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.eazo-bottom-actions {
|
|
496
|
+
display: inline-flex; align-items: center; gap: 14px;
|
|
497
|
+
flex-shrink: 0;
|
|
498
|
+
}
|
|
499
|
+
.eazo-bottom-site {
|
|
500
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
501
|
+
color: var(--eazo-ink-soft);
|
|
502
|
+
text-decoration: none;
|
|
503
|
+
font-family: var(--eazo-sans); font-size: 12px; font-weight: 500;
|
|
504
|
+
white-space: nowrap;
|
|
505
|
+
transition: color 140ms ease;
|
|
506
|
+
}
|
|
507
|
+
.eazo-bottom-site:hover { color: var(--eazo-ink); }
|
|
508
|
+
.eazo-bottom-site b { color: var(--eazo-ink); font-weight: 600; }
|
|
509
|
+
|
|
510
|
+
/* Primary CTA on the bottom banner. Renders as <a> so it picks up the
|
|
511
|
+
* same iOS-timeout fallback handler as the top-banner CTA via the
|
|
512
|
+
* shared bindCtaClick — keeps the Remix tap on the same install /
|
|
513
|
+
* deeplink path as the rest of the handoff UX. */
|
|
514
|
+
.eazo-bottom-remix {
|
|
515
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
|
|
516
|
+
height: 44px; padding: 0 20px 0 18px;
|
|
517
|
+
border: 0; cursor: pointer;
|
|
518
|
+
border-radius: 999px;
|
|
519
|
+
background: var(--eazo-coral-gradient); color: #fff;
|
|
520
|
+
font-family: var(--eazo-sans);
|
|
521
|
+
font-size: 14px; font-weight: 600; letter-spacing: -0.005em;
|
|
522
|
+
white-space: nowrap;
|
|
523
|
+
box-shadow:
|
|
524
|
+
0 12px 24px var(--eazo-glow),
|
|
525
|
+
inset 0 1px 0 rgba(255,255,255,0.18);
|
|
526
|
+
text-decoration: none;
|
|
527
|
+
transition: transform 140ms ease, box-shadow 140ms ease;
|
|
528
|
+
}
|
|
529
|
+
.eazo-bottom-remix:hover {
|
|
530
|
+
transform: translateY(-1px);
|
|
531
|
+
box-shadow:
|
|
532
|
+
0 14px 28px var(--eazo-glow),
|
|
533
|
+
inset 0 1px 0 rgba(255,255,255,0.22);
|
|
534
|
+
}
|
|
535
|
+
.eazo-bottom-remix:active { transform: translateY(0); }
|
|
536
|
+
|
|
537
|
+
/* ============ MOBILE TWEAKS (≤480px) ============ */
|
|
90
538
|
@media (max-width: 480px) {
|
|
91
539
|
.eazo-banner-root {
|
|
92
540
|
height: ${exports.BANNER_HEIGHT_MOBILE}px;
|
|
@@ -94,25 +542,89 @@ exports.BANNER_UI_CSS = `
|
|
|
94
542
|
gap: 10px;
|
|
95
543
|
}
|
|
96
544
|
.eazo-banner-copy {
|
|
97
|
-
font-size: 12px;
|
|
98
|
-
line-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
545
|
+
font-size: 12px; line-height: 1.25; white-space: normal;
|
|
546
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
|
547
|
+
}
|
|
548
|
+
.eazo-banner-cta { height: 28px; padding: 0 10px; font-size: 11px; border-radius: 8px; }
|
|
549
|
+
/* Hover doesn't resolve reliably on touch — the CTA still works as a
|
|
550
|
+
* plain link, no popover needed. Belt-and-suspenders to the JS check
|
|
551
|
+
* (the popover render is also gated on the 'open' state, which never
|
|
552
|
+
* flips without mouseenter / focus). */
|
|
553
|
+
.eazo-banner-cta-popover { display: none; }
|
|
554
|
+
|
|
555
|
+
.eazo-modal {
|
|
556
|
+
width: calc(100vw - 32px);
|
|
557
|
+
padding: 24px 20px 20px;
|
|
558
|
+
border-radius: 20px;
|
|
559
|
+
gap: 14px;
|
|
560
|
+
}
|
|
561
|
+
.eazo-orbit { width: 220px; height: 220px; }
|
|
562
|
+
.eazo-monolith {
|
|
563
|
+
width: 76px; height: 76px; border-radius: 18px;
|
|
564
|
+
font-size: 32px;
|
|
565
|
+
}
|
|
566
|
+
.eazo-orbit-node {
|
|
567
|
+
width: 28px; height: 28px; border-radius: 8px;
|
|
568
|
+
margin: -14px 0 0 -14px;
|
|
569
|
+
}
|
|
570
|
+
.eazo-modal-title { font-size: 26px; }
|
|
571
|
+
.eazo-modal-sub { font-size: 12px; }
|
|
572
|
+
|
|
573
|
+
/* Mobile: the user is already on a phone — no point showing them a QR
|
|
574
|
+
* to scan with their phone, and the "Scan to open" headline + fine
|
|
575
|
+
* print only made sense paired with the QR. Collapse to the primary
|
|
576
|
+
* CTA alone. */
|
|
577
|
+
.eazo-qr-tile { display: none; }
|
|
578
|
+
.eazo-cta-row { flex-direction: column; gap: 10px; }
|
|
579
|
+
.eazo-cta-primary { height: 44px; width: 100%; font-size: 14px; border-radius: 12px; }
|
|
580
|
+
.eazo-cta-headline { display: none; }
|
|
581
|
+
.eazo-cta-fine { display: none; }
|
|
582
|
+
|
|
583
|
+
.eazo-bottom-root {
|
|
584
|
+
height: ${exports.BOTTOM_HEIGHT_MOBILE}px;
|
|
585
|
+
padding: 0 16px 0 20px;
|
|
586
|
+
gap: 12px;
|
|
103
587
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
588
|
+
/* Tighter cells per the M5 (390px) spec: smaller icon tile, smaller
|
|
589
|
+
* value, smaller divider. Labels stay — they're a key part of the
|
|
590
|
+
* visual rhythm in M5. */
|
|
591
|
+
.eazo-bottom-stats { gap: 12px; }
|
|
592
|
+
.eazo-bottom-stat { gap: 7px; }
|
|
593
|
+
.eazo-bottom-stat-icon { width: 26px; height: 26px; border-radius: 7px; }
|
|
594
|
+
.eazo-bottom-stat-value { font-size: 14px; }
|
|
595
|
+
.eazo-bottom-stat-label { font-size: 10px; }
|
|
596
|
+
.eazo-bottom-stat-divider { height: 24px; }
|
|
597
|
+
.eazo-bottom-skel { width: 28px; height: 15px; }
|
|
598
|
+
/* M5 drops the secondary eazo.ai mark on phone widths so the Remix
|
|
599
|
+
* pill keeps unambiguous thumb-zone weight. */
|
|
600
|
+
.eazo-bottom-site { display: none; }
|
|
601
|
+
.eazo-bottom-remix {
|
|
602
|
+
height: 44px; padding: 0 18px 0 16px;
|
|
603
|
+
gap: 8px; font-size: 13px;
|
|
604
|
+
box-shadow:
|
|
605
|
+
0 10px 22px var(--eazo-glow),
|
|
606
|
+
inset 0 1px 0 rgba(255,255,255,0.18);
|
|
108
607
|
}
|
|
608
|
+
/* Drop the trailing "this app" wording on phone widths — the icon
|
|
609
|
+
* plus the verb is already unambiguous and the pill stays compact. */
|
|
610
|
+
.eazo-bottom-remix-suffix { display: none; }
|
|
109
611
|
}
|
|
110
612
|
`;
|
|
111
613
|
function ensureBannerStylesInjected() {
|
|
112
614
|
if (typeof document === "undefined")
|
|
113
615
|
return;
|
|
114
|
-
|
|
616
|
+
// Always overwrite the textContent rather than early-return on
|
|
617
|
+
// existing tag presence. Next.js Fast Refresh re-imports this module
|
|
618
|
+
// with updated `BANNER_UI_CSS`, but the previously-injected <style>
|
|
619
|
+
// tag survives the React tree's hot reload — so an early-return left
|
|
620
|
+
// the page running stale CSS until a hard refresh. Overwriting is
|
|
621
|
+
// O(short string) and idempotent.
|
|
622
|
+
const existing = document.getElementById(STYLE_ID);
|
|
623
|
+
if (existing) {
|
|
624
|
+
if (existing.textContent !== exports.BANNER_UI_CSS)
|
|
625
|
+
existing.textContent = exports.BANNER_UI_CSS;
|
|
115
626
|
return;
|
|
627
|
+
}
|
|
116
628
|
const style = document.createElement("style");
|
|
117
629
|
style.id = STYLE_ID;
|
|
118
630
|
style.setAttribute("data-eazo-sdk", "banner-ui");
|