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