@gravity-ai/react 1.1.3 → 1.1.5

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/components/AdBanner.tsx
1
+ // src/components/GravityAd.tsx
2
2
  import { useState } from "react";
3
3
 
4
4
  // src/hooks/useAdTracking.ts
@@ -9,153 +9,233 @@ function useAdTracking({
9
9
  onImpression,
10
10
  onClickTracked
11
11
  }) {
12
- const impressionTracked = useRef(false);
12
+ const containerRef = useRef(null);
13
+ const impressionFired = useRef(false);
14
+ const impUrlRef = useRef(ad?.impUrl);
13
15
  useEffect(() => {
14
- if (!ad || !ad.impUrl || disableImpressionTracking || impressionTracked.current) {
16
+ if (ad?.impUrl !== impUrlRef.current) {
17
+ impressionFired.current = false;
18
+ impUrlRef.current = ad?.impUrl;
19
+ }
20
+ }, [ad?.impUrl]);
21
+ useEffect(() => {
22
+ if (!ad?.impUrl || disableImpressionTracking || impressionFired.current) {
15
23
  return;
16
24
  }
17
- const trackImpression = async () => {
18
- try {
19
- const img = new Image();
20
- img.src = ad.impUrl;
21
- impressionTracked.current = true;
22
- onImpression?.();
23
- } catch (error) {
24
- console.error("[Gravity] Failed to track impression:", error);
25
- }
26
- };
27
- trackImpression();
25
+ const el = containerRef.current;
26
+ if (!el) return;
27
+ if (typeof IntersectionObserver === "undefined") {
28
+ fireImpression(ad.impUrl, impressionFired, onImpression);
29
+ return;
30
+ }
31
+ const observer = new IntersectionObserver(
32
+ (entries) => {
33
+ for (const entry of entries) {
34
+ if (entry.isIntersecting && !impressionFired.current && ad.impUrl) {
35
+ fireImpression(ad.impUrl, impressionFired, onImpression);
36
+ observer.disconnect();
37
+ }
38
+ }
39
+ },
40
+ { threshold: 0.5 }
41
+ );
42
+ observer.observe(el);
43
+ return () => observer.disconnect();
28
44
  }, [ad, disableImpressionTracking, onImpression]);
29
- useEffect(() => {
30
- impressionTracked.current = false;
31
- }, [ad?.impUrl]);
32
45
  const handleClick = useCallback(() => {
33
46
  if (!ad?.clickUrl) return;
34
47
  onClickTracked?.();
35
48
  }, [ad?.clickUrl, onClickTracked]);
36
49
  return {
50
+ containerRef,
37
51
  handleClick,
38
- impressionTracked: impressionTracked.current
52
+ impressionFired: impressionFired.current
39
53
  };
40
54
  }
55
+ function fireImpression(impUrl, firedRef, onImpression) {
56
+ try {
57
+ const img = new Image();
58
+ img.src = impUrl;
59
+ firedRef.current = true;
60
+ onImpression?.();
61
+ } catch {
62
+ }
63
+ }
41
64
 
42
- // src/styles.ts
43
- var baseContainerStyle = {
44
- display: "block",
45
- textDecoration: "none",
46
- cursor: "pointer",
47
- transition: "all 0.2s ease",
48
- boxSizing: "border-box",
49
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
50
- };
51
- var themeStyles = {
52
- light: {
53
- backgroundColor: "#ffffff",
54
- color: "#1a1a1a",
55
- border: "1px solid #e5e5e5",
56
- boxShadow: "0 1px 3px rgba(0, 0, 0, 0.08)"
65
+ // src/components/GravityAd.tsx
66
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
67
+ var FOCUS_STYLE_KEY = "__gravity_ad_focus__";
68
+ function injectFocusStyle() {
69
+ if (typeof document === "undefined" || document[FOCUS_STYLE_KEY]) {
70
+ return;
71
+ }
72
+ document[FOCUS_STYLE_KEY] = true;
73
+ const s = document.createElement("style");
74
+ s.textContent = "[data-gravity-ad]:focus-visible{outline:2px solid #2563EB;outline-offset:2px}";
75
+ document.head.appendChild(s);
76
+ }
77
+ var FONT = 'Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
78
+ var defaults = {
79
+ container: {
80
+ display: "flex",
81
+ flexDirection: "column",
82
+ gap: 0,
83
+ padding: 0,
84
+ background: "#FFFFFF",
85
+ color: "#18181B",
86
+ border: "1px solid #E4E4E7",
87
+ borderRadius: 10,
88
+ boxShadow: "0 1px 2px 0 rgba(0,0,0,0.04),0 1px 6px 0 rgba(0,0,0,0.06)",
89
+ fontFamily: FONT,
90
+ textDecoration: "none",
91
+ cursor: "pointer",
92
+ transition: "box-shadow 150ms ease, transform 150ms ease",
93
+ boxSizing: "border-box",
94
+ lineHeight: 1.5,
95
+ position: "relative",
96
+ overflow: "hidden"
57
97
  },
58
- dark: {
59
- backgroundColor: "#1a1a1a",
60
- color: "#f5f5f5",
61
- border: "1px solid #333333",
62
- boxShadow: "0 1px 3px rgba(0, 0, 0, 0.3)"
98
+ containerHover: {
99
+ boxShadow: "0 4px 16px 0 rgba(0,0,0,0.10), 0 2px 4px -1px rgba(0,0,0,0.06)",
100
+ transform: "translateY(-1px)"
63
101
  },
64
- minimal: {
65
- backgroundColor: "transparent",
66
- color: "inherit",
67
- border: "none",
68
- boxShadow: "none"
102
+ inner: {
103
+ display: "flex",
104
+ flexDirection: "column",
105
+ gap: 10,
106
+ padding: "14px 16px 16px"
69
107
  },
70
- branded: {
71
- backgroundColor: "#6366f1",
72
- color: "#ffffff",
73
- border: "none",
74
- boxShadow: "0 2px 8px rgba(99, 102, 241, 0.3)"
75
- }
76
- };
77
- var sizeStyles = {
78
- small: {
79
- padding: "8px 12px",
80
- fontSize: "13px",
81
- lineHeight: "1.4",
82
- borderRadius: "6px"
108
+ header: {
109
+ display: "flex",
110
+ alignItems: "center",
111
+ gap: 8
112
+ },
113
+ favicon: {
114
+ width: 20,
115
+ height: 20,
116
+ borderRadius: 4,
117
+ objectFit: "contain",
118
+ flexShrink: 0
119
+ },
120
+ brand: {
121
+ fontSize: 13,
122
+ fontWeight: 600,
123
+ color: "#18181B",
124
+ lineHeight: 1
83
125
  },
84
- medium: {
85
- padding: "12px 16px",
86
- fontSize: "14px",
87
- lineHeight: "1.5",
88
- borderRadius: "8px"
126
+ label: {
127
+ fontSize: 10,
128
+ fontWeight: 500,
129
+ letterSpacing: "0.03em",
130
+ textTransform: "uppercase",
131
+ color: "#71717A",
132
+ lineHeight: 1,
133
+ marginLeft: "auto",
134
+ padding: "2px 6px",
135
+ border: "1px solid #E4E4E7",
136
+ borderRadius: 4
89
137
  },
90
- large: {
91
- padding: "16px 20px",
92
- fontSize: "16px",
93
- lineHeight: "1.6",
94
- borderRadius: "10px"
138
+ body: {
139
+ display: "flex",
140
+ flexDirection: "column",
141
+ gap: 6
95
142
  },
96
- responsive: {
97
- padding: "clamp(8px, 2vw, 16px) clamp(12px, 3vw, 20px)",
98
- fontSize: "clamp(13px, 1.5vw, 16px)",
99
- lineHeight: "1.5",
100
- borderRadius: "clamp(6px, 1vw, 10px)"
143
+ title: {
144
+ fontSize: 14,
145
+ fontWeight: 500,
146
+ color: "#18181B",
147
+ margin: 0,
148
+ lineHeight: 1.4
149
+ },
150
+ text: {
151
+ fontSize: 13,
152
+ color: "#71717A",
153
+ margin: 0,
154
+ lineHeight: 1.5
155
+ },
156
+ cta: {
157
+ display: "inline-flex",
158
+ alignItems: "center",
159
+ justifyContent: "center",
160
+ alignSelf: "flex-start",
161
+ gap: 4,
162
+ padding: "7px 16px",
163
+ fontSize: 13,
164
+ fontWeight: 500,
165
+ color: "#FFFFFF",
166
+ background: "#2563EB",
167
+ border: "none",
168
+ borderRadius: 6,
169
+ cursor: "pointer",
170
+ transition: "background 150ms ease",
171
+ textDecoration: "none",
172
+ lineHeight: 1,
173
+ fontFamily: "inherit",
174
+ marginTop: 2
175
+ },
176
+ ctaHover: {
177
+ background: "#1E40AF"
101
178
  }
102
179
  };
103
- var baseLabelStyle = {
104
- fontSize: "10px",
105
- fontWeight: 600,
106
- textTransform: "uppercase",
107
- letterSpacing: "0.5px",
108
- opacity: 0.7,
109
- marginBottom: "4px",
110
- display: "block"
111
- };
112
- function getAdBannerStyles(theme, size, customStyles) {
113
- const combined = {
114
- ...baseContainerStyle,
115
- ...themeStyles[theme],
116
- ...sizeStyles[size]
117
- };
118
- if (customStyles?.backgroundColor) {
119
- combined.backgroundColor = customStyles.backgroundColor;
120
- }
121
- if (customStyles?.textColor) {
122
- combined.color = customStyles.textColor;
123
- }
124
- if (customStyles?.borderRadius !== void 0) {
125
- combined.borderRadius = customStyles.borderRadius;
180
+ var variantOverrides = {
181
+ inline: {
182
+ container: { overflow: "visible" },
183
+ inner: {
184
+ flexDirection: "row",
185
+ alignItems: "center",
186
+ gap: 14,
187
+ padding: "12px 16px"
188
+ },
189
+ body: { gap: 2 },
190
+ cta: { flexShrink: 0, marginTop: 0 }
191
+ },
192
+ minimal: {
193
+ container: {
194
+ background: "transparent",
195
+ border: "none",
196
+ boxShadow: "none",
197
+ borderRadius: 0,
198
+ overflow: "visible"
199
+ },
200
+ containerHover: {
201
+ boxShadow: "none",
202
+ transform: "none"
203
+ },
204
+ inner: { padding: "8px 0" }
126
205
  }
127
- if (customStyles?.style) {
128
- Object.assign(combined, customStyles.style);
206
+ };
207
+ function merge(base, ...overrides) {
208
+ let result = base;
209
+ for (const o of overrides) {
210
+ if (o) result = { ...result, ...o };
129
211
  }
130
- return combined;
212
+ return result;
131
213
  }
132
-
133
- // src/components/AdBanner.tsx
134
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
135
- function AdBanner({
214
+ function slotStyle(slot, base, variant, slotProps, extra) {
215
+ const vo = variant !== "card" ? variantOverrides[variant]?.[slot] : void 0;
216
+ return merge(base, vo, slotProps?.[slot]?.style, extra);
217
+ }
218
+ function slotClass(slot, slotProps) {
219
+ return slotProps?.[slot]?.className;
220
+ }
221
+ function GravityAd({
136
222
  ad,
137
- theme = "light",
138
- size = "medium",
223
+ variant = "card",
139
224
  className,
140
225
  style,
141
- textStyle,
142
- textClassName,
226
+ slotProps,
143
227
  showLabel = true,
144
228
  labelText = "Sponsored",
145
- labelStyle,
146
229
  onClick,
147
230
  onImpression,
148
231
  onClickTracked,
149
232
  fallback = null,
150
233
  disableImpressionTracking = false,
151
- openInNewTab = true,
152
- borderRadius,
153
- backgroundColor,
154
- textColor,
155
- accentColor
234
+ openInNewTab = true
156
235
  }) {
157
- const [isHovered, setIsHovered] = useState(false);
158
- const { handleClick } = useAdTracking({
236
+ injectFocusStyle();
237
+ const [hovered, setHovered] = useState(false);
238
+ const { containerRef, handleClick } = useAdTracking({
159
239
  ad,
160
240
  disableImpressionTracking,
161
241
  onImpression,
@@ -164,25 +244,6 @@ function AdBanner({
164
244
  if (!ad) {
165
245
  return /* @__PURE__ */ jsx(Fragment, { children: fallback });
166
246
  }
167
- const containerStyles = getAdBannerStyles(theme, size, {
168
- backgroundColor,
169
- textColor,
170
- borderRadius,
171
- style
172
- });
173
- if (isHovered && theme !== "minimal") {
174
- containerStyles.transform = "translateY(-1px)";
175
- containerStyles.boxShadow = theme === "dark" ? "0 4px 12px rgba(0, 0, 0, 0.4)" : theme === "branded" ? "0 4px 16px rgba(99, 102, 241, 0.4)" : "0 4px 12px rgba(0, 0, 0, 0.12)";
176
- }
177
- const labelStyles = {
178
- ...baseLabelStyle,
179
- color: accentColor || (theme === "branded" ? "rgba(255,255,255,0.8)" : void 0),
180
- ...labelStyle
181
- };
182
- const textStyles = {
183
- margin: 0,
184
- ...textStyle
185
- };
186
247
  const handleClickInternal = (e) => {
187
248
  handleClick();
188
249
  onClick?.();
@@ -195,24 +256,131 @@ function AdBanner({
195
256
  target: openInNewTab ? "_blank" : void 0,
196
257
  rel: openInNewTab ? "noopener noreferrer sponsored" : "sponsored"
197
258
  } : {};
198
- return /* @__PURE__ */ jsxs(
259
+ const hoverExtra = hovered ? merge(
260
+ defaults.containerHover,
261
+ variant !== "card" ? variantOverrides[variant]?.containerHover : void 0
262
+ ) : void 0;
263
+ const containerStyle = slotStyle(
264
+ "container",
265
+ defaults.container,
266
+ variant,
267
+ slotProps,
268
+ { ...hoverExtra, ...style }
269
+ );
270
+ const ctaHoverExtra = hovered ? defaults.ctaHover : void 0;
271
+ const hasHeader = ad.favicon || ad.brandName || showLabel;
272
+ const headerEl = hasHeader ? /* @__PURE__ */ jsxs(
273
+ "div",
274
+ {
275
+ style: slotStyle("header", defaults.header, variant, slotProps),
276
+ className: slotClass("header", slotProps),
277
+ children: [
278
+ ad.favicon && /* @__PURE__ */ jsx(
279
+ "img",
280
+ {
281
+ src: ad.favicon,
282
+ alt: "",
283
+ loading: "lazy",
284
+ style: slotStyle("favicon", defaults.favicon, variant, slotProps),
285
+ className: slotClass("favicon", slotProps),
286
+ onError: (e) => {
287
+ e.target.style.display = "none";
288
+ }
289
+ }
290
+ ),
291
+ ad.brandName && /* @__PURE__ */ jsx(
292
+ "span",
293
+ {
294
+ style: slotStyle("brand", defaults.brand, variant, slotProps),
295
+ className: slotClass("brand", slotProps),
296
+ children: ad.brandName
297
+ }
298
+ ),
299
+ showLabel && /* @__PURE__ */ jsx(
300
+ "span",
301
+ {
302
+ style: slotStyle("label", defaults.label, variant, slotProps),
303
+ className: slotClass("label", slotProps),
304
+ children: labelText
305
+ }
306
+ )
307
+ ]
308
+ }
309
+ ) : null;
310
+ const bodyEl = /* @__PURE__ */ jsxs(
311
+ "div",
312
+ {
313
+ style: slotStyle("body", defaults.body, variant, slotProps),
314
+ className: slotClass("body", slotProps),
315
+ children: [
316
+ ad.title && /* @__PURE__ */ jsx(
317
+ "p",
318
+ {
319
+ style: slotStyle("title", defaults.title, variant, slotProps),
320
+ className: slotClass("title", slotProps),
321
+ children: ad.title
322
+ }
323
+ ),
324
+ /* @__PURE__ */ jsx(
325
+ "p",
326
+ {
327
+ style: slotStyle("text", defaults.text, variant, slotProps),
328
+ className: slotClass("text", slotProps),
329
+ children: ad.adText
330
+ }
331
+ )
332
+ ]
333
+ }
334
+ );
335
+ const ctaEl = ad.cta ? /* @__PURE__ */ jsx(
336
+ "span",
337
+ {
338
+ style: slotStyle("cta", defaults.cta, variant, slotProps, ctaHoverExtra),
339
+ className: slotClass("cta", slotProps),
340
+ children: ad.cta
341
+ }
342
+ ) : null;
343
+ const content = variant === "inline" ? /* @__PURE__ */ jsxs(
344
+ "div",
345
+ {
346
+ style: slotStyle("inner", defaults.inner, variant, slotProps),
347
+ className: slotClass("inner", slotProps),
348
+ children: [
349
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 8 }, children: [
350
+ headerEl,
351
+ bodyEl
352
+ ] }),
353
+ ctaEl
354
+ ]
355
+ }
356
+ ) : /* @__PURE__ */ jsxs(
357
+ "div",
358
+ {
359
+ style: slotStyle("inner", defaults.inner, variant, slotProps),
360
+ className: slotClass("inner", slotProps),
361
+ children: [
362
+ headerEl,
363
+ bodyEl,
364
+ ctaEl
365
+ ]
366
+ }
367
+ );
368
+ return /* @__PURE__ */ jsx(
199
369
  "a",
200
370
  {
201
371
  ...linkProps,
372
+ ref: containerRef,
202
373
  className,
203
- style: containerStyles,
374
+ style: containerStyle,
204
375
  onClick: handleClickInternal,
205
- onMouseEnter: () => setIsHovered(true),
206
- onMouseLeave: () => setIsHovered(false),
376
+ onMouseEnter: () => setHovered(true),
377
+ onMouseLeave: () => setHovered(false),
207
378
  "data-gravity-ad": true,
208
- children: [
209
- showLabel && /* @__PURE__ */ jsx("span", { style: labelStyles, children: labelText }),
210
- /* @__PURE__ */ jsx("p", { className: textClassName, style: textStyles, children: ad.adText })
211
- ]
379
+ children: content
212
380
  }
213
381
  );
214
382
  }
215
- AdBanner.displayName = "GravityAdBanner";
383
+ GravityAd.displayName = "GravityAd";
216
384
 
217
385
  // src/components/AdText.tsx
218
386
  import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
@@ -227,7 +395,7 @@ function AdText({
227
395
  disableImpressionTracking = false,
228
396
  openInNewTab = true
229
397
  }) {
230
- const { handleClick } = useAdTracking({
398
+ const { containerRef, handleClick } = useAdTracking({
231
399
  ad,
232
400
  disableImpressionTracking,
233
401
  onImpression,
@@ -253,6 +421,7 @@ function AdText({
253
421
  return /* @__PURE__ */ jsx2(
254
422
  "a",
255
423
  {
424
+ ref: containerRef,
256
425
  href: ad.clickUrl,
257
426
  target: openInNewTab ? "_blank" : void 0,
258
427
  rel: openInNewTab ? "noopener noreferrer sponsored" : "sponsored",
@@ -264,11 +433,20 @@ function AdText({
264
433
  }
265
434
  );
266
435
  }
267
- return /* @__PURE__ */ jsx2("span", { className, style: baseStyle, "data-gravity-ad": true, children: ad.adText });
436
+ return /* @__PURE__ */ jsx2(
437
+ "span",
438
+ {
439
+ ref: containerRef,
440
+ className,
441
+ style: baseStyle,
442
+ "data-gravity-ad": true,
443
+ children: ad.adText
444
+ }
445
+ );
268
446
  }
269
447
  AdText.displayName = "GravityAdText";
270
448
  export {
271
- AdBanner,
272
449
  AdText,
450
+ GravityAd,
273
451
  useAdTracking
274
452
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ai/react",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "React components for rendering Gravity AI advertisements",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",