@halo-ads/core 0.1.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.js ADDED
@@ -0,0 +1,1079 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/exports.ts
21
+ var exports_exports = {};
22
+ __export(exports_exports, {
23
+ AdClient: () => AdClient,
24
+ AdLoader: () => AdLoader,
25
+ Analytics: () => Analytics,
26
+ ConsoleAnalyticsProvider: () => ConsoleAnalyticsProvider,
27
+ DOMRenderer: () => DOMRenderer,
28
+ EventTracker: () => EventTracker,
29
+ HaloAds: () => HaloAds,
30
+ MOCK_ADS: () => MOCK_ADS,
31
+ MockAdProvider: () => MockAdProvider,
32
+ ProfileStore: () => ProfileStore,
33
+ Recommendation: () => Recommendation
34
+ });
35
+ module.exports = __toCommonJS(exports_exports);
36
+
37
+ // src/AdClient.ts
38
+ var AdClient = class {
39
+ constructor(provider, publisherId, debug = false) {
40
+ this.provider = provider;
41
+ this.publisherId = publisherId;
42
+ this.debug = debug;
43
+ }
44
+ async fetchAd(slot, options) {
45
+ const request = {
46
+ slot,
47
+ publisherId: this.publisherId,
48
+ ...options
49
+ };
50
+ if (this.debug) {
51
+ console.log(`[Halo Ads] \u{1F4E1} Fetching ad for slot="${slot}"`, request);
52
+ }
53
+ try {
54
+ const response = await this.provider.fetchAd(request);
55
+ if (!response.fill || !response.ad) {
56
+ if (this.debug) {
57
+ console.log(`[Halo Ads] \u26AA No fill for slot="${slot}"`);
58
+ }
59
+ return null;
60
+ }
61
+ if (this.debug) {
62
+ console.log(
63
+ `[Halo Ads] \u2705 Ad selected \u2014 id=${response.ad.id} slot="${slot}"`
64
+ );
65
+ }
66
+ return response.ad;
67
+ } catch (err) {
68
+ if (this.debug) {
69
+ console.error(`[Halo Ads] \u274C Error fetching ad for slot="${slot}":`, err);
70
+ }
71
+ return null;
72
+ }
73
+ }
74
+ async fetchMultiple(slots, options) {
75
+ const results = {};
76
+ await Promise.all(
77
+ slots.map(async (slot) => {
78
+ results[slot] = await this.fetchAd(slot, options);
79
+ })
80
+ );
81
+ return results;
82
+ }
83
+ };
84
+
85
+ // src/AdLoader.ts
86
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
87
+ var SLOT_CATEGORY_HINTS = {
88
+ hero: "sports",
89
+ banner: "finance",
90
+ sidebar: "travel",
91
+ recommended: "food",
92
+ interstitial: "gaming",
93
+ footer: "technology"
94
+ };
95
+ var AdLoader = class {
96
+ constructor(client, debug = false) {
97
+ this.cache = /* @__PURE__ */ new Map();
98
+ this.loading = /* @__PURE__ */ new Set();
99
+ this.client = client;
100
+ this.debug = debug;
101
+ }
102
+ isCacheValid(entry) {
103
+ return Date.now() - entry.timestamp < CACHE_TTL_MS;
104
+ }
105
+ getCategoryHint(slot) {
106
+ if (SLOT_CATEGORY_HINTS[slot]) return SLOT_CATEGORY_HINTS[slot];
107
+ const lowerSlot = slot.toLowerCase();
108
+ for (const [keyword, category] of Object.entries(SLOT_CATEGORY_HINTS)) {
109
+ if (lowerSlot.includes(keyword)) return category;
110
+ }
111
+ return void 0;
112
+ }
113
+ async load(slot, options) {
114
+ const cacheKey = `${slot}:${JSON.stringify(options ?? {})}`;
115
+ const cached = this.cache.get(cacheKey);
116
+ if (cached && this.isCacheValid(cached)) {
117
+ if (this.debug) {
118
+ console.log(`[Halo Ads] \u{1F4BE} Serving from cache \u2014 slot="${slot}"`);
119
+ }
120
+ return cached.ad;
121
+ }
122
+ if (this.loading.has(cacheKey)) {
123
+ if (this.debug) {
124
+ console.log(`[Halo Ads] \u23F3 Request in-flight for slot="${slot}", waiting...`);
125
+ }
126
+ return new Promise((resolve) => {
127
+ const interval = setInterval(() => {
128
+ const entry = this.cache.get(cacheKey);
129
+ if (entry) {
130
+ clearInterval(interval);
131
+ resolve(entry.ad);
132
+ }
133
+ }, 50);
134
+ });
135
+ }
136
+ this.loading.add(cacheKey);
137
+ if (this.debug) {
138
+ console.log(`[Halo Ads] \u{1F504} Loading slot="${slot}"`);
139
+ }
140
+ try {
141
+ const categoryHint = this.getCategoryHint(slot);
142
+ const ad = await this.client.fetchAd(slot, {
143
+ ...options,
144
+ targeting: categoryHint ? { category: categoryHint, ...options?.targeting } : options?.targeting
145
+ });
146
+ this.cache.set(cacheKey, { ad, timestamp: Date.now() });
147
+ return ad;
148
+ } finally {
149
+ this.loading.delete(cacheKey);
150
+ }
151
+ }
152
+ /** Clear cache for a slot (force refresh on next load) */
153
+ invalidate(slot) {
154
+ if (slot) {
155
+ for (const key of this.cache.keys()) {
156
+ if (key.startsWith(`${slot}:`)) {
157
+ this.cache.delete(key);
158
+ }
159
+ }
160
+ } else {
161
+ this.cache.clear();
162
+ }
163
+ }
164
+ };
165
+
166
+ // src/Analytics.ts
167
+ var Analytics = class {
168
+ constructor(provider) {
169
+ this.config = null;
170
+ this.sessionId = "";
171
+ this.provider = provider ?? new ConsoleAnalyticsProvider();
172
+ }
173
+ configure(config, sessionId) {
174
+ this.config = config;
175
+ this.sessionId = sessionId;
176
+ }
177
+ track(name, payload) {
178
+ if (!this.config) {
179
+ console.warn("[Halo Ads] Analytics not configured. Call HaloAds.init() first.");
180
+ return;
181
+ }
182
+ const event = {
183
+ name,
184
+ payload,
185
+ publisherId: this.config.publisherId,
186
+ sessionId: this.sessionId,
187
+ timestamp: Date.now(),
188
+ url: typeof window !== "undefined" ? window.location.href : "",
189
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : ""
190
+ };
191
+ this.provider.send(event).catch((err) => {
192
+ if (this.config?.debug) {
193
+ console.error("[Halo Ads] Failed to send analytics event:", err);
194
+ }
195
+ });
196
+ }
197
+ };
198
+ var ConsoleAnalyticsProvider = class {
199
+ async send(event) {
200
+ const icon = event.name === "impression" ? "\u{1F441}" : event.name === "click" ? "\u{1F5B1}" : event.name === "page_view" ? "\u{1F4C4}" : "\u{1F4CA}";
201
+ console.log(
202
+ `%c[Halo Ads] ${icon} ${event.name}`,
203
+ "color: #6366f1; font-weight: bold;",
204
+ {
205
+ publisherId: event.publisherId,
206
+ sessionId: event.sessionId,
207
+ timestamp: new Date(event.timestamp).toISOString(),
208
+ ...event.payload ?? {}
209
+ }
210
+ );
211
+ }
212
+ };
213
+
214
+ // src/EventTracker.ts
215
+ var EventTracker = class {
216
+ constructor(analytics, profileStore, debug = false) {
217
+ this.trackedImpressions = /* @__PURE__ */ new Set();
218
+ this.analytics = analytics;
219
+ this.profileStore = profileStore;
220
+ this.debug = debug;
221
+ }
222
+ trackImpression(ad, slot) {
223
+ const key = `${ad.id}:${slot}`;
224
+ if (this.trackedImpressions.has(key)) {
225
+ return;
226
+ }
227
+ this.trackedImpressions.add(key);
228
+ this.profileStore.markAdSeen(ad.id);
229
+ this.profileStore.addInterest(ad.category);
230
+ if (this.debug) {
231
+ console.log(`[Halo Ads] \u{1F441} Impression tracked \u2014 ad=${ad.id} slot=${slot}`);
232
+ }
233
+ this.analytics.track("impression", {
234
+ adId: ad.id,
235
+ slot,
236
+ label: ad.title
237
+ });
238
+ }
239
+ trackClick(ad, slot) {
240
+ if (this.debug) {
241
+ console.log(`[Halo Ads] \u{1F5B1} Click tracked \u2014 ad=${ad.id} slot=${slot}`);
242
+ }
243
+ this.profileStore.addInterest(ad.category);
244
+ this.analytics.track("click", {
245
+ adId: ad.id,
246
+ slot,
247
+ label: ad.title,
248
+ url: ad.url
249
+ });
250
+ }
251
+ trackView(ad, slot) {
252
+ if (this.debug) {
253
+ console.log(`[Halo Ads] \u{1F52D} View tracked \u2014 ad=${ad.id} slot=${slot}`);
254
+ }
255
+ this.analytics.track("view", {
256
+ adId: ad.id,
257
+ slot
258
+ });
259
+ }
260
+ /** Set up IntersectionObserver to auto-track impressions */
261
+ observeElement(element, ad, slot) {
262
+ if (typeof IntersectionObserver === "undefined") {
263
+ this.trackImpression(ad, slot);
264
+ return () => {
265
+ };
266
+ }
267
+ const observer = new IntersectionObserver(
268
+ (entries) => {
269
+ entries.forEach((entry) => {
270
+ if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
271
+ this.trackImpression(ad, slot);
272
+ observer.unobserve(element);
273
+ }
274
+ });
275
+ },
276
+ { threshold: 0.5 }
277
+ );
278
+ observer.observe(element);
279
+ return () => observer.unobserve(element);
280
+ }
281
+ };
282
+
283
+ // src/mock/MockAdProvider.ts
284
+ var MOCK_ADS = [
285
+ {
286
+ id: "ad_001",
287
+ title: "Ultra Boost Running Shoes",
288
+ description: "Engineered for peak performance. Lightweight foam technology with superior cushioning. Run faster, go further.",
289
+ image: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=800&q=80",
290
+ url: "https://example.com/sports-shoes",
291
+ category: "sports",
292
+ ctaText: "Shop Now",
293
+ advertiser: "SpeedForce Athletics",
294
+ sponsoredLabel: "Sponsored",
295
+ theme: {
296
+ primaryColor: "#FF4500",
297
+ backgroundColor: "#1a1a2e",
298
+ ctaBackground: "#FF4500",
299
+ ctaTextColor: "#ffffff"
300
+ }
301
+ },
302
+ {
303
+ id: "ad_002",
304
+ title: "Crypto Trading \u2014 Get $50 Bonus",
305
+ description: "Trade Bitcoin, Ethereum & 200+ altcoins with zero fees. New users get $50 trading credit instantly.",
306
+ image: "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800&q=80",
307
+ url: "https://example.com/crypto-trading",
308
+ category: "finance",
309
+ ctaText: "Claim Bonus",
310
+ advertiser: "CryptoVault Exchange",
311
+ sponsoredLabel: "Promoted",
312
+ theme: {
313
+ primaryColor: "#F7931A",
314
+ backgroundColor: "#0d1117",
315
+ ctaBackground: "#F7931A",
316
+ ctaTextColor: "#000000"
317
+ }
318
+ },
319
+ {
320
+ id: "ad_003",
321
+ title: "Win Big \u2014 Casino Welcome Offer",
322
+ description: "200% match bonus up to $500 on your first deposit. 50 free spins on top slots. 18+ only. T&Cs apply.",
323
+ image: "https://images.unsplash.com/photo-1596838132731-3301c3fd4317?w=800&q=80",
324
+ url: "https://example.com/casino",
325
+ category: "gaming",
326
+ ctaText: "Play Now",
327
+ advertiser: "LuxeRoyal Casino",
328
+ sponsoredLabel: "Ad",
329
+ theme: {
330
+ primaryColor: "#FFD700",
331
+ backgroundColor: "#1a0a00",
332
+ ctaBackground: "#FFD700",
333
+ ctaTextColor: "#000000"
334
+ }
335
+ },
336
+ {
337
+ id: "ad_004",
338
+ title: "Bali for $399 \u2014 Limited Time",
339
+ description: "All-inclusive 7-night Bali escape. Flight + hotel + breakfast included. Only 12 seats remaining.",
340
+ image: "https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80",
341
+ url: "https://example.com/travel",
342
+ category: "travel",
343
+ ctaText: "Book Flight",
344
+ advertiser: "SkyWander Travel",
345
+ sponsoredLabel: "Sponsored",
346
+ theme: {
347
+ primaryColor: "#00BCD4",
348
+ backgroundColor: "#001f3f",
349
+ ctaBackground: "#00BCD4",
350
+ ctaTextColor: "#ffffff"
351
+ }
352
+ },
353
+ {
354
+ id: "ad_005",
355
+ title: "Free Delivery on Your First Order",
356
+ description: "From sushi to burgers \u2014 500+ restaurants near you. Order in 30 minutes or it's free. Use code HALO.",
357
+ image: "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=800&q=80",
358
+ url: "https://example.com/food-delivery",
359
+ category: "food",
360
+ ctaText: "Order Now",
361
+ advertiser: "QuickBite",
362
+ sponsoredLabel: "Ad",
363
+ theme: {
364
+ primaryColor: "#FF6B35",
365
+ backgroundColor: "#1a0500",
366
+ ctaBackground: "#FF6B35",
367
+ ctaTextColor: "#ffffff"
368
+ }
369
+ }
370
+ ];
371
+ var MockAdProvider = class {
372
+ constructor() {
373
+ this.ads = MOCK_ADS;
374
+ }
375
+ /**
376
+ * Simulate network latency for a realistic dev experience.
377
+ */
378
+ delay(ms = 80) {
379
+ return new Promise((resolve) => setTimeout(resolve, ms));
380
+ }
381
+ /**
382
+ * Pick an ad for a given request. In a real implementation this
383
+ * would call POST /v1/decide with targeting parameters.
384
+ */
385
+ async fetchAd(request) {
386
+ await this.delay();
387
+ const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
388
+ const slotHash = request.slot.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
389
+ const ad = this.ads[slotHash % this.ads.length];
390
+ return {
391
+ ad,
392
+ slot: request.slot,
393
+ requestId,
394
+ fill: true,
395
+ timestamp: Date.now()
396
+ };
397
+ }
398
+ async fetchAds(requests) {
399
+ return Promise.all(requests.map((r) => this.fetchAd(r)));
400
+ }
401
+ /** Exposed for testing / preview */
402
+ getAllAds() {
403
+ return [...this.ads];
404
+ }
405
+ };
406
+
407
+ // src/Recommendation.ts
408
+ var Recommendation = class {
409
+ /**
410
+ * Rank ads by user interest score.
411
+ * Categories matching user interests are scored higher.
412
+ * Future: embed-based cosine similarity against interest vectors.
413
+ */
414
+ rankAds(ads, profile) {
415
+ if (profile.interests.length === 0) return [...ads];
416
+ const interestSet = new Set(profile.interests);
417
+ return [...ads].sort((a, b) => {
418
+ const scoreA = interestSet.has(a.category) ? 2 : 0;
419
+ const scoreB = interestSet.has(b.category) ? 2 : 0;
420
+ const seenA = profile.seenAdIds.includes(a.id) ? -1 : 0;
421
+ const seenB = profile.seenAdIds.includes(b.id) ? -1 : 0;
422
+ return scoreB + seenB - (scoreA + seenA);
423
+ });
424
+ }
425
+ /**
426
+ * Get recommended ads for a user.
427
+ * Returns ranked list excluding recently seen (if possible).
428
+ */
429
+ getRecommended(profile, limit = 5) {
430
+ const ranked = this.rankAds(MOCK_ADS, profile);
431
+ const unseen = ranked.filter((ad) => !profile.seenAdIds.includes(ad.id));
432
+ if (unseen.length >= limit) return unseen.slice(0, limit);
433
+ return ranked.slice(0, limit);
434
+ }
435
+ };
436
+
437
+ // src/renderer/DOMRenderer.ts
438
+ var HALO_ATTR = "data-halo-ad";
439
+ var HALO_ROOT_ID = "__halo_root__";
440
+ var AUTO_DISMISS_MS = 9e3;
441
+ var MAX_STACK = 3;
442
+ var stylesInjected = false;
443
+ function injectStyles() {
444
+ if (stylesInjected || typeof document === "undefined") return;
445
+ stylesInjected = true;
446
+ const style = document.createElement("style");
447
+ style.id = "__halo_styles__";
448
+ style.textContent = `
449
+ #__halo_root__ {
450
+ position: fixed;
451
+ top: 76px;
452
+ right: 20px;
453
+ z-index: 2147483647;
454
+ display: flex;
455
+ flex-direction: column;
456
+ gap: 12px;
457
+ align-items: flex-end;
458
+ pointer-events: none;
459
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
460
+ }
461
+
462
+ .halo-popup {
463
+ width: 330px;
464
+ background: rgba(10, 10, 22, 0.94);
465
+ backdrop-filter: blur(28px) saturate(160%);
466
+ -webkit-backdrop-filter: blur(28px) saturate(160%);
467
+ border: 1px solid rgba(255,255,255,0.1);
468
+ border-radius: 18px;
469
+ box-shadow:
470
+ 0 8px 40px rgba(0,0,0,0.65),
471
+ 0 0 0 1px rgba(255,255,255,0.04) inset;
472
+ overflow: hidden;
473
+ pointer-events: all;
474
+ transform: translateX(calc(100% + 28px));
475
+ opacity: 0;
476
+ transition:
477
+ transform 0.44s cubic-bezier(0.34, 1.28, 0.64, 1),
478
+ opacity 0.32s ease;
479
+ will-change: transform, opacity;
480
+ }
481
+
482
+ .halo-popup.halo-in {
483
+ transform: translateX(0);
484
+ opacity: 1;
485
+ }
486
+
487
+ .halo-popup.halo-out {
488
+ transform: translateX(calc(100% + 28px));
489
+ opacity: 0;
490
+ transition:
491
+ transform 0.28s cubic-bezier(0.55, 0, 1, 0.45),
492
+ opacity 0.22s ease;
493
+ }
494
+
495
+ .halo-popup__bar {
496
+ height: 3px;
497
+ }
498
+
499
+ .halo-popup__header {
500
+ display: flex;
501
+ align-items: center;
502
+ justify-content: space-between;
503
+ padding: 9px 13px 5px;
504
+ }
505
+
506
+ .halo-popup__sponsored {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 5px;
510
+ font-size: 9px;
511
+ font-weight: 700;
512
+ letter-spacing: 1.1px;
513
+ text-transform: uppercase;
514
+ color: rgba(255,255,255,0.32);
515
+ }
516
+
517
+ .halo-popup__dot {
518
+ width: 5px;
519
+ height: 5px;
520
+ border-radius: 50%;
521
+ animation: halo-blink 2s infinite;
522
+ }
523
+
524
+ @keyframes halo-blink {
525
+ 0%, 100% { opacity: 1; }
526
+ 50% { opacity: 0.3; }
527
+ }
528
+
529
+ .halo-popup__close {
530
+ width: 22px;
531
+ height: 22px;
532
+ border-radius: 50%;
533
+ background: rgba(255,255,255,0.07);
534
+ border: 1px solid rgba(255,255,255,0.1);
535
+ color: rgba(255,255,255,0.4);
536
+ font-size: 11px;
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: center;
540
+ cursor: pointer;
541
+ transition: all 0.15s;
542
+ padding: 0;
543
+ font-family: inherit;
544
+ line-height: 1;
545
+ }
546
+
547
+ .halo-popup__close:hover {
548
+ background: rgba(255,255,255,0.14);
549
+ color: rgba(255,255,255,0.85);
550
+ }
551
+
552
+ .halo-popup__image {
553
+ width: 100%;
554
+ height: 144px;
555
+ object-fit: cover;
556
+ display: block;
557
+ transition: transform 0.4s ease;
558
+ }
559
+
560
+ .halo-popup__image:hover { transform: scale(1.03); }
561
+
562
+ .halo-popup__img-fallback {
563
+ width: 100%;
564
+ height: 144px;
565
+ display: flex;
566
+ align-items: center;
567
+ justify-content: center;
568
+ font-size: 36px;
569
+ background: linear-gradient(135deg, #1a1a3e 0%, #0d0d22 100%);
570
+ }
571
+
572
+ .halo-popup__body {
573
+ padding: 13px 15px 4px;
574
+ }
575
+
576
+ .halo-popup__advertiser {
577
+ font-size: 10px;
578
+ font-weight: 700;
579
+ letter-spacing: 0.5px;
580
+ text-transform: uppercase;
581
+ margin-bottom: 4px;
582
+ }
583
+
584
+ .halo-popup__title {
585
+ font-size: 14px;
586
+ font-weight: 700;
587
+ color: #f1f5f9;
588
+ line-height: 1.35;
589
+ letter-spacing: -0.2px;
590
+ margin-bottom: 5px;
591
+ }
592
+
593
+ .halo-popup__desc {
594
+ font-size: 12px;
595
+ color: rgba(226,232,240,0.48);
596
+ line-height: 1.55;
597
+ display: -webkit-box;
598
+ -webkit-line-clamp: 2;
599
+ -webkit-box-orient: vertical;
600
+ overflow: hidden;
601
+ }
602
+
603
+ .halo-popup__footer {
604
+ padding: 11px 15px 14px;
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: space-between;
608
+ gap: 10px;
609
+ }
610
+
611
+ .halo-popup__cta {
612
+ flex: 1;
613
+ padding: 9px 14px;
614
+ border-radius: 10px;
615
+ border: none;
616
+ font-size: 12px;
617
+ font-weight: 700;
618
+ letter-spacing: 0.2px;
619
+ cursor: pointer;
620
+ font-family: inherit;
621
+ text-decoration: none;
622
+ display: flex;
623
+ align-items: center;
624
+ justify-content: center;
625
+ gap: 4px;
626
+ transition: filter 0.15s, transform 0.15s;
627
+ box-shadow: 0 3px 12px rgba(0,0,0,0.3);
628
+ }
629
+
630
+ .halo-popup__cta:hover {
631
+ filter: brightness(1.12);
632
+ transform: translateY(-1px);
633
+ }
634
+
635
+ .halo-popup__cta:active { transform: translateY(0); }
636
+
637
+ .halo-popup__byline {
638
+ font-size: 9px;
639
+ color: rgba(255,255,255,0.18);
640
+ letter-spacing: 0.1px;
641
+ white-space: nowrap;
642
+ }
643
+
644
+ .halo-popup__progress {
645
+ height: 2px;
646
+ background: rgba(255,255,255,0.05);
647
+ overflow: hidden;
648
+ }
649
+
650
+ .halo-popup__progress-fill {
651
+ height: 100%;
652
+ background: rgba(255,255,255,0.16);
653
+ transform-origin: left;
654
+ will-change: transform;
655
+ }
656
+ `;
657
+ document.head.appendChild(style);
658
+ }
659
+ function getOrCreateRoot() {
660
+ let root = document.getElementById(HALO_ROOT_ID);
661
+ if (!root) {
662
+ root = document.createElement("div");
663
+ root.id = HALO_ROOT_ID;
664
+ document.body.appendChild(root);
665
+ }
666
+ return root;
667
+ }
668
+ function categoryEmoji(category) {
669
+ const map = {
670
+ sports: "\u{1F45F}",
671
+ finance: "\u{1F4B0}",
672
+ travel: "\u2708\uFE0F",
673
+ food: "\u{1F354}",
674
+ gaming: "\u{1F3AE}",
675
+ technology: "\u{1F4BB}",
676
+ fashion: "\u{1F457}",
677
+ health: "\u{1F48A}",
678
+ entertainment: "\u{1F3AC}"
679
+ };
680
+ return map[category] ?? "\u{1F4E2}";
681
+ }
682
+ var DOMRenderer = class {
683
+ constructor(tracker, debug = false) {
684
+ this.activeCount = 0;
685
+ this.timers = /* @__PURE__ */ new Map();
686
+ this.tracker = tracker;
687
+ this.debug = debug;
688
+ }
689
+ resolveContainer(container) {
690
+ if (typeof container === "string") {
691
+ return document.querySelector(container);
692
+ }
693
+ return container;
694
+ }
695
+ closePopup(popup, slotId) {
696
+ const timer = this.timers.get(slotId);
697
+ if (timer) {
698
+ clearTimeout(timer);
699
+ this.timers.delete(slotId);
700
+ }
701
+ popup.classList.remove("halo-in");
702
+ popup.classList.add("halo-out");
703
+ this.activeCount = Math.max(0, this.activeCount - 1);
704
+ setTimeout(() => popup.remove(), 320);
705
+ }
706
+ async render(ad, options) {
707
+ if (typeof document === "undefined") return;
708
+ if (this.activeCount >= MAX_STACK) {
709
+ if (this.debug) console.log("[Halo Ads] Max popup stack reached, queuing...");
710
+ await new Promise((r) => setTimeout(r, 2e3));
711
+ }
712
+ injectStyles();
713
+ const root = getOrCreateRoot();
714
+ const slotId = options.slot + "_" + Date.now();
715
+ const accent = ad.theme?.primaryColor ?? "#6366f1";
716
+ const ctaBg = ad.theme?.ctaBackground ?? accent;
717
+ const ctaFg = ad.theme?.ctaTextColor ?? "#ffffff";
718
+ const popup = document.createElement("div");
719
+ popup.className = "halo-popup";
720
+ popup.setAttribute(HALO_ATTR, ad.id);
721
+ popup.setAttribute("data-slot", options.slot);
722
+ popup.setAttribute("role", "complementary");
723
+ popup.setAttribute("aria-label", `Sponsored: ${ad.title}`);
724
+ popup.innerHTML = `
725
+ <div class="halo-popup__bar" style="background:${accent};box-shadow:0 0 14px ${accent}55;"></div>
726
+
727
+ <div class="halo-popup__header">
728
+ <div class="halo-popup__sponsored">
729
+ <div class="halo-popup__dot" style="background:${accent};box-shadow:0 0 5px ${accent};"></div>
730
+ Sponsored
731
+ </div>
732
+ <button class="halo-popup__close" aria-label="Close ad">\u2715</button>
733
+ </div>
734
+
735
+ <img
736
+ class="halo-popup__image"
737
+ src="${ad.image}"
738
+ alt="${ad.title}"
739
+ loading="lazy"
740
+ />
741
+ <div class="halo-popup__img-fallback" style="display:none;">${categoryEmoji(ad.category)}</div>
742
+
743
+ <div class="halo-popup__body">
744
+ <div class="halo-popup__advertiser" style="color:${accent};">${ad.advertiser ?? ""}</div>
745
+ <div class="halo-popup__title">${ad.title}</div>
746
+ <div class="halo-popup__desc">${ad.description}</div>
747
+ </div>
748
+
749
+ <div class="halo-popup__footer">
750
+ <a
751
+ class="halo-popup__cta"
752
+ href="${ad.url}"
753
+ target="_blank"
754
+ rel="noopener noreferrer"
755
+ data-halo-cta="${ad.id}"
756
+ style="background:${ctaBg};color:${ctaFg};"
757
+ >${ad.ctaText} \u2192</a>
758
+ <span class="halo-popup__byline">Ad \xB7 haloads.com</span>
759
+ </div>
760
+
761
+ <div class="halo-popup__progress">
762
+ <div class="halo-popup__progress-fill" id="halo-prog-${slotId}"></div>
763
+ </div>
764
+ `;
765
+ root.appendChild(popup);
766
+ this.activeCount++;
767
+ const img = popup.querySelector(".halo-popup__image");
768
+ const fallback = popup.querySelector(".halo-popup__img-fallback");
769
+ img.addEventListener("error", () => {
770
+ img.style.display = "none";
771
+ fallback.style.display = "flex";
772
+ });
773
+ const closeBtn = popup.querySelector(".halo-popup__close");
774
+ closeBtn.addEventListener("click", () => this.closePopup(popup, slotId));
775
+ const cta = popup.querySelector(`[data-halo-cta="${ad.id}"]`);
776
+ cta?.addEventListener("click", (e) => {
777
+ e.stopPropagation();
778
+ this.tracker.trackClick(ad, options.slot);
779
+ options.onClick?.(ad);
780
+ });
781
+ requestAnimationFrame(() => requestAnimationFrame(() => popup.classList.add("halo-in")));
782
+ const prog = document.getElementById(`halo-prog-${slotId}`);
783
+ if (prog) {
784
+ prog.style.transition = `transform ${AUTO_DISMISS_MS}ms linear`;
785
+ requestAnimationFrame(() => {
786
+ prog.style.transform = "scaleX(0)";
787
+ });
788
+ }
789
+ const timer = setTimeout(() => this.closePopup(popup, slotId), AUTO_DISMISS_MS);
790
+ this.timers.set(slotId, timer);
791
+ this.tracker.observeElement(popup, ad, options.slot);
792
+ options.onImpression?.(ad);
793
+ if (this.debug) {
794
+ console.log(`[Halo Ads] \u{1F3AF} Popup shown \u2014 slot="${options.slot}" ad="${ad.id}"`);
795
+ }
796
+ }
797
+ clear(container) {
798
+ const el = this.resolveContainer(container);
799
+ el?.querySelectorAll(`[${HALO_ATTR}]`).forEach((node) => node.remove());
800
+ }
801
+ };
802
+
803
+ // src/storage/ProfileStore.ts
804
+ var STORAGE_KEY = "__halo_profile__";
805
+ var SESSION_DURATION_MS = 30 * 60 * 1e3;
806
+ function generateId(prefix) {
807
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
808
+ }
809
+ function detectDevice() {
810
+ if (typeof navigator === "undefined") return "unknown";
811
+ const ua = navigator.userAgent.toLowerCase();
812
+ if (/mobile|android|iphone|ipad/.test(ua)) {
813
+ return /ipad|tablet/.test(ua) ? "tablet" : "mobile";
814
+ }
815
+ return "desktop";
816
+ }
817
+ function detectLanguage() {
818
+ if (typeof navigator === "undefined") return "en";
819
+ return navigator.language?.split("-")[0] ?? "en";
820
+ }
821
+ function createDefaultProfile() {
822
+ return {
823
+ interests: [],
824
+ language: detectLanguage(),
825
+ country: "",
826
+ device: detectDevice(),
827
+ sessionId: generateId("ses"),
828
+ firstSeen: Date.now(),
829
+ lastSeen: Date.now(),
830
+ seenAdIds: []
831
+ };
832
+ }
833
+ function isStorageAvailable() {
834
+ try {
835
+ const test = "__halo_test__";
836
+ localStorage.setItem(test, "1");
837
+ localStorage.removeItem(test);
838
+ return true;
839
+ } catch {
840
+ return false;
841
+ }
842
+ }
843
+ var ProfileStore = class {
844
+ constructor() {
845
+ this.storageAvailable = isStorageAvailable();
846
+ this.profile = this.load();
847
+ }
848
+ load() {
849
+ if (!this.storageAvailable) {
850
+ return createDefaultProfile();
851
+ }
852
+ try {
853
+ const raw = localStorage.getItem(STORAGE_KEY);
854
+ if (!raw) return createDefaultProfile();
855
+ const stored = JSON.parse(raw);
856
+ const sessionExpired = Date.now() - stored.lastSeen > SESSION_DURATION_MS;
857
+ if (sessionExpired) {
858
+ stored.sessionId = generateId("ses");
859
+ stored.seenAdIds = [];
860
+ }
861
+ stored.lastSeen = Date.now();
862
+ stored.device = detectDevice();
863
+ stored.language = detectLanguage();
864
+ return stored;
865
+ } catch {
866
+ return createDefaultProfile();
867
+ }
868
+ }
869
+ save() {
870
+ if (!this.storageAvailable) return;
871
+ try {
872
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.profile));
873
+ } catch {
874
+ }
875
+ }
876
+ get() {
877
+ return { ...this.profile };
878
+ }
879
+ update(partial) {
880
+ this.profile = {
881
+ ...this.profile,
882
+ ...partial,
883
+ lastSeen: Date.now()
884
+ };
885
+ this.save();
886
+ }
887
+ /** Add an ad ID to the seen list (for frequency capping) */
888
+ markAdSeen(adId) {
889
+ if (!this.profile.seenAdIds.includes(adId)) {
890
+ this.profile.seenAdIds = [...this.profile.seenAdIds, adId];
891
+ this.save();
892
+ }
893
+ }
894
+ /** Add an interest if not already present */
895
+ addInterest(interest) {
896
+ if (!this.profile.interests.includes(interest)) {
897
+ this.profile.interests = [...this.profile.interests, interest];
898
+ this.save();
899
+ }
900
+ }
901
+ clear() {
902
+ this.profile = createDefaultProfile();
903
+ if (this.storageAvailable) {
904
+ localStorage.removeItem(STORAGE_KEY);
905
+ }
906
+ }
907
+ };
908
+
909
+ // src/HaloAds.ts
910
+ var HaloAdsSDK = class {
911
+ constructor() {
912
+ this.config = null;
913
+ this.profileStore = null;
914
+ this.analytics = null;
915
+ this.tracker = null;
916
+ this.client = null;
917
+ this.loader = null;
918
+ this.renderer = null;
919
+ this.recommendation = null;
920
+ }
921
+ // ── Public API ───────────────────────────────
922
+ /**
923
+ * Initialize the Halo Ads SDK.
924
+ * Must be called before any other methods.
925
+ *
926
+ * @example
927
+ * HaloAds.init({
928
+ * publisherId: "pub_001",
929
+ * apiKey: "pk_test_...",
930
+ * debug: true
931
+ * });
932
+ */
933
+ init(config) {
934
+ if (this.config) {
935
+ if (config.debug) {
936
+ console.warn("[Halo Ads] Already initialized. Call destroy() first to re-initialize.");
937
+ }
938
+ return;
939
+ }
940
+ this.config = {
941
+ environment: "development",
942
+ debug: false,
943
+ ...config
944
+ };
945
+ const debug = this.config.debug ?? false;
946
+ if (debug) {
947
+ console.log(
948
+ "%c[Halo Ads] \u{1F680} Initialized",
949
+ "color: #6366f1; font-weight: bold; font-size: 13px;",
950
+ {
951
+ publisherId: config.publisherId,
952
+ environment: this.config.environment,
953
+ version: "0.1.0"
954
+ }
955
+ );
956
+ }
957
+ this.profileStore = new ProfileStore();
958
+ this.analytics = new Analytics();
959
+ this.analytics.configure(this.config, this.profileStore.get().sessionId);
960
+ this.tracker = new EventTracker(this.analytics, this.profileStore, debug);
961
+ const adProvider = new MockAdProvider();
962
+ this.client = new AdClient(adProvider, config.publisherId, debug);
963
+ this.loader = new AdLoader(this.client, debug);
964
+ this.renderer = new DOMRenderer(this.tracker, debug);
965
+ this.recommendation = new Recommendation();
966
+ this.analytics.track("page_view");
967
+ }
968
+ /**
969
+ * Track an analytics event.
970
+ *
971
+ * @example
972
+ * HaloAds.track("page_view")
973
+ * HaloAds.track("click", { adId: "ad_001" })
974
+ * HaloAds.track("purchase", { value: 49.99 })
975
+ */
976
+ track(name, payload) {
977
+ this.assertInitialized("track");
978
+ this.analytics.track(name, payload);
979
+ }
980
+ /**
981
+ * Fetch an ad for a slot.
982
+ *
983
+ * @example
984
+ * const ad = await HaloAds.getAd({ slot: "hero" })
985
+ */
986
+ async getAd(request) {
987
+ this.assertInitialized("getAd");
988
+ return this.loader.load(request.slot, request);
989
+ }
990
+ /**
991
+ * Render an ad into a DOM container.
992
+ * Works like Google Adsense — just point at a container.
993
+ *
994
+ * @example
995
+ * HaloAds.render({ container: "#banner", slot: "hero" })
996
+ */
997
+ async render(options) {
998
+ this.assertInitialized("render");
999
+ if (this.config?.debug) {
1000
+ console.log(`[Halo Ads] \u{1F3A8} Rendering slot="${options.slot}"...`);
1001
+ }
1002
+ const ad = await this.loader.load(options.slot, {
1003
+ format: options.format
1004
+ });
1005
+ if (!ad) {
1006
+ if (this.config?.debug) {
1007
+ console.log(`[Halo Ads] \u26AA No ad to render for slot="${options.slot}"`);
1008
+ }
1009
+ return;
1010
+ }
1011
+ await this.renderer.render(ad, options);
1012
+ }
1013
+ /**
1014
+ * Get the current user profile.
1015
+ */
1016
+ getProfile() {
1017
+ this.assertInitialized("getProfile");
1018
+ return this.profileStore.get();
1019
+ }
1020
+ /**
1021
+ * Get recommended ads based on user profile.
1022
+ */
1023
+ getRecommended(limit = 5) {
1024
+ this.assertInitialized("getRecommended");
1025
+ return this.recommendation.getRecommended(this.profileStore.get(), limit);
1026
+ }
1027
+ /**
1028
+ * Whether the SDK has been initialized.
1029
+ */
1030
+ isInitialized() {
1031
+ return this.config !== null;
1032
+ }
1033
+ /**
1034
+ * Destroy the SDK instance (useful for testing / cleanup).
1035
+ */
1036
+ destroy() {
1037
+ this.config = null;
1038
+ this.profileStore = null;
1039
+ this.analytics = null;
1040
+ this.tracker = null;
1041
+ this.client = null;
1042
+ this.loader = null;
1043
+ this.renderer = null;
1044
+ this.recommendation = null;
1045
+ }
1046
+ // ── Internal helpers ─────────────────────────
1047
+ /** @internal */
1048
+ _getTracker() {
1049
+ this.assertInitialized("_getTracker");
1050
+ return this.tracker;
1051
+ }
1052
+ /** @internal */
1053
+ _getProfileStore() {
1054
+ this.assertInitialized("_getProfileStore");
1055
+ return this.profileStore;
1056
+ }
1057
+ assertInitialized(method) {
1058
+ if (!this.config) {
1059
+ throw new Error(
1060
+ `[Halo Ads] SDK not initialized. Call HaloAds.init() before HaloAds.${method}().`
1061
+ );
1062
+ }
1063
+ }
1064
+ };
1065
+ var HaloAds = new HaloAdsSDK();
1066
+ // Annotate the CommonJS export names for ESM import in node:
1067
+ 0 && (module.exports = {
1068
+ AdClient,
1069
+ AdLoader,
1070
+ Analytics,
1071
+ ConsoleAnalyticsProvider,
1072
+ DOMRenderer,
1073
+ EventTracker,
1074
+ HaloAds,
1075
+ MOCK_ADS,
1076
+ MockAdProvider,
1077
+ ProfileStore,
1078
+ Recommendation
1079
+ });