@joystyk/ui 0.0.0 → 0.2.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.
@@ -0,0 +1,628 @@
1
+ import { isSlotId, slot, slotBoxStyles } from "./slots.js";
2
+ import { SLOT_SURFACES, adQueryUrl, defaultSurface, isContextId, parseAdFill } from "./ad-wire.js";
3
+ import { compiled } from "@gyral/core/compiled";
4
+ import { command, css, define, defineDriver, defineHook, nothing, prop, provideDrivers, subscription } from "@gyral/core";
5
+ //#region src/ad-slot.ts
6
+ var _gyral$t0 = {
7
+ "id": "q51qqnbqkd",
8
+ "html": "<a class=\"fill creative\" rel=\"sponsored noopener external\" data-intent-on=\"jy-ad-image\"> <img decoding=\"async\"> <span class=\"headline\"></span> </a>",
9
+ "parts": [
10
+ [
11
+ 3,
12
+ [0],
13
+ "href"
14
+ ],
15
+ [
16
+ 3,
17
+ [0],
18
+ "data-intent"
19
+ ],
20
+ [
21
+ 3,
22
+ [0, 1],
23
+ "src"
24
+ ],
25
+ [
26
+ 3,
27
+ [0, 1],
28
+ "alt"
29
+ ],
30
+ [
31
+ 3,
32
+ [0, 1],
33
+ "width"
34
+ ],
35
+ [
36
+ 3,
37
+ [0, 1],
38
+ "height"
39
+ ],
40
+ [7, [0, 1]],
41
+ [1, [0, 3]]
42
+ ],
43
+ "segments": [
44
+ "<a class=\"fill creative\" rel=\"sponsored noopener external\" data-intent-on=\"jy-ad-image\"",
45
+ {
46
+ "k": "attr",
47
+ "name": "href"
48
+ },
49
+ {
50
+ "k": "attr",
51
+ "name": "data-intent"
52
+ },
53
+ "> <img decoding=\"async\"",
54
+ {
55
+ "k": "attr",
56
+ "name": "src"
57
+ },
58
+ {
59
+ "k": "attr",
60
+ "name": "alt"
61
+ },
62
+ {
63
+ "k": "attr",
64
+ "name": "width"
65
+ },
66
+ {
67
+ "k": "attr",
68
+ "name": "height"
69
+ },
70
+ { "k": "hook" },
71
+ "> <span class=\"headline\">",
72
+ {
73
+ "k": "child",
74
+ "in": "span"
75
+ },
76
+ "</span> </a>"
77
+ ]
78
+ };
79
+ var _gyral$t1 = {
80
+ "id": "1fbaoqud476",
81
+ "html": "<a class=\"fill house\"> <span class=\"headline\"></span> <span class=\"text\"></span> <span class=\"cta\"></span> </a>",
82
+ "parts": [
83
+ [
84
+ 3,
85
+ [0],
86
+ "href"
87
+ ],
88
+ [1, [0, 1]],
89
+ [1, [0, 3]],
90
+ [1, [0, 5]]
91
+ ],
92
+ "segments": [
93
+ "<a class=\"fill house\"",
94
+ {
95
+ "k": "attr",
96
+ "name": "href"
97
+ },
98
+ "> <span class=\"headline\">",
99
+ {
100
+ "k": "child",
101
+ "in": "span"
102
+ },
103
+ "</span> <span class=\"text\">",
104
+ {
105
+ "k": "child",
106
+ "in": "span"
107
+ },
108
+ "</span> <span class=\"cta\">",
109
+ {
110
+ "k": "child",
111
+ "in": "span"
112
+ },
113
+ "</span> </a>"
114
+ ]
115
+ };
116
+ var _gyral$t2 = {
117
+ "id": "2bnfexl58wh",
118
+ "html": "<div part=\"box\" data-intent-on=\"jy-ad-near\"><p class=\"label\"></p></div>",
119
+ "parts": [
120
+ [
121
+ 3,
122
+ [0],
123
+ "class"
124
+ ],
125
+ [
126
+ 3,
127
+ [0],
128
+ "data-slot"
129
+ ],
130
+ [
131
+ 3,
132
+ [0],
133
+ "data-flow"
134
+ ],
135
+ [
136
+ 3,
137
+ [0],
138
+ "data-intent"
139
+ ],
140
+ [7, [0]],
141
+ [1, [0, 0]],
142
+ [0, [0]]
143
+ ],
144
+ "segments": [
145
+ "<div part=\"box\" data-intent-on=\"jy-ad-near\"",
146
+ {
147
+ "k": "attr",
148
+ "name": "class"
149
+ },
150
+ {
151
+ "k": "attr",
152
+ "name": "data-slot"
153
+ },
154
+ {
155
+ "k": "attr",
156
+ "name": "data-flow"
157
+ },
158
+ {
159
+ "k": "attr",
160
+ "name": "data-intent"
161
+ },
162
+ { "k": "hook" },
163
+ "><p class=\"label\">",
164
+ {
165
+ "k": "child",
166
+ "in": "p"
167
+ },
168
+ "</p>",
169
+ {
170
+ "k": "child",
171
+ "in": "div"
172
+ },
173
+ "</div>"
174
+ ]
175
+ };
176
+ /** The v1 source: every visitor sees ads, so it never changes. */
177
+ var alwaysShowAds = {
178
+ get: () => "show",
179
+ subscribe: () => () => {}
180
+ };
181
+ var watchEntitlement = (source) => subscription("ad-entitlement", (emit) => {
182
+ emit(source.get());
183
+ return source.subscribe(emit);
184
+ });
185
+ /** The default entitlement driver: `alwaysShowAds`. */
186
+ var adEntitlement = watchEntitlement(alwaysShowAds);
187
+ /** Gives every `<ad-slot>` below `element` its entitlement from `source`. */
188
+ var provideAdEntitlement = (element, source) => provideDrivers(element, { "ad-entitlement": watchEntitlement(source) });
189
+ /** `navigator.onLine` now and on every `online` / `offline` event. */
190
+ var connectivity = subscription("connectivity", (emit) => {
191
+ const report = () => emit(navigator.onLine);
192
+ report();
193
+ addEventListener("online", report);
194
+ addEventListener("offline", report);
195
+ return () => {
196
+ removeEventListener("online", report);
197
+ removeEventListener("offline", report);
198
+ };
199
+ });
200
+ /**
201
+ * Asks the ad endpoint for a slot's fill and parses the answer at the boundary. No cookies go
202
+ * with it. Fails on a network error, a non-2xx status or an answer that doesn't parse.
203
+ */
204
+ var adServer = defineDriver({
205
+ name: "ad-server",
206
+ run: async (url, { signal }) => {
207
+ const response = await fetch(url, {
208
+ signal,
209
+ credentials: "omit",
210
+ headers: { accept: "application/json" }
211
+ });
212
+ if (!response.ok) throw new Error(`ad endpoint answered ${String(response.status)}`);
213
+ const fill = parseAdFill(await response.json());
214
+ if (fill === null) throw new Error("ad endpoint answer did not parse");
215
+ return fill;
216
+ },
217
+ toError: (cause) => cause instanceof Error ? cause.message : String(cause)
218
+ });
219
+ /** Sends an impression beacon: a bodiless POST that outlives the page, without cookies. */
220
+ var adBeacon = defineDriver({
221
+ name: "ad-beacon",
222
+ run: async (url) => {
223
+ await fetch(url, {
224
+ method: "POST",
225
+ keepalive: true,
226
+ credentials: "omit"
227
+ });
228
+ },
229
+ toError: (cause) => cause instanceof Error ? cause.message : String(cause)
230
+ });
231
+ /** What the slot shows: its reserved box (empty or filled), or nothing at all. Pure. */
232
+ var slotPresentation = (s) => s.entitlement === "hide" || s.online === false || s.ad._tag === "Empty" ? "collapsed" : "reserved";
233
+ /** A Standard Schema from a type guard, so an invalid attribute is reported, not guessed at. */
234
+ var guard = (is, message) => ({ "~standard": {
235
+ version: 1,
236
+ vendor: "joystyk",
237
+ validate: (value) => is(value) ? { value } : { issues: [{ message }] }
238
+ } });
239
+ var isSurface = (value) => SLOT_SURFACES.includes(value);
240
+ var props = {
241
+ /** The registry id (attribute `slot-id`; `slot` is taken by the platform). */
242
+ slotId: prop.string({
243
+ required: true,
244
+ schema: guard(isSlotId, "Expected a slot id from packages/ui/src/slots.ts.")
245
+ }),
246
+ /** The kind of page the slot is on (attribute `page`). Default: the slot's first surface. */
247
+ page: prop.string({ schema: guard(isSurface, `Expected one of ${SLOT_SURFACES.join(", ")}.`) }),
248
+ /** The game the page or screen is about (attribute `game-id`). */
249
+ gameId: prop.string({ schema: guard(isContextId, "Expected a kebab-case game id.") }),
250
+ /** The category the page is about (attribute `category`). */
251
+ category: prop.string({ schema: guard(isContextId, "Expected a kebab-case category id.") })
252
+ };
253
+ /** The context the slot sends: what the page is about, never who is looking. */
254
+ var queryOf = (p) => ({
255
+ slot: p.slotId,
256
+ page: p.page ?? defaultSurface(p.slotId),
257
+ game: p.gameId ?? null,
258
+ category: p.category ?? null
259
+ });
260
+ var watchAds = () => command(adEntitlement, void 0, { onSuccess: (entitlement) => ({
261
+ _tag: "Entitled",
262
+ entitlement
263
+ }) });
264
+ var watchConnectivity = () => command(connectivity, void 0, { onSuccess: (online) => ({
265
+ _tag: "Connectivity",
266
+ online
267
+ }) });
268
+ /** Asks the endpoint for `url`'s fill; the latest request wins. */
269
+ var requestFill = (url) => command(adServer, url, {
270
+ onSuccess: (fill) => ({
271
+ _tag: "Loaded",
272
+ url,
273
+ fill
274
+ }),
275
+ onFailure: () => ({
276
+ _tag: "LoadFailed",
277
+ url
278
+ }),
279
+ key: "ad-server",
280
+ concurrency: "switch"
281
+ });
282
+ /** Reports an impression. Nothing comes back: a lost beacon is a lost count, never an error shown. */
283
+ var sendBeacon = (url) => command(adBeacon, url, { onFailure: () => void 0 });
284
+ /** Starts the request once the box is near, ads are shown and the browser is online. */
285
+ var load = (s, p) => {
286
+ if (!s.near || s.ad._tag !== "Waiting" || s.entitlement === "hide" || s.online === false) return s;
287
+ const url = adQueryUrl(queryOf(p));
288
+ return [{
289
+ ...s,
290
+ ad: {
291
+ _tag: "Loading",
292
+ url
293
+ }
294
+ }, [requestFill(url)]];
295
+ };
296
+ var urlOf = (ad) => ad._tag === "Waiting" ? null : ad.url;
297
+ var NEAR_EVENT = "jy-ad-near";
298
+ var IMAGE_EVENT = "jy-ad-image";
299
+ var observers = /* @__PURE__ */ new WeakMap();
300
+ /**
301
+ * Tells the slot, once, when its box comes within 300 px of the viewport: the lazy load.
302
+ * Dispatches `jy-ad-near` on the box.
303
+ */
304
+ var nearViewport = defineHook({
305
+ client: (el, _args, prev) => {
306
+ if (prev !== void 0) return;
307
+ const observer = new IntersectionObserver((entries) => {
308
+ if (!entries.some((e) => e.isIntersecting)) return;
309
+ observer.disconnect();
310
+ el.dispatchEvent(new CustomEvent(NEAR_EVENT, { bubbles: true }));
311
+ }, { rootMargin: "300px" });
312
+ observer.observe(el);
313
+ observers.set(el, () => observer.disconnect());
314
+ },
315
+ dispose: (el) => {
316
+ observers.get(el)?.();
317
+ observers.delete(el);
318
+ }
319
+ });
320
+ /**
321
+ * Reports a creative's image: `seen` once it has loaded and at least half of it is on
322
+ * screen, `broken` if it fails to load. Dispatches `jy-ad-image` with the status as detail.
323
+ */
324
+ var reportImage = defineHook({
325
+ client: (el, [src], prev) => {
326
+ if (prev?.[0] === src) return;
327
+ observers.get(el)?.();
328
+ if (!(el instanceof HTMLImageElement)) return;
329
+ let visible = false;
330
+ let done = false;
331
+ const report = (status) => {
332
+ if (done) return;
333
+ done = true;
334
+ stop();
335
+ el.dispatchEvent(new CustomEvent(IMAGE_EVENT, {
336
+ bubbles: true,
337
+ detail: status
338
+ }));
339
+ };
340
+ const check = () => {
341
+ if (el.complete && el.naturalWidth > 0 && visible) report("seen");
342
+ };
343
+ const onError = () => report("broken");
344
+ const observer = new IntersectionObserver((entries) => {
345
+ visible = entries.some((e) => e.isIntersecting);
346
+ check();
347
+ }, { threshold: .5 });
348
+ const stop = () => {
349
+ observer.disconnect();
350
+ el.removeEventListener("load", check);
351
+ el.removeEventListener("error", onError);
352
+ };
353
+ el.addEventListener("load", check);
354
+ el.addEventListener("error", onError);
355
+ observer.observe(el);
356
+ observers.set(el, stop);
357
+ if (el.complete && el.naturalWidth === 0 && el.getAttribute("src") !== "") queueMicrotask(() => {
358
+ if (el.complete && el.naturalWidth === 0) onError();
359
+ });
360
+ },
361
+ dispose: (el) => {
362
+ observers.get(el)?.();
363
+ observers.delete(el);
364
+ }
365
+ });
366
+ var styles = css`
367
+ @layer components {
368
+ :host {
369
+ display: block;
370
+ container-type: inline-size;
371
+ }
372
+ :host(:state(collapsed)) {
373
+ display: none;
374
+ }
375
+ .box {
376
+ box-sizing: border-box;
377
+ min-block-size: 0;
378
+ /* The same grid empty or filled: the label never moves when a fill arrives (no layout shift). */
379
+ grid-template-rows: auto minmax(0, 1fr);
380
+ gap: var(--jy-space-3xs, 0.25rem);
381
+ margin-inline: auto;
382
+ padding: var(--jy-space-2xs, 0.5rem);
383
+ border: 1px dashed color-mix(in oklch, var(--jy-text, CanvasText) 30%, transparent);
384
+ border-radius: var(--jy-radius-s, 0.5rem);
385
+ background: repeating-linear-gradient(
386
+ 135deg,
387
+ transparent 0 10px,
388
+ color-mix(in oklch, var(--jy-text, CanvasText) 4%, transparent) 10px 20px
389
+ );
390
+ color: var(--jy-text-muted, GrayText);
391
+ font-size: 0.8rem;
392
+ text-align: center;
393
+ overflow: clip;
394
+ contain: layout paint;
395
+ }
396
+ .box.filled {
397
+ border-style: solid;
398
+ background: var(--jy-surface-raised, Canvas);
399
+ }
400
+ p {
401
+ margin: 0;
402
+ }
403
+ /* The label reads before anything else in the slot, as the ASA's 2022 rulings ask: bold
404
+ 12 px capitals on a solid chip in the page's text colour (over 14:1 contrast in both
405
+ schemes), unlike the slot's surface and outside the creative (advertising-policy.md, 5). */
406
+ .label {
407
+ justify-self: start;
408
+ padding-inline: 0.5em;
409
+ border-radius: 0.25rem;
410
+ background: var(--jy-text, CanvasText);
411
+ color: var(--jy-surface-raised, Canvas);
412
+ font-family: var(--jy-font-mono, ui-monospace, monospace);
413
+ font-size: 0.75rem;
414
+ font-weight: 700;
415
+ line-height: 1.4;
416
+ letter-spacing: 0.08em;
417
+ text-transform: uppercase;
418
+ text-align: start;
419
+ }
420
+ .fill {
421
+ display: grid;
422
+ grid-template-rows: minmax(0, 1fr) auto;
423
+ gap: var(--jy-space-3xs, 0.25rem);
424
+ min-block-size: 0;
425
+ color: var(--jy-text, CanvasText);
426
+ text-decoration: none;
427
+ border-radius: var(--jy-radius-s, 0.5rem);
428
+ }
429
+ .fill:focus-visible {
430
+ outline: 2px solid var(--jy-focus-ring, Highlight);
431
+ outline-offset: 2px;
432
+ }
433
+ .fill:hover .headline,
434
+ .fill:hover .cta {
435
+ text-decoration: underline;
436
+ }
437
+ img {
438
+ display: block;
439
+ inline-size: 100%;
440
+ block-size: 100%;
441
+ min-block-size: 0;
442
+ object-fit: contain;
443
+ }
444
+ .headline {
445
+ font-weight: 600;
446
+ line-height: 1.25;
447
+ display: -webkit-box;
448
+ -webkit-box-orient: vertical;
449
+ -webkit-line-clamp: 2;
450
+ line-clamp: 2;
451
+ overflow: hidden;
452
+ }
453
+ .house {
454
+ grid-template-rows: auto auto auto;
455
+ align-content: center;
456
+ justify-items: center;
457
+ }
458
+ .house .text {
459
+ color: var(--jy-text-muted, GrayText);
460
+ display: -webkit-box;
461
+ -webkit-box-orient: vertical;
462
+ -webkit-line-clamp: 5;
463
+ line-clamp: 5;
464
+ overflow: hidden;
465
+ }
466
+ .house .cta {
467
+ color: var(--jy-accent, LinkText);
468
+ font-weight: 600;
469
+ }
470
+ /* Short boxes (banners, the presenting line): image and words side by side. */
471
+ .box[data-flow='row'] {
472
+ padding-block: var(--jy-space-3xs, 0.25rem);
473
+ gap: 0;
474
+ }
475
+ .box.filled[data-flow='row'] .fill {
476
+ grid-template-rows: minmax(0, 1fr);
477
+ grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
478
+ align-items: center;
479
+ text-align: start;
480
+ }
481
+ .box.filled[data-flow='row'] .house {
482
+ grid-template-columns: minmax(0, 1fr) auto;
483
+ grid-template-rows: auto auto;
484
+ justify-items: start;
485
+ }
486
+ .box.filled[data-flow='row'] .house .text {
487
+ grid-column: 1;
488
+ -webkit-line-clamp: 1;
489
+ line-clamp: 1;
490
+ }
491
+ .box.filled[data-flow='row'] .house .cta {
492
+ grid-column: 2;
493
+ grid-row: 1 / span 2;
494
+ }
495
+ .box.filled[data-flow='row'] .headline {
496
+ -webkit-line-clamp: 1;
497
+ line-clamp: 1;
498
+ }
499
+ @media (forced-colors: active) {
500
+ .box {
501
+ border-color: CanvasText;
502
+ }
503
+ .label {
504
+ border: 1px solid CanvasText;
505
+ }
506
+ }
507
+ }
508
+ `;
509
+ /** Banners and the presenting line are short: their fill lays out in a row. */
510
+ var flowOf = (slotId) => {
511
+ const shape = slot(slotId).shape;
512
+ return shape === "medium rectangle" || shape === "tall rail" ? "column" : "row";
513
+ };
514
+ var creativeView = (f, intent) => compiled(_gyral$t0, [
515
+ f.href,
516
+ intent,
517
+ f.image.src,
518
+ f.alt,
519
+ f.image.width,
520
+ f.image.height,
521
+ reportImage(f.image.src),
522
+ f.headline
523
+ ]);
524
+ var houseView = (f) => compiled(_gyral$t1, [
525
+ f.href,
526
+ f.headline,
527
+ f.text,
528
+ f.cta
529
+ ]);
530
+ var AdSlot = define()("ad-slot", {
531
+ props,
532
+ init: () => ({
533
+ entitlement: "show",
534
+ online: null,
535
+ near: false,
536
+ ad: { _tag: "Waiting" }
537
+ }),
538
+ subscriptions: () => [watchAds(), watchConnectivity()],
539
+ states: (s) => ({
540
+ collapsed: slotPresentation(s) === "collapsed",
541
+ online: s.online === true
542
+ }),
543
+ intent: {
544
+ Near: () => ({ _tag: "Near" }),
545
+ Image: ({ event }) => {
546
+ const status = event instanceof CustomEvent ? event.detail : void 0;
547
+ return status === "seen" || status === "broken" ? {
548
+ _tag: "Image",
549
+ status
550
+ } : void 0;
551
+ }
552
+ },
553
+ update: {
554
+ Entitled: (s, m, { props: p }) => load({
555
+ ...s,
556
+ entitlement: m.entitlement
557
+ }, p),
558
+ Connectivity: (s, m, { props: p }) => load({
559
+ ...s,
560
+ online: m.online
561
+ }, p),
562
+ Near: (s, _m, { props: p }) => load({
563
+ ...s,
564
+ near: true
565
+ }, p),
566
+ Loaded: (s, m) => urlOf(s.ad) !== m.url || s.ad._tag !== "Loading" ? s : {
567
+ ...s,
568
+ ad: m.fill.kind === "collapse" ? {
569
+ _tag: "Empty",
570
+ url: m.url
571
+ } : {
572
+ _tag: "Filled",
573
+ url: m.url,
574
+ fill: m.fill,
575
+ seen: false
576
+ }
577
+ },
578
+ LoadFailed: (s, m) => urlOf(s.ad) !== m.url || s.ad._tag !== "Loading" ? s : {
579
+ ...s,
580
+ ad: {
581
+ _tag: "Empty",
582
+ url: m.url
583
+ }
584
+ },
585
+ Image: (s, m) => {
586
+ if (s.ad._tag !== "Filled" || s.ad.fill.kind !== "creative") return s;
587
+ if (m.status === "broken") return {
588
+ ...s,
589
+ ad: {
590
+ _tag: "Empty",
591
+ url: s.ad.url
592
+ }
593
+ };
594
+ return s.ad.seen ? s : [{
595
+ ...s,
596
+ ad: {
597
+ ...s.ad,
598
+ seen: true
599
+ }
600
+ }, [sendBeacon(s.ad.fill.beacon)]];
601
+ },
602
+ PropsChanged: (s, { props: p }) => {
603
+ const url = adQueryUrl(queryOf(p));
604
+ return urlOf(s.ad) === url ? s : load({
605
+ ...s,
606
+ ad: { _tag: "Waiting" }
607
+ }, p);
608
+ }
609
+ },
610
+ view: (s, i, { props: p }) => {
611
+ if (slotPresentation(s) === "collapsed") return nothing;
612
+ const fill = s.ad._tag === "Filled" ? s.ad.fill : null;
613
+ return compiled(_gyral$t2, [
614
+ fill === null ? "box" : "box filled",
615
+ p.slotId,
616
+ flowOf(p.slotId),
617
+ i.Near,
618
+ nearViewport(),
619
+ slot(p.slotId).label,
620
+ fill === null ? nothing : fill.kind === "creative" ? creativeView(fill, i.Image) : houseView(fill)
621
+ ]);
622
+ },
623
+ styles: [styles, slotBoxStyles()]
624
+ });
625
+ //#endregion
626
+ export { AdSlot, adBeacon, adEntitlement, adServer, alwaysShowAds, connectivity, provideAdEntitlement, queryOf, requestFill, slotPresentation };
627
+
628
+ //# sourceMappingURL=ad-slot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ad-slot.js","names":["_gyral$compiled"],"sources":["../src/ad-slot.ts"],"sourcesContent":["// <ad-slot>: the one billboard component (docs/design-docs/monetization.md). It reserves its\n// slot's exact box from the slot registry, labels it as advertising and, once the box comes\n// near the viewport, asks the site's ad endpoint what to show: a booked creative (an image\n// and a headline, linked through the first-party click notice), a house promotion, or\n// nothing. Whatever arrives fills the reserved box, so nothing shifts. Games never render it;\n// it belongs to the frame (D-07), and nothing it shows ever enters a game's canvas.\n//\n// The request carries the page's context only (ad-wire.ts): the slot, the kind of page and\n// the game or category it is about. No cookies (`credentials: 'omit'`), no user or device id.\n//\n// Collapse rules, in `slotPresentation`:\n// - the entitlement source says \"hide\" (ad-free, v2): the slot collapses;\n// - the browser is offline: the slot collapses. Until the connectivity driver first reports,\n// the slot assumes it is online and keeps the box the server rendered; on a page the\n// service worker served offline (`data-offline` on <html>), tokens.css hides it instead\n// until it hears it is online (its `online` state), so nothing shifts at first paint;\n// - the endpoint said \"collapse\", failed, or sent something that doesn't parse, or the\n// creative's image failed to load: the slot collapses;\n// - otherwise it keeps its reserved box; at a width where the registry has no box (the rail\n// on narrow screens) CSS hides it.\n//\n// Impressions: a creative counts once, when its image has loaded and at least half of it is\n// on screen; the slot then sends one beacon (monetization.md, \"Counting\"). House promotions\n// aren't counted.\n//\n// The entitlement source, the connectivity signal, the ad request and the beacon are drivers,\n// substituted by name: `provideAdEntitlement(element, source)` above the slots, `el.drivers`\n// or `provideDrivers` in tests.\nimport {\n command,\n css,\n define,\n defineHook,\n defineDriver,\n html,\n nothing,\n prop,\n provideDrivers,\n subscription,\n type Command,\n type Next,\n type PropsOf,\n} from '@gyral/core';\nimport {\n adQueryUrl,\n defaultSurface,\n isContextId,\n parseAdFill,\n SLOT_SURFACES,\n type AdFill,\n type AdQuery,\n type CreativeFill,\n type HouseFill,\n} from './ad-wire.ts';\nimport { isSlotId, slot, slotBoxStyles, type SlotId, type SlotSurface } from './slots.ts';\n\n/** Whether the visitor sees ads. 'hide' arrives with ad-free entitlements in v2. */\nexport type AdEntitlement = 'show' | 'hide';\n\n/** Where `<ad-slot>` learns whether to show ads. */\nexport interface AdEntitlementSource {\n get(): AdEntitlement;\n /** Calls `listener` on every change; returns how to stop. */\n subscribe(listener: (entitlement: AdEntitlement) => void): () => void;\n}\n\n/** The v1 source: every visitor sees ads, so it never changes. */\nexport const alwaysShowAds: AdEntitlementSource = {\n get: () => 'show',\n subscribe: () => () => {},\n};\n\nconst watchEntitlement = (source: AdEntitlementSource) =>\n subscription<AdEntitlement>('ad-entitlement', (emit) => {\n emit(source.get());\n return source.subscribe(emit);\n });\n\n/** The default entitlement driver: `alwaysShowAds`. */\nexport const adEntitlement = watchEntitlement(alwaysShowAds);\n\n/** Gives every `<ad-slot>` below `element` its entitlement from `source`. */\nexport const provideAdEntitlement = (element: Element, source: AdEntitlementSource): (() => void) =>\n provideDrivers(element, { 'ad-entitlement': watchEntitlement(source) });\n\n/** `navigator.onLine` now and on every `online` / `offline` event. */\nexport const connectivity = subscription<boolean>('connectivity', (emit) => {\n const report = (): void => emit(navigator.onLine);\n report();\n addEventListener('online', report);\n addEventListener('offline', report);\n return () => {\n removeEventListener('online', report);\n removeEventListener('offline', report);\n };\n});\n\n/**\n * Asks the ad endpoint for a slot's fill and parses the answer at the boundary. No cookies go\n * with it. Fails on a network error, a non-2xx status or an answer that doesn't parse.\n */\nexport const adServer = defineDriver<string, AdFill, string>({\n name: 'ad-server',\n run: async (url, { signal }) => {\n const response = await fetch(url, { signal, credentials: 'omit', headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`ad endpoint answered ${String(response.status)}`);\n const fill = parseAdFill(await response.json());\n if (fill === null) throw new Error('ad endpoint answer did not parse');\n return fill;\n },\n toError: (cause) => (cause instanceof Error ? cause.message : String(cause)),\n});\n\n/** Sends an impression beacon: a bodiless POST that outlives the page, without cookies. */\nexport const adBeacon = defineDriver<string, void, string>({\n name: 'ad-beacon',\n run: async (url) => {\n await fetch(url, { method: 'POST', keepalive: true, credentials: 'omit' });\n },\n toError: (cause) => (cause instanceof Error ? cause.message : String(cause)),\n});\n\n/** Where the slot's fill stands. */\nexport type AdLoad =\n /** Not near the viewport yet: nothing asked. */\n | { readonly _tag: 'Waiting' }\n | { readonly _tag: 'Loading'; readonly url: string }\n | { readonly _tag: 'Filled'; readonly url: string; readonly fill: CreativeFill | HouseFill; readonly seen: boolean }\n /** Nothing to show, or something failed: the slot collapses. */\n | { readonly _tag: 'Empty'; readonly url: string };\n\nexport interface AdSlotState {\n readonly entitlement: AdEntitlement;\n /** What the connectivity driver last said; null until it first reports (assumed online). */\n readonly online: boolean | null;\n /** The box has come near the viewport once. */\n readonly near: boolean;\n readonly ad: AdLoad;\n}\n\nexport type AdSlotMsg =\n | { readonly _tag: 'Entitled'; readonly entitlement: AdEntitlement }\n | { readonly _tag: 'Connectivity'; readonly online: boolean }\n /** The box came within a screen's reach of the viewport. */\n | { readonly _tag: 'Near' }\n | { readonly _tag: 'Loaded'; readonly url: string; readonly fill: AdFill }\n | { readonly _tag: 'LoadFailed'; readonly url: string }\n /** The creative's image loaded and is at least half on screen, or it failed to load. */\n | { readonly _tag: 'Image'; readonly status: 'seen' | 'broken' };\n\n/** What the slot shows: its reserved box (empty or filled), or nothing at all. Pure. */\nexport const slotPresentation = (s: AdSlotState): 'reserved' | 'collapsed' =>\n s.entitlement === 'hide' || s.online === false || s.ad._tag === 'Empty' ? 'collapsed' : 'reserved';\n\n/** A Standard Schema from a type guard, so an invalid attribute is reported, not guessed at. */\nconst guard = <T extends string>(is: (value: unknown) => value is T, message: string) => ({\n '~standard': {\n version: 1 as const,\n vendor: 'joystyk',\n validate: (value: unknown): { readonly value: T } | { readonly issues: readonly { readonly message: string }[] } =>\n is(value) ? { value } : { issues: [{ message }] },\n },\n});\n\nconst isSurface = (value: unknown): value is SlotSurface => (SLOT_SURFACES as readonly unknown[]).includes(value);\n\nconst props = {\n /** The registry id (attribute `slot-id`; `slot` is taken by the platform). */\n slotId: prop.string({\n required: true,\n schema: guard<SlotId>(isSlotId, 'Expected a slot id from packages/ui/src/slots.ts.'),\n }),\n /** The kind of page the slot is on (attribute `page`). Default: the slot's first surface. */\n page: prop.string({ schema: guard(isSurface, `Expected one of ${SLOT_SURFACES.join(', ')}.`) }),\n /** The game the page or screen is about (attribute `game-id`). */\n gameId: prop.string({ schema: guard(isContextId, 'Expected a kebab-case game id.') }),\n /** The category the page is about (attribute `category`). */\n category: prop.string({ schema: guard(isContextId, 'Expected a kebab-case category id.') }),\n};\nexport type AdSlotProps = PropsOf<typeof props>;\n\n/** The context the slot sends: what the page is about, never who is looking. */\nexport const queryOf = (p: AdSlotProps): AdQuery => ({\n slot: p.slotId,\n page: p.page ?? defaultSurface(p.slotId),\n game: p.gameId ?? null,\n category: p.category ?? null,\n});\n\nconst watchAds = (): Command<AdSlotMsg> =>\n command(adEntitlement, undefined, { onSuccess: (entitlement): AdSlotMsg => ({ _tag: 'Entitled', entitlement }) });\nconst watchConnectivity = (): Command<AdSlotMsg> =>\n command(connectivity, undefined, { onSuccess: (online): AdSlotMsg => ({ _tag: 'Connectivity', online }) });\n\n/** Asks the endpoint for `url`'s fill; the latest request wins. */\nexport const requestFill = (url: string): Command<AdSlotMsg> =>\n command(adServer, url, {\n onSuccess: (fill): AdSlotMsg => ({ _tag: 'Loaded', url, fill }),\n onFailure: (): AdSlotMsg => ({ _tag: 'LoadFailed', url }),\n key: 'ad-server',\n concurrency: 'switch',\n });\n\n/** Reports an impression. Nothing comes back: a lost beacon is a lost count, never an error shown. */\nconst sendBeacon = (url: string): Command<AdSlotMsg> =>\n command<string, void, string, AdSlotMsg>(adBeacon, url, { onFailure: () => undefined });\n\n/** Starts the request once the box is near, ads are shown and the browser is online. */\nconst load = (s: AdSlotState, p: AdSlotProps): Next<AdSlotState, AdSlotMsg> => {\n if (!s.near || s.ad._tag !== 'Waiting' || s.entitlement === 'hide' || s.online === false) return s;\n const url = adQueryUrl(queryOf(p));\n return [{ ...s, ad: { _tag: 'Loading', url } }, [requestFill(url)]];\n};\n\nconst urlOf = (ad: AdLoad): string | null => (ad._tag === 'Waiting' ? null : ad.url);\n\nconst NEAR_EVENT = 'jy-ad-near';\nconst IMAGE_EVENT = 'jy-ad-image';\n\nconst observers = new WeakMap<Element, () => void>();\n\n/**\n * Tells the slot, once, when its box comes within 300 px of the viewport: the lazy load.\n * Dispatches `jy-ad-near` on the box.\n */\nconst nearViewport = defineHook<[]>({\n client: (el, _args, prev) => {\n if (prev !== undefined) return;\n const observer = new IntersectionObserver(\n (entries) => {\n if (!entries.some((e) => e.isIntersecting)) return;\n observer.disconnect();\n el.dispatchEvent(new CustomEvent(NEAR_EVENT, { bubbles: true }));\n },\n { rootMargin: '300px' },\n );\n observer.observe(el);\n observers.set(el, () => observer.disconnect());\n },\n dispose: (el) => {\n observers.get(el)?.();\n observers.delete(el);\n },\n});\n\n/**\n * Reports a creative's image: `seen` once it has loaded and at least half of it is on\n * screen, `broken` if it fails to load. Dispatches `jy-ad-image` with the status as detail.\n */\nconst reportImage = defineHook<[src: string]>({\n client: (el, [src], prev) => {\n if (prev?.[0] === src) return;\n observers.get(el)?.();\n if (!(el instanceof HTMLImageElement)) return;\n let visible = false;\n let done = false;\n const report = (status: 'seen' | 'broken'): void => {\n if (done) return;\n done = true;\n stop();\n el.dispatchEvent(new CustomEvent(IMAGE_EVENT, { bubbles: true, detail: status }));\n };\n const check = (): void => {\n if (el.complete && el.naturalWidth > 0 && visible) report('seen');\n };\n const onError = (): void => report('broken');\n const observer = new IntersectionObserver(\n (entries) => {\n visible = entries.some((e) => e.isIntersecting);\n check();\n },\n { threshold: 0.5 },\n );\n const stop = (): void => {\n observer.disconnect();\n el.removeEventListener('load', check);\n el.removeEventListener('error', onError);\n };\n el.addEventListener('load', check);\n el.addEventListener('error', onError);\n observer.observe(el);\n observers.set(el, stop);\n if (el.complete && el.naturalWidth === 0 && el.getAttribute('src') !== '') {\n // Already failed before the listener was attached.\n queueMicrotask(() => {\n if (el.complete && el.naturalWidth === 0) onError();\n });\n }\n },\n dispose: (el) => {\n observers.get(el)?.();\n observers.delete(el);\n },\n});\n\nconst styles = css`\n @layer components {\n :host {\n display: block;\n container-type: inline-size;\n }\n :host(:state(collapsed)) {\n display: none;\n }\n .box {\n box-sizing: border-box;\n min-block-size: 0;\n /* The same grid empty or filled: the label never moves when a fill arrives (no layout shift). */\n grid-template-rows: auto minmax(0, 1fr);\n gap: var(--jy-space-3xs, 0.25rem);\n margin-inline: auto;\n padding: var(--jy-space-2xs, 0.5rem);\n border: 1px dashed color-mix(in oklch, var(--jy-text, CanvasText) 30%, transparent);\n border-radius: var(--jy-radius-s, 0.5rem);\n background: repeating-linear-gradient(\n 135deg,\n transparent 0 10px,\n color-mix(in oklch, var(--jy-text, CanvasText) 4%, transparent) 10px 20px\n );\n color: var(--jy-text-muted, GrayText);\n font-size: 0.8rem;\n text-align: center;\n overflow: clip;\n contain: layout paint;\n }\n .box.filled {\n border-style: solid;\n background: var(--jy-surface-raised, Canvas);\n }\n p {\n margin: 0;\n }\n /* The label reads before anything else in the slot, as the ASA's 2022 rulings ask: bold\n 12 px capitals on a solid chip in the page's text colour (over 14:1 contrast in both\n schemes), unlike the slot's surface and outside the creative (advertising-policy.md, 5). */\n .label {\n justify-self: start;\n padding-inline: 0.5em;\n border-radius: 0.25rem;\n background: var(--jy-text, CanvasText);\n color: var(--jy-surface-raised, Canvas);\n font-family: var(--jy-font-mono, ui-monospace, monospace);\n font-size: 0.75rem;\n font-weight: 700;\n line-height: 1.4;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n text-align: start;\n }\n .fill {\n display: grid;\n grid-template-rows: minmax(0, 1fr) auto;\n gap: var(--jy-space-3xs, 0.25rem);\n min-block-size: 0;\n color: var(--jy-text, CanvasText);\n text-decoration: none;\n border-radius: var(--jy-radius-s, 0.5rem);\n }\n .fill:focus-visible {\n outline: 2px solid var(--jy-focus-ring, Highlight);\n outline-offset: 2px;\n }\n .fill:hover .headline,\n .fill:hover .cta {\n text-decoration: underline;\n }\n img {\n display: block;\n inline-size: 100%;\n block-size: 100%;\n min-block-size: 0;\n object-fit: contain;\n }\n .headline {\n font-weight: 600;\n line-height: 1.25;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow: hidden;\n }\n .house {\n grid-template-rows: auto auto auto;\n align-content: center;\n justify-items: center;\n }\n .house .text {\n color: var(--jy-text-muted, GrayText);\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 5;\n line-clamp: 5;\n overflow: hidden;\n }\n .house .cta {\n color: var(--jy-accent, LinkText);\n font-weight: 600;\n }\n /* Short boxes (banners, the presenting line): image and words side by side. */\n .box[data-flow='row'] {\n padding-block: var(--jy-space-3xs, 0.25rem);\n gap: 0;\n }\n .box.filled[data-flow='row'] .fill {\n grid-template-rows: minmax(0, 1fr);\n grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);\n align-items: center;\n text-align: start;\n }\n .box.filled[data-flow='row'] .house {\n grid-template-columns: minmax(0, 1fr) auto;\n grid-template-rows: auto auto;\n justify-items: start;\n }\n .box.filled[data-flow='row'] .house .text {\n grid-column: 1;\n -webkit-line-clamp: 1;\n line-clamp: 1;\n }\n .box.filled[data-flow='row'] .house .cta {\n grid-column: 2;\n grid-row: 1 / span 2;\n }\n .box.filled[data-flow='row'] .headline {\n -webkit-line-clamp: 1;\n line-clamp: 1;\n }\n @media (forced-colors: active) {\n .box {\n border-color: CanvasText;\n }\n .label {\n border: 1px solid CanvasText;\n }\n }\n }\n`;\n\n/** Banners and the presenting line are short: their fill lays out in a row. */\nconst flowOf = (slotId: SlotId): 'row' | 'column' => {\n const shape = slot(slotId).shape;\n return shape === 'medium rectangle' || shape === 'tall rail' ? 'column' : 'row';\n};\n\n// `sponsored` for search engines; `external` keeps the catalog's router from handling the click\n// itself: the browser asks the server, which counts it and answers with \"You're leaving Joystyk\".\nconst creativeView = (f: CreativeFill, intent: string) => html`<a\n class=\"fill creative\"\n href=${f.href}\n rel=\"sponsored noopener external\"\n data-intent=${intent}\n data-intent-on=\"jy-ad-image\"\n>\n <img src=${f.image.src} alt=${f.alt} width=${f.image.width} height=${f.image.height} decoding=\"async\" ${reportImage(f.image.src)} />\n <span class=\"headline\">${f.headline}</span>\n</a>`;\n\nconst houseView = (f: HouseFill) => html`<a class=\"fill house\" href=${f.href}>\n <span class=\"headline\">${f.headline}</span>\n <span class=\"text\">${f.text}</span>\n <span class=\"cta\">${f.cta}</span>\n</a>`;\n\nexport const AdSlot = define<AdSlotState, AdSlotMsg, AdSlotProps>()('ad-slot', {\n props,\n init: () => ({ entitlement: 'show', online: null, near: false, ad: { _tag: 'Waiting' } }),\n subscriptions: () => [watchAds(), watchConnectivity()],\n // `online` only once the browser says so: tokens.css keeps a slot on a page served offline\n // hidden without it.\n states: (s) => ({ collapsed: slotPresentation(s) === 'collapsed', online: s.online === true }),\n intent: {\n Near: () => ({ _tag: 'Near' }),\n Image: ({ event }) => {\n const status = event instanceof CustomEvent ? (event.detail as unknown) : undefined;\n return status === 'seen' || status === 'broken' ? { _tag: 'Image', status } : undefined;\n },\n },\n update: {\n Entitled: (s, m, { props: p }) => load({ ...s, entitlement: m.entitlement }, p),\n Connectivity: (s, m, { props: p }) => load({ ...s, online: m.online }, p),\n Near: (s, _m, { props: p }) => load({ ...s, near: true }, p),\n Loaded: (s, m) =>\n urlOf(s.ad) !== m.url || s.ad._tag !== 'Loading'\n ? s\n : {\n ...s,\n ad:\n m.fill.kind === 'collapse'\n ? { _tag: 'Empty', url: m.url }\n : { _tag: 'Filled', url: m.url, fill: m.fill, seen: false },\n },\n LoadFailed: (s, m) =>\n urlOf(s.ad) !== m.url || s.ad._tag !== 'Loading' ? s : { ...s, ad: { _tag: 'Empty', url: m.url } },\n Image: (s, m) => {\n if (s.ad._tag !== 'Filled' || s.ad.fill.kind !== 'creative') return s;\n if (m.status === 'broken') return { ...s, ad: { _tag: 'Empty', url: s.ad.url } };\n return s.ad.seen ? s : [{ ...s, ad: { ...s.ad, seen: true } }, [sendBeacon(s.ad.fill.beacon)]];\n },\n // Client navigation can reuse the element for another game or category: ask again.\n PropsChanged: (s, { props: p }) => {\n const url = adQueryUrl(queryOf(p));\n return urlOf(s.ad) === url ? s : load({ ...s, ad: { _tag: 'Waiting' } }, p);\n },\n },\n view: (s, i, { props: p }) => {\n if (slotPresentation(s) === 'collapsed') return nothing;\n const fill = s.ad._tag === 'Filled' ? s.ad.fill : null;\n return html`<div\n class=${fill === null ? 'box' : 'box filled'}\n data-slot=${p.slotId}\n data-flow=${flowOf(p.slotId)}\n part=\"box\"\n data-intent=${i.Near}\n data-intent-on=\"jy-ad-near\"\n ${nearViewport()}\n >\n <p class=\"label\">${slot(p.slotId).label}</p>\n ${fill === null ? nothing : fill.kind === 'creative' ? creativeView(fill, i.Image) : houseView(fill)}\n </div>`;\n },\n styles: [styles, slotBoxStyles()],\n});\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ad-slot': InstanceType<typeof AdSlot>;\n }\n}\n"],"mappings":"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"}