@joystyk/ui 0.0.0-stage → 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,96 @@
1
+ import * as z from 'zod/mini';
2
+ import { type SlotId, type SlotSurface } from './slots.js';
3
+ /** Where `<ad-slot>` asks which creative to show. */
4
+ export declare const AD_SLOT_PATH = "/api/ads/slot";
5
+ /** Where a rendered creative reports its impression (a beacon, POST with no body). */
6
+ export declare const AD_IMPRESSION_PATH = "/api/ads/impression";
7
+ /** The first-party click notice ("You're leaving Joystyk"): `/ads/click/<creative id>?slot=<slot id>`. */
8
+ export declare const AD_CLICK_PREFIX = "/ads/click/";
9
+ /** Creative images, at content-hashed, immutable URLs: `/ads/creative/<sha256>.<ext>`. */
10
+ export declare const AD_CREATIVE_PREFIX = "/ads/creative/";
11
+ export declare const SLOT_SURFACES: readonly ["home", "category", "game", "results", "daily"];
12
+ /** A game or category id: kebab-case, as the catalog writes them. */
13
+ export declare const isContextId: (value: unknown) => value is string;
14
+ /** The context a slot sends: what the page is about, never who is looking at it. */
15
+ export interface AdQuery {
16
+ readonly slot: SlotId;
17
+ readonly page: SlotSurface;
18
+ /** The game the page or screen is about, if any. The server derives its category. */
19
+ readonly game: string | null;
20
+ /** The category page's category, if any (ignored when `game` is set). */
21
+ readonly category: string | null;
22
+ }
23
+ /** The surface a slot is on when its element doesn't say: the first the registry lists. */
24
+ export declare const defaultSurface: (slotId: SlotId) => SlotSurface;
25
+ /**
26
+ * The request URL for `query`, with its parameters in one fixed order and empty ones left out,
27
+ * so equal contexts share one cache entry at the edge.
28
+ */
29
+ export declare function adQueryUrl(query: AdQuery): string;
30
+ /** A booked creative: an image, its alt text and a headline, linked through the click notice. */
31
+ export declare const CreativeFillSchema: z.ZodMiniObject<{
32
+ kind: z.ZodMiniLiteral<"creative">;
33
+ id: z.ZodMiniInt;
34
+ image: z.ZodMiniObject<{
35
+ src: z.ZodMiniString<string>;
36
+ width: z.ZodMiniInt;
37
+ height: z.ZodMiniInt;
38
+ }, z.core.$strip>;
39
+ alt: z.ZodMiniString<string>;
40
+ headline: z.ZodMiniString<string>;
41
+ href: z.ZodMiniString<string>;
42
+ beacon: z.ZodMiniString<string>;
43
+ }, z.core.$strip>;
44
+ /** Our own promotion for an unsold slot: a game from the catalog, or "Advertise with us". */
45
+ export declare const HouseFillSchema: z.ZodMiniObject<{
46
+ kind: z.ZodMiniLiteral<"house">;
47
+ headline: z.ZodMiniString<string>;
48
+ text: z.ZodMiniString<string>;
49
+ cta: z.ZodMiniString<string>;
50
+ href: z.ZodMiniString<string>;
51
+ }, z.core.$strip>;
52
+ /** Nothing to show: the slot collapses. */
53
+ export declare const CollapseFillSchema: z.ZodMiniObject<{
54
+ kind: z.ZodMiniLiteral<"collapse">;
55
+ }, z.core.$strip>;
56
+ export declare const AdFillSchema: z.ZodMiniDiscriminatedUnion<[z.ZodMiniObject<{
57
+ kind: z.ZodMiniLiteral<"creative">;
58
+ id: z.ZodMiniInt;
59
+ image: z.ZodMiniObject<{
60
+ src: z.ZodMiniString<string>;
61
+ width: z.ZodMiniInt;
62
+ height: z.ZodMiniInt;
63
+ }, z.core.$strip>;
64
+ alt: z.ZodMiniString<string>;
65
+ headline: z.ZodMiniString<string>;
66
+ href: z.ZodMiniString<string>;
67
+ beacon: z.ZodMiniString<string>;
68
+ }, z.core.$strip>, z.ZodMiniObject<{
69
+ kind: z.ZodMiniLiteral<"house">;
70
+ headline: z.ZodMiniString<string>;
71
+ text: z.ZodMiniString<string>;
72
+ cta: z.ZodMiniString<string>;
73
+ href: z.ZodMiniString<string>;
74
+ }, z.core.$strip>, z.ZodMiniObject<{
75
+ kind: z.ZodMiniLiteral<"collapse">;
76
+ }, z.core.$strip>], "kind">;
77
+ export type CreativeFill = z.infer<typeof CreativeFillSchema>;
78
+ export type HouseFill = z.infer<typeof HouseFillSchema>;
79
+ export type AdFill = z.infer<typeof AdFillSchema>;
80
+ /** Parses an endpoint answer; null for anything that doesn't fit, which collapses the slot. */
81
+ export declare const parseAdFill: (value: unknown) => AdFill | null;
82
+ /** The endpoint's query string, as an object of its parameters. */
83
+ export declare const AdQuerySchema: z.ZodMiniObject<{
84
+ slot: z.ZodMiniString<string>;
85
+ page: z.ZodMiniEnum<{
86
+ results: "results";
87
+ home: "home";
88
+ category: "category";
89
+ game: "game";
90
+ daily: "daily";
91
+ }>;
92
+ game: z.ZodMiniOptional<z.ZodMiniString<string>>;
93
+ category: z.ZodMiniOptional<z.ZodMiniString<string>>;
94
+ }, z.core.$strip>;
95
+ /** Parses the query string the endpoint receives. Null when any part is missing or invalid. */
96
+ export declare function parseAdQuery(params: URLSearchParams): AdQuery | null;
@@ -0,0 +1,101 @@
1
+ import { SLOTS, isSlotId } from "./slots.js";
2
+ import * as z from "zod/mini";
3
+ //#region src/ad-wire.ts
4
+ /** Where `<ad-slot>` asks which creative to show. */
5
+ var AD_SLOT_PATH = "/api/ads/slot";
6
+ /** Where a rendered creative reports its impression (a beacon, POST with no body). */
7
+ var AD_IMPRESSION_PATH = "/api/ads/impression";
8
+ /** The first-party click notice ("You're leaving Joystyk"): `/ads/click/<creative id>?slot=<slot id>`. */
9
+ var AD_CLICK_PREFIX = "/ads/click/";
10
+ /** Creative images, at content-hashed, immutable URLs: `/ads/creative/<sha256>.<ext>`. */
11
+ var AD_CREATIVE_PREFIX = "/ads/creative/";
12
+ var SLOT_SURFACES = [
13
+ "home",
14
+ "category",
15
+ "game",
16
+ "results",
17
+ "daily"
18
+ ];
19
+ var KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
20
+ /** A game or category id: kebab-case, as the catalog writes them. */
21
+ var isContextId = (value) => typeof value === "string" && value.length <= 64 && KEBAB.test(value);
22
+ /** The surface a slot is on when its element doesn't say: the first the registry lists. */
23
+ var defaultSurface = (slotId) => SLOTS.find((s) => s.id === slotId)?.surfaces[0] ?? "home";
24
+ /**
25
+ * The request URL for `query`, with its parameters in one fixed order and empty ones left out,
26
+ * so equal contexts share one cache entry at the edge.
27
+ */
28
+ function adQueryUrl(query) {
29
+ const params = new URLSearchParams({
30
+ slot: query.slot,
31
+ page: query.page
32
+ });
33
+ if (query.game !== null) params.set("game", query.game);
34
+ else if (query.category !== null) params.set("category", query.category);
35
+ return `${AD_SLOT_PATH}?${params.toString()}`;
36
+ }
37
+ /** A path on this site: starts with one `/`, never `//` or a backslash (no other origin). */
38
+ var sitePath = z.string().check(z.refine((value) => value.startsWith("/") && !value.startsWith("//") && !value.includes("\\") && value.length <= 512, "Expected a path on this site"));
39
+ var text = (max) => z.string().check(z.minLength(1), z.maxLength(max));
40
+ var dimension = z.int().check(z.positive(), z.lte(4096));
41
+ /** A booked creative: an image, its alt text and a headline, linked through the click notice. */
42
+ var CreativeFillSchema = z.object({
43
+ kind: z.literal("creative"),
44
+ id: z.int().check(z.positive()),
45
+ image: z.object({
46
+ src: sitePath,
47
+ width: dimension,
48
+ height: dimension
49
+ }),
50
+ alt: text(150),
51
+ headline: text(90),
52
+ /** The click notice on this site; the advertiser's link never reaches the page. */
53
+ href: sitePath,
54
+ /** Where the slot reports that the creative rendered. */
55
+ beacon: sitePath
56
+ });
57
+ /** Our own promotion for an unsold slot: a game from the catalog, or "Advertise with us". */
58
+ var HouseFillSchema = z.object({
59
+ kind: z.literal("house"),
60
+ headline: text(90),
61
+ text: text(200),
62
+ /** The call to action, the link's visible words. */
63
+ cta: text(40),
64
+ href: sitePath
65
+ });
66
+ /** Nothing to show: the slot collapses. */
67
+ var CollapseFillSchema = z.object({ kind: z.literal("collapse") });
68
+ var AdFillSchema = z.discriminatedUnion("kind", [
69
+ CreativeFillSchema,
70
+ HouseFillSchema,
71
+ CollapseFillSchema
72
+ ]);
73
+ /** Parses an endpoint answer; null for anything that doesn't fit, which collapses the slot. */
74
+ var parseAdFill = (value) => {
75
+ const parsed = AdFillSchema.safeParse(value);
76
+ return parsed.success ? parsed.data : null;
77
+ };
78
+ var contextId = z.string().check(z.refine(isContextId, "Expected a kebab-case id"));
79
+ /** The endpoint's query string, as an object of its parameters. */
80
+ var AdQuerySchema = z.object({
81
+ slot: z.string().check(z.refine(isSlotId, "Expected a slot id from packages/ui/src/slots.ts")),
82
+ page: z.enum(SLOT_SURFACES),
83
+ game: z.optional(contextId),
84
+ category: z.optional(contextId)
85
+ }).check(z.refine((q) => SLOTS.some((s) => s.id === q.slot && s.surfaces.includes(q.page)), "The slot never appears on that kind of page"));
86
+ /** Parses the query string the endpoint receives. Null when any part is missing or invalid. */
87
+ function parseAdQuery(params) {
88
+ const parsed = AdQuerySchema.safeParse(Object.fromEntries(params));
89
+ if (!parsed.success) return null;
90
+ const { slot, page, game, category } = parsed.data;
91
+ return {
92
+ slot,
93
+ page,
94
+ game: game ?? null,
95
+ category: game === void 0 ? category ?? null : null
96
+ };
97
+ }
98
+ //#endregion
99
+ export { AD_CLICK_PREFIX, AD_CREATIVE_PREFIX, AD_IMPRESSION_PATH, AD_SLOT_PATH, AdFillSchema, AdQuerySchema, CollapseFillSchema, CreativeFillSchema, HouseFillSchema, SLOT_SURFACES, adQueryUrl, defaultSurface, isContextId, parseAdFill, parseAdQuery };
100
+
101
+ //# sourceMappingURL=ad-wire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ad-wire.js","names":[],"sources":["../src/ad-wire.ts"],"sourcesContent":["// What `<ad-slot>` asks the site's ad endpoint, and what it gets back (monetization.md,\n// \"Serving\"). Both sides import this file: the site builds its answers to fit it, and the\n// browser parses every answer through it before rendering anything (invariant 2). Browser\n// code imports zod/mini (D-22).\n//\n// A request carries only the context of the page: the slot, the kind of page, and the game\n// or category the page is about. Never a user, a cookie or a device: every visitor on the\n// same page and date gets the same answer, so it can be cached at the edge.\nimport * as z from 'zod/mini';\nimport { SLOTS, isSlotId, type SlotId, type SlotSurface } from './slots.ts';\n\n/** Where `<ad-slot>` asks which creative to show. */\nexport const AD_SLOT_PATH = '/api/ads/slot';\n/** Where a rendered creative reports its impression (a beacon, POST with no body). */\nexport const AD_IMPRESSION_PATH = '/api/ads/impression';\n/** The first-party click notice (\"You're leaving Joystyk\"): `/ads/click/<creative id>?slot=<slot id>`. */\nexport const AD_CLICK_PREFIX = '/ads/click/';\n/** Creative images, at content-hashed, immutable URLs: `/ads/creative/<sha256>.<ext>`. */\nexport const AD_CREATIVE_PREFIX = '/ads/creative/';\n\nexport const SLOT_SURFACES = ['home', 'category', 'game', 'results', 'daily'] as const satisfies readonly SlotSurface[];\n\nconst KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n/** A game or category id: kebab-case, as the catalog writes them. */\nexport const isContextId = (value: unknown): value is string =>\n typeof value === 'string' && value.length <= 64 && KEBAB.test(value);\n\n/** The context a slot sends: what the page is about, never who is looking at it. */\nexport interface AdQuery {\n readonly slot: SlotId;\n readonly page: SlotSurface;\n /** The game the page or screen is about, if any. The server derives its category. */\n readonly game: string | null;\n /** The category page's category, if any (ignored when `game` is set). */\n readonly category: string | null;\n}\n\n/** The surface a slot is on when its element doesn't say: the first the registry lists. */\nexport const defaultSurface = (slotId: SlotId): SlotSurface =>\n SLOTS.find((s) => s.id === slotId)?.surfaces[0] ?? 'home';\n\n/**\n * The request URL for `query`, with its parameters in one fixed order and empty ones left out,\n * so equal contexts share one cache entry at the edge.\n */\nexport function adQueryUrl(query: AdQuery): string {\n const params = new URLSearchParams({ slot: query.slot, page: query.page });\n if (query.game !== null) params.set('game', query.game);\n else if (query.category !== null) params.set('category', query.category);\n return `${AD_SLOT_PATH}?${params.toString()}`;\n}\n\n/** A path on this site: starts with one `/`, never `//` or a backslash (no other origin). */\nconst sitePath = z\n .string()\n .check(\n z.refine(\n (value) => value.startsWith('/') && !value.startsWith('//') && !value.includes('\\\\') && value.length <= 512,\n 'Expected a path on this site',\n ),\n );\nconst text = (max: number) => z.string().check(z.minLength(1), z.maxLength(max));\nconst dimension = z.int().check(z.positive(), z.lte(4096));\n\n/** A booked creative: an image, its alt text and a headline, linked through the click notice. */\nexport const CreativeFillSchema = z.object({\n kind: z.literal('creative'),\n id: z.int().check(z.positive()),\n image: z.object({ src: sitePath, width: dimension, height: dimension }),\n alt: text(150),\n headline: text(90),\n /** The click notice on this site; the advertiser's link never reaches the page. */\n href: sitePath,\n /** Where the slot reports that the creative rendered. */\n beacon: sitePath,\n});\n\n/** Our own promotion for an unsold slot: a game from the catalog, or \"Advertise with us\". */\nexport const HouseFillSchema = z.object({\n kind: z.literal('house'),\n headline: text(90),\n text: text(200),\n /** The call to action, the link's visible words. */\n cta: text(40),\n href: sitePath,\n});\n\n/** Nothing to show: the slot collapses. */\nexport const CollapseFillSchema = z.object({ kind: z.literal('collapse') });\n\nexport const AdFillSchema = z.discriminatedUnion('kind', [CreativeFillSchema, HouseFillSchema, CollapseFillSchema]);\n\nexport type CreativeFill = z.infer<typeof CreativeFillSchema>;\nexport type HouseFill = z.infer<typeof HouseFillSchema>;\nexport type AdFill = z.infer<typeof AdFillSchema>;\n\n/** Parses an endpoint answer; null for anything that doesn't fit, which collapses the slot. */\nexport const parseAdFill = (value: unknown): AdFill | null => {\n const parsed = AdFillSchema.safeParse(value);\n return parsed.success ? parsed.data : null;\n};\n\nconst contextId = z.string().check(z.refine(isContextId, 'Expected a kebab-case id'));\n\n/** The endpoint's query string, as an object of its parameters. */\nexport const AdQuerySchema = z\n .object({\n slot: z.string().check(z.refine(isSlotId, 'Expected a slot id from packages/ui/src/slots.ts')),\n page: z.enum(SLOT_SURFACES),\n game: z.optional(contextId),\n category: z.optional(contextId),\n })\n .check(\n z.refine(\n (q) => SLOTS.some((s) => s.id === q.slot && s.surfaces.includes(q.page)),\n 'The slot never appears on that kind of page',\n ),\n );\n\n/** Parses the query string the endpoint receives. Null when any part is missing or invalid. */\nexport function parseAdQuery(params: URLSearchParams): AdQuery | null {\n const parsed = AdQuerySchema.safeParse(Object.fromEntries(params));\n if (!parsed.success) return null;\n const { slot, page, game, category } = parsed.data;\n return { slot: slot as SlotId, page, game: game ?? null, category: game === undefined ? (category ?? null) : null };\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { type PropsOf } from '@gyral/core';
2
+ /**
3
+ * `<jy-button>`: the frame's button. It wraps a native `<button>`, so it gets keyboard
4
+ * activation, focus and the button role from the platform. The label is the element's
5
+ * content: `<jy-button>Play</jy-button>`.
6
+ *
7
+ * With the `toggle` attribute it is a toggle button: it exposes `aria-pressed`, flips on
8
+ * each activation and starts from the `pressed` attribute.
9
+ *
10
+ * Outputs (listen with `child()` in a Gyral parent, or `OUTPUT_EVENT` elsewhere):
11
+ * - `{ _tag: 'Activated' }` for a plain button;
12
+ * - `{ _tag: 'Toggled', pressed }` for a toggle button, with the new state.
13
+ */
14
+ declare const props: {
15
+ toggle: import("@gyral/core").Prop<boolean>;
16
+ pressed: import("@gyral/core").Prop<boolean>;
17
+ };
18
+ export type ButtonProps = PropsOf<typeof props>;
19
+ export interface ButtonState {
20
+ readonly pressed: boolean;
21
+ }
22
+ export type ButtonMsg = {
23
+ readonly _tag: 'Activate';
24
+ };
25
+ export type ButtonOutput = {
26
+ readonly _tag: 'Activated';
27
+ } | {
28
+ readonly _tag: 'Toggled';
29
+ readonly pressed: boolean;
30
+ };
31
+ export declare const Button: import("@gyral/core").GyralElementClass<ButtonState, ButtonMsg, PropsOf<{
32
+ toggle: import("@gyral/core").Prop<boolean>;
33
+ pressed: import("@gyral/core").Prop<boolean>;
34
+ }>, ButtonOutput, "Activate">;
35
+ declare global {
36
+ interface HTMLElementTagNameMap {
37
+ 'jy-button': InstanceType<typeof Button>;
38
+ }
39
+ }
40
+ export {};
package/dist/button.js ADDED
@@ -0,0 +1,133 @@
1
+ import { compiled } from "@gyral/core/compiled";
2
+ import { css, define, outputs, prop } from "@gyral/core";
3
+ //#region src/button.ts
4
+ var _gyral$t0 = {
5
+ "id": "16pf2gug297",
6
+ "html": "<button type=\"button\" part=\"button\"><slot></slot></button>",
7
+ "parts": [[
8
+ 3,
9
+ [0],
10
+ "aria-pressed"
11
+ ], [
12
+ 3,
13
+ [0],
14
+ "data-intent"
15
+ ]],
16
+ "segments": [
17
+ "<button type=\"button\" part=\"button\"",
18
+ {
19
+ "k": "attr",
20
+ "name": "aria-pressed"
21
+ },
22
+ {
23
+ "k": "attr",
24
+ "name": "data-intent"
25
+ },
26
+ "><slot></slot></button>"
27
+ ]
28
+ };
29
+ /**
30
+ * `<jy-button>`: the frame's button. It wraps a native `<button>`, so it gets keyboard
31
+ * activation, focus and the button role from the platform. The label is the element's
32
+ * content: `<jy-button>Play</jy-button>`.
33
+ *
34
+ * With the `toggle` attribute it is a toggle button: it exposes `aria-pressed`, flips on
35
+ * each activation and starts from the `pressed` attribute.
36
+ *
37
+ * Outputs (listen with `child()` in a Gyral parent, or `OUTPUT_EVENT` elsewhere):
38
+ * - `{ _tag: 'Activated' }` for a plain button;
39
+ * - `{ _tag: 'Toggled', pressed }` for a toggle button, with the new state.
40
+ */
41
+ var props = {
42
+ toggle: prop.boolean(),
43
+ pressed: prop.boolean()
44
+ };
45
+ var emit = outputs();
46
+ var styles = css`
47
+ @layer components {
48
+ :host {
49
+ display: inline-block;
50
+ }
51
+
52
+ button {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: var(--jy-space-2xs, 0.5rem);
57
+ min-block-size: var(--jy-target-min, 2.75rem);
58
+ min-inline-size: var(--jy-target-min, 2.75rem);
59
+ padding-block: var(--jy-space-2xs, 0.5rem);
60
+ padding-inline: var(--jy-space-s, 1rem);
61
+ border: 1px solid transparent;
62
+ border-radius: var(--jy-radius-pill, 999px);
63
+ background: var(--jy-accent, oklch(50% 0.2 285));
64
+ color: var(--jy-on-accent, oklch(99% 0.005 285));
65
+ font: inherit;
66
+ font-weight: 600;
67
+ line-height: 1.2;
68
+ cursor: pointer;
69
+ touch-action: manipulation;
70
+ transition:
71
+ background-color var(--jy-duration-fast, 120ms) var(--jy-ease-out, ease-out),
72
+ scale var(--jy-duration-fast, 120ms) var(--jy-ease-out, ease-out);
73
+
74
+ &:hover {
75
+ background: var(--jy-accent-hover, oklch(44% 0.2 285));
76
+ }
77
+
78
+ &:active {
79
+ scale: 0.97;
80
+ }
81
+
82
+ &:focus-visible {
83
+ outline: 2px solid var(--jy-focus-ring, oklch(55% 0.22 285));
84
+ outline-offset: 2px;
85
+ }
86
+
87
+ &[aria-pressed='false'] {
88
+ border-color: var(--jy-border, currentColor);
89
+ background: var(--jy-surface-raised, Canvas);
90
+ color: var(--jy-text, CanvasText);
91
+ }
92
+ }
93
+
94
+ @media (prefers-reduced-motion: reduce) {
95
+ button:active {
96
+ scale: none;
97
+ }
98
+ }
99
+
100
+ @media (forced-colors: active) {
101
+ button {
102
+ border-color: ButtonText;
103
+ }
104
+
105
+ button[aria-pressed='true'] {
106
+ background: Highlight;
107
+ color: HighlightText;
108
+ }
109
+ }
110
+ }
111
+ `;
112
+ var Button = define()("jy-button", {
113
+ props,
114
+ init: (p) => ({ pressed: p.pressed }),
115
+ intent: { Activate: () => ({ _tag: "Activate" }) },
116
+ update: {
117
+ Activate: (s, _m, { props: p }) => {
118
+ if (!p.toggle) return [s, [emit({ _tag: "Activated" })]];
119
+ const pressed = !s.pressed;
120
+ return [{ pressed }, [emit({
121
+ _tag: "Toggled",
122
+ pressed
123
+ })]];
124
+ },
125
+ PropsChanged: (s, { props: p, prev }) => p.pressed === prev.pressed ? s : { pressed: p.pressed }
126
+ },
127
+ view: (s, i, { props: p }) => compiled(_gyral$t0, [p.toggle ? String(s.pressed) : null, i.Activate]),
128
+ styles
129
+ });
130
+ //#endregion
131
+ export { Button };
132
+
133
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.js","names":["_gyral$compiled"],"sources":["../src/button.ts"],"sourcesContent":["import { css, define, html, outputs, prop, type PropsOf } from '@gyral/core';\n\n/**\n * `<jy-button>`: the frame's button. It wraps a native `<button>`, so it gets keyboard\n * activation, focus and the button role from the platform. The label is the element's\n * content: `<jy-button>Play</jy-button>`.\n *\n * With the `toggle` attribute it is a toggle button: it exposes `aria-pressed`, flips on\n * each activation and starts from the `pressed` attribute.\n *\n * Outputs (listen with `child()` in a Gyral parent, or `OUTPUT_EVENT` elsewhere):\n * - `{ _tag: 'Activated' }` for a plain button;\n * - `{ _tag: 'Toggled', pressed }` for a toggle button, with the new state.\n */\nconst props = {\n toggle: prop.boolean(),\n pressed: prop.boolean(),\n};\nexport type ButtonProps = PropsOf<typeof props>;\n\nexport interface ButtonState {\n readonly pressed: boolean;\n}\n\nexport type ButtonMsg = { readonly _tag: 'Activate' };\n\nexport type ButtonOutput = { readonly _tag: 'Activated' } | { readonly _tag: 'Toggled'; readonly pressed: boolean };\n\nconst emit = outputs<ButtonOutput>();\n\nconst styles = css`\n @layer components {\n :host {\n display: inline-block;\n }\n\n button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--jy-space-2xs, 0.5rem);\n min-block-size: var(--jy-target-min, 2.75rem);\n min-inline-size: var(--jy-target-min, 2.75rem);\n padding-block: var(--jy-space-2xs, 0.5rem);\n padding-inline: var(--jy-space-s, 1rem);\n border: 1px solid transparent;\n border-radius: var(--jy-radius-pill, 999px);\n background: var(--jy-accent, oklch(50% 0.2 285));\n color: var(--jy-on-accent, oklch(99% 0.005 285));\n font: inherit;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n touch-action: manipulation;\n transition:\n background-color var(--jy-duration-fast, 120ms) var(--jy-ease-out, ease-out),\n scale var(--jy-duration-fast, 120ms) var(--jy-ease-out, ease-out);\n\n &:hover {\n background: var(--jy-accent-hover, oklch(44% 0.2 285));\n }\n\n &:active {\n scale: 0.97;\n }\n\n &:focus-visible {\n outline: 2px solid var(--jy-focus-ring, oklch(55% 0.22 285));\n outline-offset: 2px;\n }\n\n &[aria-pressed='false'] {\n border-color: var(--jy-border, currentColor);\n background: var(--jy-surface-raised, Canvas);\n color: var(--jy-text, CanvasText);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n button:active {\n scale: none;\n }\n }\n\n @media (forced-colors: active) {\n button {\n border-color: ButtonText;\n }\n\n button[aria-pressed='true'] {\n background: Highlight;\n color: HighlightText;\n }\n }\n }\n`;\n\nexport const Button = define<ButtonState, ButtonMsg, ButtonProps, ButtonOutput>()('jy-button', {\n props,\n init: (p) => ({ pressed: p.pressed }),\n intent: {\n Activate: () => ({ _tag: 'Activate' }),\n },\n update: {\n Activate: (s, _m, { props: p }) => {\n if (!p.toggle) return [s, [emit({ _tag: 'Activated' })]];\n const pressed = !s.pressed;\n return [{ pressed }, [emit({ _tag: 'Toggled', pressed })]];\n },\n // A parent that changes `pressed` takes control of the toggle state.\n PropsChanged: (s, { props: p, prev }) => (p.pressed === prev.pressed ? s : { pressed: p.pressed }),\n },\n view: (s, i, { props: p }) => html`\n <button\n type=\"button\"\n part=\"button\"\n aria-pressed=${p.toggle ? String(s.pressed) : null}\n data-intent=${i.Activate}\n >\n <slot></slot>\n </button>\n `,\n styles,\n});\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'jy-button': InstanceType<typeof Button>;\n }\n}\n"],"mappings":";;;AAAA,IAAA,YAAA;CAAA,MAAA;CAAA,QAAA;CAAA,SAAA,CAAA;EAAA;EAAA,CAAA,CAAA;EAAA;CAAA,GAAA;EAAA;EAAA,CAAA,CAAA;EAAA;CAAA,CAAA;CAAA,YAAA;EAAA;EAAA;GAAA,KAAA;GAAA,QAAA;EAAA;EAAA;GAAA,KAAA;GAAA,QAAA;EAAA;EAAA;CAAA;AAAA;;;;;;;;;;;;;AAcA,IAAA,QAAA;CACA,QAAA,KAAA,QAAA;CACA,SAAA,KAAA,QAAA;AACA;AAWA,IAAA,OAAA,QAAA;AAEA,IAAA,SAAA,GAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAA,SAAA,OAAA,CAAA,CAAA,aAAA;CACA;CACA,OAAA,OAAA,EAAA,SAAA,EAAA,QAAA;CACA,QAAA,EACA,iBAAA,EAAA,MAAA,WAAA,GACA;CACA,QAAA;EACA,WAAA,GAAA,IAAA,EAAA,OAAA,QAAA;GACA,IAAA,CAAA,EAAA,QAAA,OAAA,CAAA,GAAA,CAAA,KAAA,EAAA,MAAA,YAAA,CAAA,CAAA,CAAA;GACA,MAAA,UAAA,CAAA,EAAA;GACA,OAAA,CAAA,EAAA,QAAA,GAAA,CAAA,KAAA;IAAA,MAAA;IAAA;GAAA,CAAA,CAAA,CAAA;EACA;EAEA,eAAA,GAAA,EAAA,OAAA,GAAA,WAAA,EAAA,YAAA,KAAA,UAAA,IAAA,EAAA,SAAA,EAAA,QAAA;CACA;CACA,OAAA,GAAA,GAAA,EAAA,OAAA,QAAgCA,SAAA,WAAA,CAAA,EAIX,SAAA,OAAA,EAAA,OAAA,IAAA,MAAmC,EACpC,QAAU,CAAA;CAK9B;AACA,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { Button, type ButtonMsg, type ButtonOutput, type ButtonProps, type ButtonState } from './button.js';
2
+ export { AdSlot, adBeacon, adEntitlement, adServer, alwaysShowAds, connectivity, provideAdEntitlement, queryOf, requestFill, slotPresentation, type AdEntitlement, type AdEntitlementSource, type AdLoad, type AdSlotMsg, type AdSlotProps, type AdSlotState, } from './ad-slot.js';
3
+ export { SLOTS, SLOT_IDS, isSlotId, slot, slotBoxStyles, type SlotDefinition, type SlotId, type SlotSize, type SlotSurface, type TargetingKey, } from './slots.js';
4
+ export { AD_CLICK_PREFIX, AD_CREATIVE_PREFIX, AD_IMPRESSION_PATH, AD_SLOT_PATH, AdFillSchema, AdQuerySchema, adQueryUrl, defaultSurface, isContextId, parseAdFill, parseAdQuery, SLOT_SURFACES, type AdFill, type AdQuery, type CreativeFill, type HouseFill, } from './ad-wire.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { SLOTS, SLOT_IDS, isSlotId, slot, slotBoxStyles } from "./slots.js";
2
+ import { AD_CLICK_PREFIX, AD_CREATIVE_PREFIX, AD_IMPRESSION_PATH, AD_SLOT_PATH, AdFillSchema, AdQuerySchema, SLOT_SURFACES, adQueryUrl, defaultSurface, isContextId, parseAdFill, parseAdQuery } from "./ad-wire.js";
3
+ import { AdSlot, adBeacon, adEntitlement, adServer, alwaysShowAds, connectivity, provideAdEntitlement, queryOf, requestFill, slotPresentation } from "./ad-slot.js";
4
+ import { Button } from "./button.js";
5
+ export { AD_CLICK_PREFIX, AD_CREATIVE_PREFIX, AD_IMPRESSION_PATH, AD_SLOT_PATH, AdFillSchema, AdQuerySchema, AdSlot, Button, SLOTS, SLOT_IDS, SLOT_SURFACES, adBeacon, adEntitlement, adQueryUrl, adServer, alwaysShowAds, connectivity, defaultSurface, isContextId, isSlotId, parseAdFill, parseAdQuery, provideAdEntitlement, queryOf, requestFill, slot, slotBoxStyles, slotPresentation };
@@ -0,0 +1,38 @@
1
+ export type SlotId = 'index-hero' | 'index-inline' | 'category-rail' | 'game-page' | 'results' | 'daily-presenting';
2
+ /** Every slot id, in registry order. */
3
+ export declare const SLOT_IDS: readonly SlotId[];
4
+ export declare const isSlotId: (value: unknown) => value is SlotId;
5
+ /** Pages and screens a slot can appear on. `results` and `daily` belong to the launcher. */
6
+ export type SlotSurface = 'home' | 'category' | 'game' | 'results' | 'daily';
7
+ export type TargetingKey = 'page' | 'game' | 'category' | 'date';
8
+ /** A creative's box, in CSS pixels. */
9
+ export interface SlotSize {
10
+ readonly width: number;
11
+ readonly height: number;
12
+ }
13
+ export interface SlotDefinition {
14
+ readonly id: SlotId;
15
+ /** Where the slot sits, as the monetization design describes it. */
16
+ readonly placement: string;
17
+ readonly shape: 'wide banner' | 'responsive banner' | 'tall rail' | 'medium rectangle' | 'logo and line';
18
+ readonly surfaces: readonly SlotSurface[];
19
+ /**
20
+ * The box on narrow and wide containers. The slot is wide when the space it sits in is at
21
+ * least as wide as the wide box; null hides the slot at that width.
22
+ */
23
+ readonly sizes: {
24
+ readonly narrow: SlotSize | null;
25
+ readonly wide: SlotSize | null;
26
+ };
27
+ readonly targeting: readonly TargetingKey[];
28
+ /** The visible label, as US FTC and UK ASA rules require. */
29
+ readonly label: 'Advertisement' | 'Sponsored';
30
+ }
31
+ export declare const SLOTS: readonly SlotDefinition[];
32
+ export declare function slot(id: SlotId): SlotDefinition;
33
+ /**
34
+ * The CSS that gives every slot its reserved box, generated from the registry so the sizes
35
+ * have one source. `<ad-slot>` is its own size container: a slot takes its wide box when its
36
+ * container is at least as wide as that box, and its narrow box (or nothing) otherwise.
37
+ */
38
+ export declare function slotBoxStyles(slots?: readonly SlotDefinition[]): string;
package/dist/slots.js ADDED
@@ -0,0 +1,154 @@
1
+ //#region src/slots.ts
2
+ /** Every slot id, in registry order. */
3
+ var SLOT_IDS = [
4
+ "index-hero",
5
+ "index-inline",
6
+ "category-rail",
7
+ "game-page",
8
+ "results",
9
+ "daily-presenting"
10
+ ];
11
+ var isSlotId = (value) => typeof value === "string" && SLOT_IDS.includes(value);
12
+ var SLOTS = [
13
+ {
14
+ id: "index-hero",
15
+ placement: "Top of the home page, under the featured games. One sponsor per day or week.",
16
+ shape: "wide banner",
17
+ surfaces: ["home"],
18
+ sizes: {
19
+ narrow: {
20
+ width: 320,
21
+ height: 100
22
+ },
23
+ wide: {
24
+ width: 728,
25
+ height: 90
26
+ }
27
+ },
28
+ targeting: ["page", "date"],
29
+ label: "Advertisement"
30
+ },
31
+ {
32
+ id: "index-inline",
33
+ placement: "Between rows of the game grid; several per page, rotating creatives.",
34
+ shape: "responsive banner",
35
+ surfaces: ["home"],
36
+ sizes: {
37
+ narrow: {
38
+ width: 320,
39
+ height: 100
40
+ },
41
+ wide: {
42
+ width: 728,
43
+ height: 90
44
+ }
45
+ },
46
+ targeting: [
47
+ "page",
48
+ "category",
49
+ "date"
50
+ ],
51
+ label: "Advertisement"
52
+ },
53
+ {
54
+ id: "category-rail",
55
+ placement: "Sidebar on the index and category pages; hidden on narrow screens.",
56
+ shape: "tall rail",
57
+ surfaces: ["home", "category"],
58
+ sizes: {
59
+ narrow: null,
60
+ wide: {
61
+ width: 160,
62
+ height: 600
63
+ }
64
+ },
65
+ targeting: [
66
+ "page",
67
+ "category",
68
+ "date"
69
+ ],
70
+ label: "Advertisement"
71
+ },
72
+ {
73
+ id: "game-page",
74
+ placement: "Game page, under the launcher and above the rules.",
75
+ shape: "medium rectangle",
76
+ surfaces: ["game"],
77
+ sizes: {
78
+ narrow: {
79
+ width: 300,
80
+ height: 250
81
+ },
82
+ wide: {
83
+ width: 300,
84
+ height: 250
85
+ }
86
+ },
87
+ targeting: [
88
+ "page",
89
+ "game",
90
+ "category",
91
+ "date"
92
+ ],
93
+ label: "Advertisement"
94
+ },
95
+ {
96
+ id: "results",
97
+ placement: "The game-over screen, beside \"play again\". Never an interstitial.",
98
+ shape: "medium rectangle",
99
+ surfaces: ["results"],
100
+ sizes: {
101
+ narrow: {
102
+ width: 300,
103
+ height: 250
104
+ },
105
+ wide: {
106
+ width: 300,
107
+ height: 250
108
+ }
109
+ },
110
+ targeting: [
111
+ "game",
112
+ "category",
113
+ "date"
114
+ ],
115
+ label: "Advertisement"
116
+ },
117
+ {
118
+ id: "daily-presenting",
119
+ placement: "Daily games: a presenting sponsor, sold per day.",
120
+ shape: "logo and line",
121
+ surfaces: ["daily"],
122
+ sizes: {
123
+ narrow: {
124
+ width: 320,
125
+ height: 50
126
+ },
127
+ wide: {
128
+ width: 468,
129
+ height: 60
130
+ }
131
+ },
132
+ targeting: ["game", "date"],
133
+ label: "Sponsored"
134
+ }
135
+ ];
136
+ var byId = new Map(SLOTS.map((s) => [s.id, s]));
137
+ function slot(id) {
138
+ const found = byId.get(id);
139
+ if (found === void 0) throw new Error(`Unknown slot "${id}"; slots are listed in packages/ui/src/slots.ts.`);
140
+ return found;
141
+ }
142
+ var box = (size) => size === null ? "display: none;" : `display: grid; inline-size: min(100%, ${String(size.width)}px); aspect-ratio: ${String(size.width)} / ${String(size.height)};`;
143
+ /**
144
+ * The CSS that gives every slot its reserved box, generated from the registry so the sizes
145
+ * have one source. `<ad-slot>` is its own size container: a slot takes its wide box when its
146
+ * container is at least as wide as that box, and its narrow box (or nothing) otherwise.
147
+ */
148
+ function slotBoxStyles(slots = SLOTS) {
149
+ return `@layer components {\n${slots.map((s) => ` .box[data-slot='${s.id}'] { ${box(s.sizes.narrow)} }`).join("\n")}\n${slots.filter((s) => s.sizes.wide !== null).map((s) => ` @container (inline-size >= ${String(s.sizes.wide?.width)}px) {\n .box[data-slot='${s.id}'] { ${box(s.sizes.wide)} }\n }`).join("\n")}\n}\n`;
150
+ }
151
+ //#endregion
152
+ export { SLOTS, SLOT_IDS, isSlotId, slot, slotBoxStyles };
153
+
154
+ //# sourceMappingURL=slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slots.js","names":[],"sources":["../src/slots.ts"],"sourcesContent":["// The billboard slot registry (docs/design-docs/monetization.md, \"v1 slots\"): the one source\n// of truth for slot ids, where each slot may appear, its size per breakpoint and the keys a\n// campaign can target. `<ad-slot>` reserves each slot's exact box from it, so a creative\n// arriving later never shifts the layout. It lives in `ui`, beside `<ad-slot>`, because the\n// catalog's pages and the launcher's results screen both render slots and only `ui` is\n// below both; `@joystyk/catalog` re-exports it.\n\nexport type SlotId = 'index-hero' | 'index-inline' | 'category-rail' | 'game-page' | 'results' | 'daily-presenting';\n\n/** Every slot id, in registry order. */\nexport const SLOT_IDS: readonly SlotId[] = [\n 'index-hero',\n 'index-inline',\n 'category-rail',\n 'game-page',\n 'results',\n 'daily-presenting',\n];\n\nexport const isSlotId = (value: unknown): value is SlotId =>\n typeof value === 'string' && (SLOT_IDS as readonly string[]).includes(value);\n\n/** Pages and screens a slot can appear on. `results` and `daily` belong to the launcher. */\nexport type SlotSurface = 'home' | 'category' | 'game' | 'results' | 'daily';\n\nexport type TargetingKey = 'page' | 'game' | 'category' | 'date';\n\n/** A creative's box, in CSS pixels. */\nexport interface SlotSize {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface SlotDefinition {\n readonly id: SlotId;\n /** Where the slot sits, as the monetization design describes it. */\n readonly placement: string;\n readonly shape: 'wide banner' | 'responsive banner' | 'tall rail' | 'medium rectangle' | 'logo and line';\n readonly surfaces: readonly SlotSurface[];\n /**\n * The box on narrow and wide containers. The slot is wide when the space it sits in is at\n * least as wide as the wide box; null hides the slot at that width.\n */\n readonly sizes: { readonly narrow: SlotSize | null; readonly wide: SlotSize | null };\n readonly targeting: readonly TargetingKey[];\n /** The visible label, as US FTC and UK ASA rules require. */\n readonly label: 'Advertisement' | 'Sponsored';\n}\n\nexport const SLOTS: readonly SlotDefinition[] = [\n {\n id: 'index-hero',\n placement: 'Top of the home page, under the featured games. One sponsor per day or week.',\n shape: 'wide banner',\n surfaces: ['home'],\n sizes: { narrow: { width: 320, height: 100 }, wide: { width: 728, height: 90 } },\n targeting: ['page', 'date'],\n label: 'Advertisement',\n },\n {\n id: 'index-inline',\n placement: 'Between rows of the game grid; several per page, rotating creatives.',\n shape: 'responsive banner',\n surfaces: ['home'],\n sizes: { narrow: { width: 320, height: 100 }, wide: { width: 728, height: 90 } },\n targeting: ['page', 'category', 'date'],\n label: 'Advertisement',\n },\n {\n id: 'category-rail',\n placement: 'Sidebar on the index and category pages; hidden on narrow screens.',\n shape: 'tall rail',\n surfaces: ['home', 'category'],\n sizes: { narrow: null, wide: { width: 160, height: 600 } },\n targeting: ['page', 'category', 'date'],\n label: 'Advertisement',\n },\n {\n id: 'game-page',\n placement: 'Game page, under the launcher and above the rules.',\n shape: 'medium rectangle',\n surfaces: ['game'],\n sizes: { narrow: { width: 300, height: 250 }, wide: { width: 300, height: 250 } },\n targeting: ['page', 'game', 'category', 'date'],\n label: 'Advertisement',\n },\n {\n id: 'results',\n placement: 'The game-over screen, beside \"play again\". Never an interstitial.',\n shape: 'medium rectangle',\n surfaces: ['results'],\n sizes: { narrow: { width: 300, height: 250 }, wide: { width: 300, height: 250 } },\n targeting: ['game', 'category', 'date'],\n label: 'Advertisement',\n },\n {\n id: 'daily-presenting',\n placement: 'Daily games: a presenting sponsor, sold per day.',\n shape: 'logo and line',\n surfaces: ['daily'],\n sizes: { narrow: { width: 320, height: 50 }, wide: { width: 468, height: 60 } },\n targeting: ['game', 'date'],\n label: 'Sponsored',\n },\n];\n\nconst byId = new Map(SLOTS.map((s) => [s.id, s]));\n\nexport function slot(id: SlotId): SlotDefinition {\n const found = byId.get(id);\n if (found === undefined) throw new Error(`Unknown slot \"${id}\"; slots are listed in packages/ui/src/slots.ts.`);\n return found;\n}\n\nconst box = (size: SlotSize | null): string =>\n size === null\n ? 'display: none;'\n : `display: grid; inline-size: min(100%, ${String(size.width)}px); aspect-ratio: ${String(size.width)} / ${String(size.height)};`;\n\n/**\n * The CSS that gives every slot its reserved box, generated from the registry so the sizes\n * have one source. `<ad-slot>` is its own size container: a slot takes its wide box when its\n * container is at least as wide as that box, and its narrow box (or nothing) otherwise.\n */\nexport function slotBoxStyles(slots: readonly SlotDefinition[] = SLOTS): string {\n const narrow = slots.map((s) => ` .box[data-slot='${s.id}'] { ${box(s.sizes.narrow)} }`).join('\\n');\n const wide = slots\n .filter((s) => s.sizes.wide !== null)\n .map(\n (s) =>\n ` @container (inline-size >= ${String(s.sizes.wide?.width)}px) {\\n .box[data-slot='${s.id}'] { ${box(s.sizes.wide)} }\\n }`,\n )\n .join('\\n');\n return `@layer components {\\n${narrow}\\n${wide}\\n}\\n`;\n}\n"],"mappings":";;AAUA,IAAa,WAA8B;CACzC;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAa,YAAY,UACvB,OAAO,UAAU,YAAa,SAA+B,SAAS,KAAK;AA6B7E,IAAa,QAAmC;CAC9C;EACE,IAAI;EACJ,WAAW;EACX,OAAO;EACP,UAAU,CAAC,MAAM;EACjB,OAAO;GAAE,QAAQ;IAAE,OAAO;IAAK,QAAQ;GAAI;GAAG,MAAM;IAAE,OAAO;IAAK,QAAQ;GAAG;EAAE;EAC/E,WAAW,CAAC,QAAQ,MAAM;EAC1B,OAAO;CACT;CACA;EACE,IAAI;EACJ,WAAW;EACX,OAAO;EACP,UAAU,CAAC,MAAM;EACjB,OAAO;GAAE,QAAQ;IAAE,OAAO;IAAK,QAAQ;GAAI;GAAG,MAAM;IAAE,OAAO;IAAK,QAAQ;GAAG;EAAE;EAC/E,WAAW;GAAC;GAAQ;GAAY;EAAM;EACtC,OAAO;CACT;CACA;EACE,IAAI;EACJ,WAAW;EACX,OAAO;EACP,UAAU,CAAC,QAAQ,UAAU;EAC7B,OAAO;GAAE,QAAQ;GAAM,MAAM;IAAE,OAAO;IAAK,QAAQ;GAAI;EAAE;EACzD,WAAW;GAAC;GAAQ;GAAY;EAAM;EACtC,OAAO;CACT;CACA;EACE,IAAI;EACJ,WAAW;EACX,OAAO;EACP,UAAU,CAAC,MAAM;EACjB,OAAO;GAAE,QAAQ;IAAE,OAAO;IAAK,QAAQ;GAAI;GAAG,MAAM;IAAE,OAAO;IAAK,QAAQ;GAAI;EAAE;EAChF,WAAW;GAAC;GAAQ;GAAQ;GAAY;EAAM;EAC9C,OAAO;CACT;CACA;EACE,IAAI;EACJ,WAAW;EACX,OAAO;EACP,UAAU,CAAC,SAAS;EACpB,OAAO;GAAE,QAAQ;IAAE,OAAO;IAAK,QAAQ;GAAI;GAAG,MAAM;IAAE,OAAO;IAAK,QAAQ;GAAI;EAAE;EAChF,WAAW;GAAC;GAAQ;GAAY;EAAM;EACtC,OAAO;CACT;CACA;EACE,IAAI;EACJ,WAAW;EACX,OAAO;EACP,UAAU,CAAC,OAAO;EAClB,OAAO;GAAE,QAAQ;IAAE,OAAO;IAAK,QAAQ;GAAG;GAAG,MAAM;IAAE,OAAO;IAAK,QAAQ;GAAG;EAAE;EAC9E,WAAW,CAAC,QAAQ,MAAM;EAC1B,OAAO;CACT;AACF;AAEA,IAAM,OAAO,IAAI,IAAI,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAEhD,SAAgB,KAAK,IAA4B;CAC/C,MAAM,QAAQ,KAAK,IAAI,EAAE;CACzB,IAAI,UAAU,KAAA,GAAW,MAAM,IAAI,MAAM,iBAAiB,GAAG,iDAAiD;CAC9G,OAAO;AACT;AAEA,IAAM,OAAO,SACX,SAAS,OACL,mBACA,yCAAyC,OAAO,KAAK,KAAK,EAAE,qBAAqB,OAAO,KAAK,KAAK,EAAE,KAAK,OAAO,KAAK,MAAM,EAAE;;;;;;AAOnI,SAAgB,cAAc,QAAmC,OAAe;CAS9E,OAAO,wBARQ,MAAM,KAAK,MAAM,qBAAqB,EAAE,GAAG,OAAO,IAAI,EAAE,MAAM,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,IAQhE,EAAO,IAPzB,MACV,QAAQ,MAAM,EAAE,MAAM,SAAS,IAAI,CAAC,CACpC,KACE,MACC,gCAAgC,OAAO,EAAE,MAAM,MAAM,KAAK,EAAE,6BAA6B,EAAE,GAAG,OAAO,IAAI,EAAE,MAAM,IAAI,EAAE,QAC3H,CAAC,CACA,KAAK,IACkC,EAAK;AACjD"}