@kywi-software/js 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.
Files changed (55) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +75 -0
  3. package/dist/audience/adapter-runner.d.ts +10 -0
  4. package/dist/audience/adapter-runner.d.ts.map +1 -0
  5. package/dist/audience/behavioral-collector.d.ts +15 -0
  6. package/dist/audience/behavioral-collector.d.ts.map +1 -0
  7. package/dist/audience/data-layer.d.ts +11 -0
  8. package/dist/audience/data-layer.d.ts.map +1 -0
  9. package/dist/audience/dom-patcher.d.ts +2 -0
  10. package/dist/audience/dom-patcher.d.ts.map +1 -0
  11. package/dist/audience/evaluate.d.ts +3 -0
  12. package/dist/audience/evaluate.d.ts.map +1 -0
  13. package/dist/audience/index.d.ts +24 -0
  14. package/dist/audience/index.d.ts.map +1 -0
  15. package/dist/audience/preview.d.ts +6 -0
  16. package/dist/audience/preview.d.ts.map +1 -0
  17. package/dist/audience/selfid-collector.d.ts +3 -0
  18. package/dist/audience/selfid-collector.d.ts.map +1 -0
  19. package/dist/audience/selfid-submit.d.ts +3 -0
  20. package/dist/audience/selfid-submit.d.ts.map +1 -0
  21. package/dist/audience/selfid-widget.d.ts +39 -0
  22. package/dist/audience/selfid-widget.d.ts.map +1 -0
  23. package/dist/audience/transparency-styles.d.ts +3 -0
  24. package/dist/audience/transparency-styles.d.ts.map +1 -0
  25. package/dist/audience/transparency.d.ts +12 -0
  26. package/dist/audience/transparency.d.ts.map +1 -0
  27. package/dist/consent.d.ts +24 -0
  28. package/dist/consent.d.ts.map +1 -0
  29. package/dist/context.d.ts +3 -0
  30. package/dist/context.d.ts.map +1 -0
  31. package/dist/cookies.d.ts +13 -0
  32. package/dist/cookies.d.ts.map +1 -0
  33. package/dist/entity.d.ts +3 -0
  34. package/dist/entity.d.ts.map +1 -0
  35. package/dist/experiments/index.d.ts +27 -0
  36. package/dist/experiments/index.d.ts.map +1 -0
  37. package/dist/feed.d.ts +51 -0
  38. package/dist/feed.d.ts.map +1 -0
  39. package/dist/form-hooks.d.ts +3 -0
  40. package/dist/form-hooks.d.ts.map +1 -0
  41. package/dist/index.d.ts +24 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/kywi.esm.js +1314 -0
  44. package/dist/kywi.esm.js.map +7 -0
  45. package/dist/kywi.js +1339 -0
  46. package/dist/kywi.js.map +7 -0
  47. package/dist/module-system.d.ts +2 -0
  48. package/dist/module-system.d.ts.map +1 -0
  49. package/dist/personalization/badge.d.ts +2 -0
  50. package/dist/personalization/badge.d.ts.map +1 -0
  51. package/dist/render-feed.d.ts +17 -0
  52. package/dist/render-feed.d.ts.map +1 -0
  53. package/dist/types.d.ts +44 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/package.json +48 -0
package/dist/kywi.js ADDED
@@ -0,0 +1,1339 @@
1
+ "use strict";
2
+ var Kywi = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ Kywi: () => Kywi,
25
+ bootAudienceEngine: () => bootAudienceEngine,
26
+ bootExperiments: () => bootExperiments
27
+ });
28
+
29
+ // src/context.ts
30
+ function readContext() {
31
+ const meta = (name) => {
32
+ if (typeof document === "undefined") return "";
33
+ const el = document.querySelector(`meta[name="kywi:${name}"]`);
34
+ return el?.getAttribute("content") ?? "";
35
+ };
36
+ return {
37
+ siteId: meta("site-id") || document?.documentElement?.getAttribute("data-kywi-site") || "",
38
+ locale: document?.documentElement?.lang || "en",
39
+ themePath: meta("theme-path") || "",
40
+ basePath: meta("base-path") || "/",
41
+ csrfToken: meta("csrf-token") || "",
42
+ isEditMode: document?.body?.classList?.contains("kywi-edit-mode") ?? false
43
+ };
44
+ }
45
+
46
+ // src/entity.ts
47
+ function createEntity(type, basePath, csrfToken, data) {
48
+ const fields = { ...data };
49
+ const dirty = /* @__PURE__ */ new Map();
50
+ const errors = [];
51
+ const headers = { "Content-Type": "application/json" };
52
+ if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
53
+ const entity = {
54
+ get(field) {
55
+ return fields[field];
56
+ },
57
+ set(field, value) {
58
+ fields[field] = value;
59
+ dirty.set(field, value);
60
+ return entity;
61
+ },
62
+ async save() {
63
+ const id = fields["id"];
64
+ const url = id ? `${basePath}/content/${type}/${id}` : `${basePath}/content/${type}`;
65
+ const res = await fetch(url, { method: id ? "PUT" : "POST", headers, body: JSON.stringify(Object.fromEntries(dirty)) });
66
+ const json = await res.json();
67
+ if (!res.ok || json.error) {
68
+ errors.push(json.error?.message ?? "Save failed");
69
+ throw entity;
70
+ }
71
+ if (json.data) Object.assign(fields, json.data);
72
+ dirty.clear();
73
+ return entity;
74
+ },
75
+ async delete() {
76
+ const id = fields["id"];
77
+ await fetch(`${basePath}/content/${type}/${id}`, { method: "DELETE", headers });
78
+ },
79
+ getErrors() {
80
+ return [...errors];
81
+ },
82
+ async loadBy(field, value) {
83
+ const url = field === "id" ? `${basePath}/content/${type}/${value}` : `${basePath}/content/${type}?${field}=${encodeURIComponent(value)}`;
84
+ const res = await fetch(url, { headers });
85
+ const json = await res.json();
86
+ if (!res.ok || json.error) {
87
+ errors.push(json.error?.message ?? "Load failed");
88
+ throw entity;
89
+ }
90
+ if (json.data) Object.assign(fields, json.data);
91
+ return entity;
92
+ }
93
+ };
94
+ return entity;
95
+ }
96
+ function createEntityFactory(basePath, csrfToken) {
97
+ return (type) => createEntity(type, basePath, csrfToken, {});
98
+ }
99
+
100
+ // src/feed.ts
101
+ var PropBuilder = class {
102
+ constructor(builder, field, conj) {
103
+ this.builder = builder;
104
+ this.field = field;
105
+ this.conj = conj;
106
+ }
107
+ add(op, value) {
108
+ this.builder._filters.push({ field: this.field, operator: op, value, conjunction: this.conj });
109
+ return this.builder;
110
+ }
111
+ isEQ(v) {
112
+ return this.add("eq", v);
113
+ }
114
+ isNEQ(v) {
115
+ return this.add("neq", v);
116
+ }
117
+ isLT(v) {
118
+ return this.add("lt", v);
119
+ }
120
+ isLTE(v) {
121
+ return this.add("lte", v);
122
+ }
123
+ isGT(v) {
124
+ return this.add("gt", v);
125
+ }
126
+ isGTE(v) {
127
+ return this.add("gte", v);
128
+ }
129
+ beginsWith(v) {
130
+ return this.add("beginsWith", v);
131
+ }
132
+ endsWith(v) {
133
+ return this.add("endsWith", v);
134
+ }
135
+ contains(v) {
136
+ return this.add("contains", v);
137
+ }
138
+ isNull() {
139
+ return this.add("isNull");
140
+ }
141
+ isNotNull() {
142
+ return this.add("isNotNull");
143
+ }
144
+ };
145
+ var FeedBuilder = class {
146
+ constructor(contentType, basePath, csrfToken) {
147
+ this.contentType = contentType;
148
+ this.basePath = basePath;
149
+ this.csrfToken = csrfToken;
150
+ }
151
+ _filters = [];
152
+ _sort = null;
153
+ _limit = null;
154
+ _page = null;
155
+ where() {
156
+ return this;
157
+ }
158
+ prop(field) {
159
+ return new PropBuilder(this, field, "and");
160
+ }
161
+ andProp(field) {
162
+ return new PropBuilder(this, field, "and");
163
+ }
164
+ orProp(field) {
165
+ return new PropBuilder(this, field, "or");
166
+ }
167
+ sort(field, direction = "asc") {
168
+ this._sort = { field, direction };
169
+ return this;
170
+ }
171
+ maxItems(n) {
172
+ this._limit = n;
173
+ return this;
174
+ }
175
+ page(n) {
176
+ this._page = n;
177
+ return this;
178
+ }
179
+ buildBody() {
180
+ const filterArray = this._filters.map((f) => ({
181
+ field: f.field,
182
+ operator: f.operator,
183
+ value: f.value,
184
+ conjunction: f.conjunction
185
+ }));
186
+ const body = { contentType: this.contentType };
187
+ if (filterArray.length > 0) body["filters"] = filterArray;
188
+ if (this._sort) body["sort"] = this._sort;
189
+ if (this._limit !== null) body["limit"] = this._limit;
190
+ if (this._page !== null) body["page"] = this._page;
191
+ return body;
192
+ }
193
+ buildHeaders() {
194
+ const headers = { "Content-Type": "application/json" };
195
+ if (this.csrfToken) headers["X-CSRF-Token"] = this.csrfToken;
196
+ return headers;
197
+ }
198
+ async aggregate(fn, field) {
199
+ const body = this.buildBody();
200
+ const agg = { function: fn };
201
+ if (field) agg["field"] = field;
202
+ body["aggregate"] = agg;
203
+ const res = await fetch(`${this.basePath}/feeds/aggregate`, {
204
+ method: "POST",
205
+ headers: this.buildHeaders(),
206
+ body: JSON.stringify(body)
207
+ });
208
+ const json = await res.json();
209
+ return json.data ?? {};
210
+ }
211
+ async getQuery() {
212
+ const body = this.buildBody();
213
+ const res = await fetch(`${this.basePath}/feeds/query`, {
214
+ method: "POST",
215
+ headers: this.buildHeaders(),
216
+ body: JSON.stringify(body)
217
+ });
218
+ const json = await res.json();
219
+ const payload = json.data;
220
+ const rawItems = Array.isArray(payload) ? payload : payload?.items ?? [];
221
+ const total = Array.isArray(payload) ? json.meta?.total ?? rawItems.length : payload?.total ?? rawItems.length;
222
+ const page = Array.isArray(payload) ? json.meta?.page ?? 1 : payload?.page ?? 1;
223
+ const pages = Array.isArray(payload) ? json.meta?.totalPages ?? 0 : payload?.totalPages ?? 0;
224
+ const items = rawItems.map((d) => ({
225
+ get(field) {
226
+ return d[field];
227
+ },
228
+ set() {
229
+ throw new Error("Feed results are read-only");
230
+ },
231
+ save() {
232
+ return Promise.reject(new Error("Feed results are read-only"));
233
+ },
234
+ delete() {
235
+ return Promise.reject(new Error("Feed results are read-only"));
236
+ },
237
+ getErrors() {
238
+ return [];
239
+ },
240
+ loadBy() {
241
+ return Promise.reject(new Error("Use Kywi.getEntity() to load by field"));
242
+ }
243
+ }));
244
+ return { items, total, page, pages };
245
+ }
246
+ };
247
+ function createFeedFactory(basePath, csrfToken) {
248
+ return (type) => new FeedBuilder(type, basePath, csrfToken);
249
+ }
250
+
251
+ // src/render-feed.ts
252
+ async function renderFeed(options) {
253
+ const { feed, container, template, loadingText = "", emptyText = "", wrapperTag = "div", wrapperClass, unsafeHtml = false, onRender } = options;
254
+ if (loadingText) container.textContent = loadingText;
255
+ const result = await feed.getQuery();
256
+ container.textContent = "";
257
+ if (result.items.length === 0) {
258
+ if (emptyText) container.textContent = emptyText;
259
+ return;
260
+ }
261
+ const wrapper = document.createElement(wrapperTag);
262
+ if (wrapperClass) wrapper.className = wrapperClass;
263
+ for (const item of result.items) {
264
+ const rendered = template(item);
265
+ if (rendered instanceof Node) {
266
+ wrapper.appendChild(rendered);
267
+ } else if (unsafeHtml) {
268
+ const span = document.createElement("span");
269
+ span["innerHTML"] = rendered;
270
+ wrapper.appendChild(span);
271
+ } else {
272
+ const span = document.createElement("span");
273
+ span.textContent = rendered;
274
+ wrapper.appendChild(span);
275
+ }
276
+ }
277
+ container.appendChild(wrapper);
278
+ if (onRender) onRender(container);
279
+ }
280
+
281
+ // src/module-system.ts
282
+ function readParams(el) {
283
+ const params = {};
284
+ for (const attr of Array.from(el.attributes)) {
285
+ if (attr.name.startsWith("data-kywi-param-")) {
286
+ params[attr.name.slice(16)] = attr.value;
287
+ }
288
+ }
289
+ return params;
290
+ }
291
+ function initModules() {
292
+ const elements = document.querySelectorAll("[data-kywi-module]");
293
+ for (const el of Array.from(elements)) {
294
+ const htmlEl = el;
295
+ const moduleName = htmlEl.getAttribute("data-kywi-module");
296
+ if (!moduleName) continue;
297
+ const def = Kywi.Module[moduleName];
298
+ if (!def || !def.renderClient) continue;
299
+ const context = { targetEl: htmlEl, ...readParams(htmlEl) };
300
+ def.renderClient.call({ context, ...def });
301
+ }
302
+ }
303
+
304
+ // src/form-hooks.ts
305
+ function initFormHooks() {
306
+ const forms = document.querySelectorAll("[data-kywi-form]");
307
+ for (const formEl of Array.from(forms)) {
308
+ const htmlForm = formEl;
309
+ const hooks = Kywi.DisplayObject.Form._hooks;
310
+ if (hooks["onAfterRender"]) hooks["onAfterRender"].call({ context: { targetEl: htmlForm } });
311
+ htmlForm.addEventListener("submit", (e) => {
312
+ if (hooks["onSubmit"]) {
313
+ const result = hooks["onSubmit"].call({ context: { targetEl: htmlForm } });
314
+ if (result === false) e.preventDefault();
315
+ }
316
+ });
317
+ }
318
+ }
319
+
320
+ // src/experiments/index.ts
321
+ function apiBaseFrom(ctx) {
322
+ return ctx.basePath && ctx.basePath !== "/" ? `${ctx.basePath}/api/v1` : "/api/v1";
323
+ }
324
+ function metaContent(name) {
325
+ if (typeof document === "undefined") return null;
326
+ return document.querySelector(`meta[name="kywi:${name}"]`)?.getAttribute("content") ?? null;
327
+ }
328
+ var submitListenerInstalled = false;
329
+ var currentFormConversion = null;
330
+ function installFormListener() {
331
+ if (submitListenerInstalled || typeof document === "undefined") return;
332
+ submitListenerInstalled = true;
333
+ document.addEventListener(
334
+ "submit",
335
+ (e) => {
336
+ const form = e.target;
337
+ if (!form || form.tagName !== "FORM") return;
338
+ const formId = form.getAttribute("data-kywi-form-id") ?? form.getAttribute("id");
339
+ if (formId && currentFormConversion) currentFormConversion(formId);
340
+ },
341
+ true
342
+ );
343
+ }
344
+ function beacon(url, body, csrfToken) {
345
+ try {
346
+ void fetch(url, {
347
+ method: "POST",
348
+ headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
349
+ body: JSON.stringify(body),
350
+ keepalive: true
351
+ }).catch(() => {
352
+ });
353
+ } catch {
354
+ }
355
+ }
356
+ function bootExperiments(config = {}) {
357
+ const ctx = config.context ?? readContext();
358
+ const apiBase = config.apiBase ?? apiBaseFrom(ctx);
359
+ const csrf = ctx.csrfToken || void 0;
360
+ const experimentId = metaContent("experiment-id");
361
+ const visitorId = config.visitorId ?? metaContent("visitor-id") ?? void 0;
362
+ const currentPath = config.currentPath ?? (typeof location !== "undefined" ? location.pathname : "/");
363
+ function trackConversion(signal) {
364
+ beacon(`${apiBase}/kywi/experiments/conversion`, { visitorId, event: signal }, csrf);
365
+ }
366
+ function track(eventName) {
367
+ trackConversion({ type: "custom", eventName });
368
+ }
369
+ if (experimentId) {
370
+ beacon(`${apiBase}/kywi/experiments/exposure`, { experimentId, visitorId }, csrf);
371
+ }
372
+ if (config.autoPageview !== false) {
373
+ trackConversion({ type: "pageview", url: currentPath });
374
+ }
375
+ currentFormConversion = (formId) => trackConversion({ type: "form_submit", formId });
376
+ installFormListener();
377
+ const runtime = { track, trackConversion };
378
+ const g = globalThis.kywi ?? {};
379
+ g["track"] = track;
380
+ g["trackConversion"] = trackConversion;
381
+ globalThis.kywi = g;
382
+ return runtime;
383
+ }
384
+
385
+ // ../core/dist/audiences/types.js
386
+ function createEmptySignals() {
387
+ return {
388
+ utm: { source: null, medium: null, campaign: null, term: null, content: null },
389
+ referrer: { raw: null, domain: null, sourceType: "direct" },
390
+ session: { pageViewCount: 0, totalPageViews: 0, sessionDuration: 0, entryPage: "/", pagesVisited: [] },
391
+ behavioral: { categoryScores: {}, totalScore: 0 },
392
+ selfId: {},
393
+ identity: { visitorId: "", isKnown: false, maLeadId: null },
394
+ adapters: {},
395
+ meta: { isOptedOut: false, isPreview: false, previewAudienceId: null, resolvedAt: "server" }
396
+ };
397
+ }
398
+
399
+ // ../core/dist/audiences/evaluator/glob.js
400
+ function matchGlob(pattern, value) {
401
+ const parts = pattern.split("*");
402
+ if (parts.length === 1)
403
+ return pattern === value;
404
+ let pos = 0;
405
+ const first = parts[0];
406
+ if (first !== "") {
407
+ if (!value.startsWith(first))
408
+ return false;
409
+ pos = first.length;
410
+ }
411
+ for (let i = 1; i < parts.length - 1; i++) {
412
+ const part = parts[i];
413
+ if (part === "")
414
+ continue;
415
+ const idx = value.indexOf(part, pos);
416
+ if (idx < 0)
417
+ return false;
418
+ pos = idx + part.length;
419
+ }
420
+ const last = parts[parts.length - 1];
421
+ if (last !== "") {
422
+ if (!value.endsWith(last))
423
+ return false;
424
+ if (value.length - last.length < pos)
425
+ return false;
426
+ }
427
+ return true;
428
+ }
429
+
430
+ // ../core/dist/audiences/evaluator/instant-match.js
431
+ function resolveSignalValue(signals, ref) {
432
+ if (ref === "utm.source")
433
+ return signals.utm.source;
434
+ if (ref === "utm.medium")
435
+ return signals.utm.medium;
436
+ if (ref === "utm.campaign")
437
+ return signals.utm.campaign;
438
+ if (ref === "utm.term")
439
+ return signals.utm.term;
440
+ if (ref === "utm.content")
441
+ return signals.utm.content;
442
+ if (ref === "referrer.domain")
443
+ return signals.referrer.domain;
444
+ if (ref === "referrer.sourceType")
445
+ return signals.referrer.sourceType;
446
+ if (ref === "identity.isKnown")
447
+ return signals.identity.isKnown ? "true" : "false";
448
+ if (ref === "session.entryPage")
449
+ return signals.session.entryPage;
450
+ if (ref === "session.pagesVisited")
451
+ return signals.session.pagesVisited.join(",");
452
+ if (ref.startsWith("selfId.")) {
453
+ const key = ref.slice(7);
454
+ return signals.selfId[key] ?? null;
455
+ }
456
+ if (ref.startsWith("adapter.")) {
457
+ const key = ref.slice(8);
458
+ const val = signals.adapters[key];
459
+ return val !== void 0 && val !== null ? String(val) : null;
460
+ }
461
+ return null;
462
+ }
463
+ function evaluateInstantCondition(condition, signals) {
464
+ const actual = resolveSignalValue(signals, condition.signal);
465
+ const expected = condition.value;
466
+ switch (condition.operator) {
467
+ case "equals":
468
+ return actual === expected;
469
+ case "not_equals":
470
+ return actual !== expected;
471
+ case "contains":
472
+ return actual !== null && typeof expected === "string" && actual.includes(expected);
473
+ case "starts_with":
474
+ return actual !== null && typeof expected === "string" && actual.startsWith(expected);
475
+ case "matches_glob":
476
+ return actual !== null && typeof expected === "string" && matchGlob(expected, actual);
477
+ case "is_one_of":
478
+ return actual !== null && Array.isArray(condition.value) && condition.value.includes(actual);
479
+ case "is_true":
480
+ return actual === "true";
481
+ case "is_false":
482
+ return actual === "false";
483
+ case "gte":
484
+ return actual !== null && typeof expected === "string" && Number(actual) >= Number(expected);
485
+ case "lte":
486
+ return actual !== null && typeof expected === "string" && Number(actual) <= Number(expected);
487
+ default:
488
+ return false;
489
+ }
490
+ }
491
+
492
+ // ../core/dist/audiences/evaluator/score.js
493
+ function evaluateOneScore(cond, signals) {
494
+ const threshold = cond.threshold ?? 0;
495
+ switch (cond.trigger) {
496
+ case "pageViewCount":
497
+ return signals.session.pageViewCount >= threshold ? cond.points : 0;
498
+ case "totalPageViews":
499
+ return signals.session.totalPageViews >= threshold ? cond.points : 0;
500
+ case "timeOnSite":
501
+ return signals.session.sessionDuration >= threshold ? cond.points : 0;
502
+ case "categoryViewed": {
503
+ const cat = cond.category ?? "";
504
+ const score = signals.behavioral.categoryScores[cat];
505
+ return score !== void 0 && score >= threshold ? cond.points : 0;
506
+ }
507
+ case "urlMatch": {
508
+ const pattern = cond.pattern ?? "*";
509
+ const matched = signals.session.pagesVisited.some((url) => matchGlob(pattern, url));
510
+ return matched ? cond.points : 0;
511
+ }
512
+ case "signalMatch": {
513
+ if (!cond.signal || !cond.operator)
514
+ return 0;
515
+ const matched = evaluateInstantCondition({
516
+ type: "instant",
517
+ id: cond.id,
518
+ groupId: cond.groupId,
519
+ signal: cond.signal,
520
+ operator: cond.operator,
521
+ value: cond.value ?? ""
522
+ }, signals);
523
+ return matched ? cond.points : 0;
524
+ }
525
+ default:
526
+ return 0;
527
+ }
528
+ }
529
+ function evaluateScoreConditions(conditions, signals) {
530
+ let total = 0;
531
+ for (const cond of conditions) {
532
+ total += evaluateOneScore(cond, signals);
533
+ }
534
+ return total;
535
+ }
536
+
537
+ // ../core/dist/audiences/evaluator/condition-registry.js
538
+ var registry = /* @__PURE__ */ new Map();
539
+ function evaluateCustomCondition(condition, signals) {
540
+ const evaluator = registry.get(condition.type);
541
+ if (!evaluator)
542
+ return false;
543
+ return evaluator(condition, signals);
544
+ }
545
+ function hasConditionType(name) {
546
+ return registry.has(name);
547
+ }
548
+
549
+ // ../core/dist/audiences/evaluator/resolver.js
550
+ function evaluateGroup(group, signals) {
551
+ const conditionTraces = [];
552
+ const instants = [];
553
+ const scores = [];
554
+ const customs = [];
555
+ for (const c of group.conditions) {
556
+ const conditionType = c.type;
557
+ if (c.type === "instant")
558
+ instants.push(c);
559
+ else if (c.type === "score")
560
+ scores.push(c);
561
+ else if (hasConditionType(conditionType))
562
+ customs.push(c);
563
+ }
564
+ let allInstantsPassed = true;
565
+ for (const ic of instants) {
566
+ const passed = evaluateInstantCondition(ic, signals);
567
+ conditionTraces.push({ conditionId: ic.id, type: "instant", passed });
568
+ if (!passed)
569
+ allInstantsPassed = false;
570
+ }
571
+ for (const cc of customs) {
572
+ const passed = evaluateCustomCondition(cc, signals);
573
+ conditionTraces.push({ conditionId: cc.id, type: cc.type, passed });
574
+ if (!passed)
575
+ allInstantsPassed = false;
576
+ }
577
+ const totalScore = evaluateScoreConditions(scores, signals);
578
+ for (const sc of scores) {
579
+ const contributed = evaluateScoreConditions([sc], signals);
580
+ conditionTraces.push({ conditionId: sc.id, type: "score", passed: contributed > 0, scoreContributed: contributed });
581
+ }
582
+ return { matched: allInstantsPassed, conditionTraces, totalScore };
583
+ }
584
+ function resolveAudiences(audiences, signals) {
585
+ const allMatchedIds = [];
586
+ const traces = [];
587
+ for (const audience of audiences) {
588
+ const groupTraces = [];
589
+ let audienceMatched = false;
590
+ let accumulatedScore = 0;
591
+ for (const group of audience.conditionGroups) {
592
+ const { matched, conditionTraces, totalScore } = evaluateGroup(group, signals);
593
+ accumulatedScore += totalScore;
594
+ groupTraces.push({ groupId: group.id, matched, conditionTraces });
595
+ if (matched)
596
+ audienceMatched = true;
597
+ }
598
+ if (audience.scoreThreshold !== null) {
599
+ const scoreThresholdMet = accumulatedScore >= audience.scoreThreshold;
600
+ const hasInstantConditions = audience.conditionGroups.some((g) => g.conditions.some((c) => c.type === "instant"));
601
+ if (hasInstantConditions) {
602
+ audienceMatched = audienceMatched && scoreThresholdMet;
603
+ } else {
604
+ audienceMatched = scoreThresholdMet;
605
+ }
606
+ }
607
+ traces.push({
608
+ audienceId: audience.id,
609
+ matched: audienceMatched,
610
+ groupTraces,
611
+ ...accumulatedScore > 0 ? { totalScore: accumulatedScore } : {}
612
+ });
613
+ if (audienceMatched)
614
+ allMatchedIds.push(audience.id);
615
+ }
616
+ return { winningAudienceId: allMatchedIds[0] ?? null, allMatchedIds, traces };
617
+ }
618
+
619
+ // src/audience/behavioral-collector.ts
620
+ var SESSION_KEY = "kywi_session";
621
+ var TOTAL_VIEWS_KEY = "kywi_total_views";
622
+ var CATEGORY_SCORES_KEY = "kywi_category_scores";
623
+ var SESSION_START_KEY = "kywi_session_start";
624
+ function getSession() {
625
+ const raw = sessionStorage.getItem(SESSION_KEY);
626
+ if (raw) {
627
+ try {
628
+ return JSON.parse(raw);
629
+ } catch {
630
+ }
631
+ }
632
+ return { pageViewCount: 0, entryPage: "", pagesVisited: [] };
633
+ }
634
+ function getSessionStart() {
635
+ const raw = sessionStorage.getItem(SESSION_START_KEY);
636
+ if (raw) return Number(raw);
637
+ const now = Date.now();
638
+ sessionStorage.setItem(SESSION_START_KEY, String(now));
639
+ return now;
640
+ }
641
+ function getCategoryScores() {
642
+ const raw = localStorage.getItem(CATEGORY_SCORES_KEY);
643
+ if (raw) {
644
+ try {
645
+ return JSON.parse(raw);
646
+ } catch {
647
+ }
648
+ }
649
+ return {};
650
+ }
651
+ function collectBehavioral(currentPath, category) {
652
+ const session = getSession();
653
+ session.pageViewCount += 1;
654
+ if (!session.entryPage) session.entryPage = currentPath;
655
+ session.pagesVisited.push(currentPath);
656
+ sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
657
+ const totalViews = (Number(localStorage.getItem(TOTAL_VIEWS_KEY)) || 0) + 1;
658
+ localStorage.setItem(TOTAL_VIEWS_KEY, String(totalViews));
659
+ const sessionStart = getSessionStart();
660
+ const categoryScores = getCategoryScores();
661
+ if (category) {
662
+ categoryScores[category] = (categoryScores[category] ?? 0) + 1;
663
+ localStorage.setItem(CATEGORY_SCORES_KEY, JSON.stringify(categoryScores));
664
+ }
665
+ return {
666
+ session: { pageViewCount: session.pageViewCount, totalPageViews: totalViews, sessionDuration: Math.floor((Date.now() - sessionStart) / 1e3), entryPage: session.entryPage, pagesVisited: session.pagesVisited },
667
+ behavioral: { categoryScores, totalScore: 0 }
668
+ };
669
+ }
670
+
671
+ // src/audience/selfid-collector.ts
672
+ var SELFID_KEY = "kywi_selfid";
673
+ function collectSelfId() {
674
+ const raw = localStorage.getItem(SELFID_KEY);
675
+ if (!raw) return {};
676
+ try {
677
+ return JSON.parse(raw);
678
+ } catch {
679
+ return {};
680
+ }
681
+ }
682
+ function storeSelfId(responses) {
683
+ localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
684
+ }
685
+
686
+ // src/audience/evaluate.ts
687
+ function merge(base, partial) {
688
+ return {
689
+ utm: partial.utm ?? base.utm,
690
+ referrer: partial.referrer ?? base.referrer,
691
+ session: partial.session ?? base.session,
692
+ behavioral: partial.behavioral ?? base.behavioral,
693
+ selfId: { ...base.selfId, ...partial.selfId },
694
+ identity: partial.identity ?? base.identity,
695
+ adapters: { ...base.adapters, ...partial.adapters },
696
+ meta: { ...base.meta, ...partial.meta }
697
+ };
698
+ }
699
+ function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory) {
700
+ let signals = merge(createEmptySignals(), serverSignals);
701
+ const client = collectBehavioral(currentPath, pageCategory);
702
+ const selfId = collectSelfId();
703
+ signals = merge(signals, { session: client.session, behavioral: client.behavioral, selfId, meta: { ...signals.meta, resolvedAt: "client" } });
704
+ const { winningAudienceId, allMatchedIds } = resolveAudiences(audiences, signals);
705
+ const isOptedOut2 = signals.meta.isOptedOut;
706
+ return { winningAudienceId: isOptedOut2 ? null : winningAudienceId, allMatchedIds, signals, isOptedOut: isOptedOut2 };
707
+ }
708
+
709
+ // src/audience/dom-patcher.ts
710
+ function patchVariantContainers(winningAudienceId) {
711
+ const containers = document.querySelectorAll(".kywi-variant-container");
712
+ for (const container of Array.from(containers)) {
713
+ const el = container;
714
+ el.classList.remove("kywi-variant-loading");
715
+ el.removeAttribute("aria-busy");
716
+ const skeleton = el.querySelector(".kywi-variant-skeleton");
717
+ if (skeleton) skeleton.remove();
718
+ const target = winningAudienceId ?? "default";
719
+ el.setAttribute("data-variant", target);
720
+ for (const child of Array.from(el.querySelectorAll("[data-kywi-variant]"))) {
721
+ const h = child;
722
+ h.style.display = h.getAttribute("data-kywi-variant") === target ? "" : "none";
723
+ }
724
+ }
725
+ }
726
+
727
+ // src/cookies.ts
728
+ var COOKIE_NAMES = {
729
+ SIGNALS: "kywi_signals",
730
+ VISITOR: "kywi_visitor",
731
+ UTM: "kywi_utm",
732
+ AUDIENCE: "kywi_audience",
733
+ OPTOUT: "kywi_optout",
734
+ KNOWN: "kywi_known",
735
+ PREVIEW_INIT: "kywi_preview_init"
736
+ };
737
+ function readCookies() {
738
+ const map = /* @__PURE__ */ new Map();
739
+ if (typeof document === "undefined") return map;
740
+ for (const pair of document.cookie.split(";")) {
741
+ const trimmed = pair.trim();
742
+ const eqIdx = trimmed.indexOf("=");
743
+ if (eqIdx < 0) continue;
744
+ map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
745
+ }
746
+ return map;
747
+ }
748
+ function setCookie(name, value, maxAge) {
749
+ let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
750
+ if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
751
+ document.cookie = str;
752
+ }
753
+ function deleteCookie(name) {
754
+ document.cookie = `${name}=; Path=/; Max-Age=0`;
755
+ }
756
+
757
+ // src/audience/transparency-styles.ts
758
+ var STYLE_ELEMENT_ID = "kywi-transparency-styles";
759
+ var CSS = `
760
+ .kywi-transparency-bar,
761
+ .kywi-transparency-bar * {
762
+ box-sizing: border-box;
763
+ }
764
+ .kywi-transparency-bar {
765
+ position: fixed;
766
+ left: 0;
767
+ right: 0;
768
+ bottom: 0;
769
+ z-index: 2147483000;
770
+ display: flex;
771
+ flex-wrap: wrap;
772
+ align-items: center;
773
+ gap: 12px;
774
+ margin: 0;
775
+ padding: 12px 16px;
776
+ background: rgba(23, 23, 28, 0.96);
777
+ color: #f4f4f5;
778
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
779
+ font-size: 13px;
780
+ line-height: 1.4;
781
+ border-top: 1px solid rgba(255, 255, 255, 0.12);
782
+ box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.25);
783
+ animation: kywi-transparency-in 220ms ease-out;
784
+ }
785
+ @media (prefers-reduced-motion: reduce) {
786
+ .kywi-transparency-bar { animation: none; }
787
+ }
788
+ @keyframes kywi-transparency-in {
789
+ from { transform: translateY(100%); opacity: 0; }
790
+ to { transform: translateY(0); opacity: 1; }
791
+ }
792
+ .kywi-transparency-text {
793
+ flex: 1 1 240px;
794
+ color: #f4f4f5;
795
+ }
796
+ .kywi-transparency-bar button {
797
+ appearance: none;
798
+ -webkit-appearance: none;
799
+ font-family: inherit;
800
+ font-size: 12px;
801
+ line-height: 1.4;
802
+ cursor: pointer;
803
+ border-radius: 999px;
804
+ transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
805
+ }
806
+ .kywi-transparency-opt-out,
807
+ .kywi-transparency-opt-in {
808
+ flex: 0 0 auto;
809
+ padding: 6px 14px;
810
+ border: 1px solid rgba(255, 255, 255, 0.35);
811
+ background: transparent;
812
+ color: #f4f4f5;
813
+ font-weight: 600;
814
+ }
815
+ .kywi-transparency-opt-out:hover,
816
+ .kywi-transparency-opt-in:hover {
817
+ background: rgba(255, 255, 255, 0.12);
818
+ border-color: rgba(255, 255, 255, 0.5);
819
+ }
820
+ .kywi-transparency-dismiss {
821
+ flex: 0 0 auto;
822
+ width: 26px;
823
+ height: 26px;
824
+ padding: 0;
825
+ border: 1px solid transparent;
826
+ background: transparent;
827
+ color: #d4d4d8;
828
+ font-size: 13px;
829
+ }
830
+ .kywi-transparency-dismiss:hover {
831
+ background: rgba(255, 255, 255, 0.1);
832
+ color: #f4f4f5;
833
+ }
834
+ .kywi-transparency-bar button:focus-visible {
835
+ outline: 2px solid #7dd3fc;
836
+ outline-offset: 2px;
837
+ }
838
+ @media (max-width: 480px) {
839
+ .kywi-transparency-bar {
840
+ padding: 10px 12px;
841
+ }
842
+ .kywi-transparency-text {
843
+ flex-basis: 100%;
844
+ }
845
+ }
846
+ `;
847
+ function ensureTransparencyStyles() {
848
+ if (typeof document === "undefined") return;
849
+ if (document.getElementById(STYLE_ELEMENT_ID)) return;
850
+ const style = document.createElement("style");
851
+ style.id = STYLE_ELEMENT_ID;
852
+ style.textContent = CSS;
853
+ document.head.appendChild(style);
854
+ }
855
+
856
+ // src/audience/transparency.ts
857
+ var DISMISSED_KEY = "kywi_transparency_dismissed";
858
+ function isDismissed() {
859
+ return localStorage.getItem(DISMISSED_KEY) === "1";
860
+ }
861
+ function dismiss() {
862
+ localStorage.setItem(DISMISSED_KEY, "1");
863
+ removeBar();
864
+ }
865
+ function removeBar() {
866
+ document.querySelector(".kywi-transparency-bar")?.remove();
867
+ }
868
+ function optOut() {
869
+ setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
870
+ deleteCookie(COOKIE_NAMES.AUDIENCE);
871
+ }
872
+ function optIn() {
873
+ deleteCookie(COOKIE_NAMES.OPTOUT);
874
+ }
875
+ function createButton(className, text, onClick) {
876
+ const btn = document.createElement("button");
877
+ btn.className = className;
878
+ btn.type = "button";
879
+ btn.textContent = text;
880
+ btn.addEventListener("click", onClick);
881
+ return btn;
882
+ }
883
+ function renderTransparencyBar(state) {
884
+ removeBar();
885
+ if (isDismissed()) return;
886
+ if (state.isPersonalized && !state.isOptedOut || state.isOptedOut && state.hasMatchAvailable) {
887
+ ensureTransparencyStyles();
888
+ }
889
+ if (state.isPersonalized && !state.isOptedOut) {
890
+ const bar = document.createElement("div");
891
+ bar.className = "kywi-transparency-bar kywi-transparency-active";
892
+ bar.setAttribute("role", "status");
893
+ const text = document.createElement("span");
894
+ text.className = "kywi-transparency-text";
895
+ text.textContent = "This page has been personalized based on your browsing context.";
896
+ bar.appendChild(text);
897
+ bar.appendChild(createButton("kywi-transparency-opt-out", "Opt out", () => {
898
+ optOut();
899
+ window.location.reload();
900
+ }));
901
+ const dismissBtn = createButton("kywi-transparency-dismiss", "\u2715", () => dismiss());
902
+ dismissBtn.setAttribute("aria-label", "Dismiss");
903
+ bar.appendChild(dismissBtn);
904
+ document.body.appendChild(bar);
905
+ } else if (state.isOptedOut && state.hasMatchAvailable) {
906
+ const bar = document.createElement("div");
907
+ bar.className = "kywi-transparency-bar kywi-transparency-opted-out";
908
+ bar.setAttribute("role", "status");
909
+ const text = document.createElement("span");
910
+ text.className = "kywi-transparency-text";
911
+ text.textContent = "A personalized experience is available.";
912
+ bar.appendChild(text);
913
+ bar.appendChild(createButton("kywi-transparency-opt-in", "Enable", () => {
914
+ optIn();
915
+ window.location.reload();
916
+ }));
917
+ const dismissBtn = createButton("kywi-transparency-dismiss", "\u2715", () => dismiss());
918
+ dismissBtn.setAttribute("aria-label", "Dismiss");
919
+ bar.appendChild(dismissBtn);
920
+ document.body.appendChild(bar);
921
+ }
922
+ }
923
+
924
+ // src/audience/preview.ts
925
+ var PREVIEW_KEY = "kywi_preview_audience";
926
+ function getPreviewAudienceId() {
927
+ return sessionStorage.getItem(PREVIEW_KEY);
928
+ }
929
+ function setPreviewAudienceId(id) {
930
+ sessionStorage.setItem(PREVIEW_KEY, id);
931
+ }
932
+ function clearPreview() {
933
+ sessionStorage.removeItem(PREVIEW_KEY);
934
+ }
935
+ function checkPreviewInit() {
936
+ const cookies = readCookies();
937
+ const initId = cookies.get(COOKIE_NAMES.PREVIEW_INIT);
938
+ if (initId) {
939
+ setPreviewAudienceId(initId);
940
+ deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
941
+ }
942
+ }
943
+ function renderPreviewBanner(audienceName) {
944
+ document.querySelector(".kywi-preview-banner")?.remove();
945
+ const banner = document.createElement("div");
946
+ banner.className = "kywi-preview-banner";
947
+ banner.setAttribute("role", "status");
948
+ const text = document.createElement("span");
949
+ text.className = "kywi-preview-text";
950
+ text.textContent = "Previewing as: ";
951
+ const strong = document.createElement("strong");
952
+ strong.textContent = audienceName;
953
+ text.appendChild(strong);
954
+ banner.appendChild(text);
955
+ const exitBtn = document.createElement("button");
956
+ exitBtn.className = "kywi-preview-exit";
957
+ exitBtn.type = "button";
958
+ exitBtn.textContent = "Exit Preview";
959
+ exitBtn.addEventListener("click", () => {
960
+ clearPreview();
961
+ window.location.reload();
962
+ });
963
+ banner.appendChild(exitBtn);
964
+ document.body.appendChild(banner);
965
+ }
966
+
967
+ // src/audience/data-layer.ts
968
+ function populateDataLayer(visitor) {
969
+ const g = window.kywi ?? {};
970
+ g.visitor = visitor;
971
+ window.kywi = g;
972
+ }
973
+ function fireKywiReady() {
974
+ window.dispatchEvent(new CustomEvent("kywiReady"));
975
+ }
976
+ function readServerMeta() {
977
+ const m = (n) => document.querySelector(`meta[name="kywi:${n}"]`)?.getAttribute("content") ?? null;
978
+ return { audienceId: m("audience-id"), experimentId: m("experiment-id"), variantId: m("variant-id"), visitorId: m("visitor-id") ?? "" };
979
+ }
980
+
981
+ // src/audience/adapter-runner.ts
982
+ var CACHE_PREFIX = "kywi_adapter_";
983
+ function getCached(adapterId) {
984
+ const raw = sessionStorage.getItem(CACHE_PREFIX + adapterId);
985
+ if (!raw) return null;
986
+ try {
987
+ return JSON.parse(raw);
988
+ } catch {
989
+ return null;
990
+ }
991
+ }
992
+ function setCache(adapterId, data) {
993
+ sessionStorage.setItem(CACHE_PREFIX + adapterId, JSON.stringify(data));
994
+ }
995
+ async function runClientAdapters(adapters, currentSignals, apiBase = "/api/v1") {
996
+ const merged = {};
997
+ const tasks = adapters.map(async (adapter) => {
998
+ const cached = getCached(adapter.id);
999
+ if (cached) return cached;
1000
+ let result;
1001
+ if (adapter.side === "server-proxy") {
1002
+ const res = await fetch(`${apiBase}/kywi/adapters/${adapter.id}`, {
1003
+ method: "POST",
1004
+ headers: { "Content-Type": "application/json" },
1005
+ body: JSON.stringify({ signals: currentSignals })
1006
+ });
1007
+ const json = await res.json();
1008
+ result = json.data ?? {};
1009
+ } else {
1010
+ result = await adapter.collect(currentSignals);
1011
+ }
1012
+ setCache(adapter.id, result);
1013
+ return result;
1014
+ });
1015
+ const results = await Promise.all(tasks);
1016
+ for (const r of results) Object.assign(merged, r);
1017
+ return merged;
1018
+ }
1019
+
1020
+ // src/audience/selfid-submit.ts
1021
+ function handleSelfIdSubmit(responses, audiences, currentPath) {
1022
+ storeSelfId(responses);
1023
+ if (audiences && currentPath) {
1024
+ const cookies = readCookies();
1025
+ const serverMeta = readServerMeta();
1026
+ const serverSignals = {
1027
+ identity: { visitorId: serverMeta.visitorId || cookies.get(COOKIE_NAMES.VISITOR) || "", isKnown: cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1028
+ meta: { isOptedOut: cookies.has(COOKIE_NAMES.OPTOUT), isPreview: false, previewAudienceId: null, resolvedAt: "server" }
1029
+ };
1030
+ const result = evaluateClientSide(audiences, serverSignals, currentPath);
1031
+ patchVariantContainers(result.winningAudienceId);
1032
+ populateDataLayer({
1033
+ visitorId: result.signals.identity.visitorId,
1034
+ audienceId: result.winningAudienceId,
1035
+ experimentId: serverMeta.experimentId,
1036
+ variantId: serverMeta.variantId,
1037
+ isPreview: false,
1038
+ isOptedOut: result.isOptedOut
1039
+ });
1040
+ }
1041
+ }
1042
+
1043
+ // src/audience/selfid-widget.ts
1044
+ var SHOWN_KEY = "kywi_selfid_shown";
1045
+ var MS_PER_DAY = 864e5;
1046
+ function createSelfIdWidget(config) {
1047
+ const frequency = config.frequency ?? "once";
1048
+ const displayMode = config.displayMode ?? "inline";
1049
+ function shouldShow() {
1050
+ if (frequency === "always") return true;
1051
+ if (frequency === "session") return false;
1052
+ const raw = localStorage.getItem(SHOWN_KEY);
1053
+ if (!raw) return true;
1054
+ if (frequency === "once") return false;
1055
+ const ts = Number(raw);
1056
+ return !isNaN(ts) && Date.now() - ts > MS_PER_DAY;
1057
+ }
1058
+ function markShown() {
1059
+ localStorage.setItem(SHOWN_KEY, String(Date.now()));
1060
+ }
1061
+ function handleSubmit(responses) {
1062
+ storeSelfId(responses);
1063
+ handleSelfIdSubmit(responses, config.audiences ?? [], config.currentPath ?? "/");
1064
+ markShown();
1065
+ }
1066
+ function render(container) {
1067
+ container.replaceChildren();
1068
+ container.className = `kywi-selfid-widget kywi-selfid-${displayMode}`;
1069
+ if (config.headline) {
1070
+ const h = document.createElement("h3");
1071
+ h.className = "kywi-selfid-headline";
1072
+ h.textContent = config.headline;
1073
+ container.appendChild(h);
1074
+ }
1075
+ if (config.subheadline) {
1076
+ const p = document.createElement("p");
1077
+ p.className = "kywi-selfid-subheadline";
1078
+ p.textContent = config.subheadline;
1079
+ container.appendChild(p);
1080
+ }
1081
+ const form = document.createElement("form");
1082
+ form.className = "kywi-selfid-form";
1083
+ for (const field of config.fields) {
1084
+ const fieldWrap = document.createElement("div");
1085
+ fieldWrap.className = "kywi-selfid-field";
1086
+ const label = document.createElement("label");
1087
+ label.htmlFor = `kywi-field-${field.id}`;
1088
+ label.textContent = field.label;
1089
+ fieldWrap.appendChild(label);
1090
+ let input;
1091
+ if (field.type === "select") {
1092
+ const select = document.createElement("select");
1093
+ select.id = `kywi-field-${field.id}`;
1094
+ select.name = field.id;
1095
+ if (field.required) select.required = true;
1096
+ if (field.options) {
1097
+ const placeholder = document.createElement("option");
1098
+ placeholder.value = "";
1099
+ placeholder.textContent = "";
1100
+ select.appendChild(placeholder);
1101
+ for (const opt of field.options) {
1102
+ const option = document.createElement("option");
1103
+ option.value = opt;
1104
+ option.textContent = opt;
1105
+ select.appendChild(option);
1106
+ }
1107
+ }
1108
+ input = select;
1109
+ } else {
1110
+ const inp = document.createElement("input");
1111
+ inp.id = `kywi-field-${field.id}`;
1112
+ inp.name = field.id;
1113
+ inp.type = field.type;
1114
+ if (field.required) inp.required = true;
1115
+ input = inp;
1116
+ }
1117
+ fieldWrap.appendChild(input);
1118
+ form.appendChild(fieldWrap);
1119
+ }
1120
+ const actions = document.createElement("div");
1121
+ actions.className = "kywi-selfid-actions";
1122
+ const submitBtn = document.createElement("button");
1123
+ submitBtn.type = "submit";
1124
+ submitBtn.className = "kywi-selfid-submit";
1125
+ submitBtn.textContent = config.submitLabel ?? "Submit";
1126
+ actions.appendChild(submitBtn);
1127
+ if (config.skipLabel) {
1128
+ const skipBtn = document.createElement("button");
1129
+ skipBtn.type = "button";
1130
+ skipBtn.className = "kywi-selfid-skip";
1131
+ skipBtn.textContent = config.skipLabel;
1132
+ skipBtn.addEventListener("click", () => {
1133
+ markShown();
1134
+ container.replaceChildren();
1135
+ });
1136
+ actions.appendChild(skipBtn);
1137
+ }
1138
+ form.appendChild(actions);
1139
+ form.addEventListener("submit", (e) => {
1140
+ e.preventDefault();
1141
+ const data = new FormData(form);
1142
+ const responses = {};
1143
+ data.forEach((value, key) => {
1144
+ responses[key] = String(value);
1145
+ });
1146
+ handleSubmit(responses);
1147
+ });
1148
+ container.appendChild(form);
1149
+ }
1150
+ return { shouldShow, markShown, render, handleSubmit };
1151
+ }
1152
+
1153
+ // src/personalization/badge.ts
1154
+ function isOptedOut() {
1155
+ return readCookies().has(COOKIE_NAMES.OPTOUT);
1156
+ }
1157
+ function createButton2(className, text, onClick) {
1158
+ const btn = document.createElement("button");
1159
+ btn.className = className;
1160
+ btn.type = "button";
1161
+ btn.textContent = text;
1162
+ btn.addEventListener("click", onClick);
1163
+ return btn;
1164
+ }
1165
+ function renderPersonalizationBadge() {
1166
+ const containers = document.querySelectorAll("[data-kywi-personalization-badge]");
1167
+ if (containers.length === 0) return;
1168
+ for (const container of Array.from(containers)) {
1169
+ const existing = container.querySelector(".kywi-personalization-badge");
1170
+ if (existing) existing.remove();
1171
+ const badge = document.createElement("div");
1172
+ badge.className = "kywi-personalization-badge";
1173
+ const optedOut = isOptedOut();
1174
+ const status = document.createElement("span");
1175
+ status.className = "kywi-badge-status";
1176
+ status.textContent = optedOut ? "Personalization disabled" : "Personalization active";
1177
+ badge.appendChild(status);
1178
+ if (optedOut) {
1179
+ badge.appendChild(createButton2("kywi-badge-opt-in", "Opt in", () => {
1180
+ deleteCookie(COOKIE_NAMES.OPTOUT);
1181
+ renderPersonalizationBadge();
1182
+ }));
1183
+ } else {
1184
+ badge.appendChild(createButton2("kywi-badge-opt-out", "Opt out", () => {
1185
+ setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
1186
+ deleteCookie(COOKIE_NAMES.AUDIENCE);
1187
+ renderPersonalizationBadge();
1188
+ }));
1189
+ }
1190
+ container.appendChild(badge);
1191
+ }
1192
+ }
1193
+
1194
+ // src/audience/index.ts
1195
+ function resolveApiBase(explicit) {
1196
+ if (explicit) return explicit;
1197
+ const basePath = Kywi.context?.basePath;
1198
+ if (!basePath || basePath === "/") return "/api/v1";
1199
+ return `${basePath}/api/v1`;
1200
+ }
1201
+ async function bootAudienceEngine(config) {
1202
+ const { audiences, currentPath, pageCategory, serverSignals } = config;
1203
+ const apiBase = resolveApiBase(config.apiBase);
1204
+ checkPreviewInit();
1205
+ const serverMeta = readServerMeta();
1206
+ const previewId = getPreviewAudienceId();
1207
+ if (previewId) {
1208
+ patchVariantContainers(previewId);
1209
+ renderPreviewBanner(previewId);
1210
+ renderPersonalizationBadge();
1211
+ populateDataLayer({ visitorId: serverMeta.visitorId, audienceId: previewId, experimentId: null, variantId: null, isPreview: true, isOptedOut: false });
1212
+ fireKywiReady();
1213
+ return;
1214
+ }
1215
+ const cookies = readCookies();
1216
+ let merged = {
1217
+ ...serverSignals,
1218
+ identity: { visitorId: serverMeta.visitorId || cookies.get(COOKIE_NAMES.VISITOR) || "", isKnown: cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1219
+ meta: { isOptedOut: cookies.has(COOKIE_NAMES.OPTOUT), isPreview: false, previewAudienceId: null, resolvedAt: "server" }
1220
+ };
1221
+ if (config.adapters && config.adapters.length > 0) {
1222
+ const adapterResults = await runClientAdapters(config.adapters, merged, apiBase);
1223
+ merged.adapters = { ...merged.adapters, ...adapterResults };
1224
+ }
1225
+ const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1226
+ patchVariantContainers(result.winningAudienceId);
1227
+ renderTransparencyBar({ isPersonalized: result.winningAudienceId !== null, isOptedOut: result.isOptedOut, hasMatchAvailable: result.allMatchedIds.length > 0 });
1228
+ renderPersonalizationBadge();
1229
+ populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: result.winningAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1230
+ fireKywiReady();
1231
+ if (result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1232
+ fetch(`${apiBase}/kywi/audience`, {
1233
+ method: "POST",
1234
+ headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
1235
+ body: JSON.stringify({ audienceId: result.winningAudienceId })
1236
+ }).catch(() => {
1237
+ });
1238
+ }
1239
+ if (config.selfIdWidget) {
1240
+ const widget = createSelfIdWidget(config.selfIdWidget);
1241
+ if (widget.shouldShow()) {
1242
+ const doRender = () => {
1243
+ const existing = document.getElementById("kywi-selfid-container");
1244
+ const container = existing ?? document.createElement("div");
1245
+ if (!container.id) {
1246
+ container.id = "kywi-selfid-container";
1247
+ document.body.appendChild(container);
1248
+ }
1249
+ widget.render(container);
1250
+ };
1251
+ scheduleSelfIdWidget(config.selfIdWidget.trigger, doRender);
1252
+ }
1253
+ }
1254
+ }
1255
+ function scheduleSelfIdWidget(trigger, render) {
1256
+ if (!trigger || trigger.type === "immediate") {
1257
+ render();
1258
+ return;
1259
+ }
1260
+ if (trigger.type === "manual") return;
1261
+ if (trigger.type === "delay") {
1262
+ window.setTimeout(render, Math.max(0, trigger.seconds) * 1e3);
1263
+ return;
1264
+ }
1265
+ const target = Math.min(Math.max(trigger.depth, 0), 1);
1266
+ let fired = false;
1267
+ const onScroll = () => {
1268
+ if (fired) return;
1269
+ const scrollable = document.documentElement.scrollHeight - window.innerHeight;
1270
+ const progress = scrollable > 0 ? window.scrollY / scrollable : 1;
1271
+ if (progress >= target) {
1272
+ fired = true;
1273
+ window.removeEventListener("scroll", onScroll);
1274
+ render();
1275
+ }
1276
+ };
1277
+ window.addEventListener("scroll", onScroll, { passive: true });
1278
+ onScroll();
1279
+ }
1280
+
1281
+ // src/index.ts
1282
+ var preInitCallbacks = [];
1283
+ var booted = false;
1284
+ var Kywi = {
1285
+ version: "0.1.0",
1286
+ context: null,
1287
+ Module: {},
1288
+ preInit(fn) {
1289
+ if (booted) {
1290
+ fn(Kywi);
1291
+ } else {
1292
+ preInitCallbacks.push(fn);
1293
+ }
1294
+ },
1295
+ UI: {
1296
+ extend(definition) {
1297
+ return { ...definition };
1298
+ }
1299
+ },
1300
+ DisplayObject: {
1301
+ Form: {
1302
+ _hooks: {},
1303
+ extend(methods) {
1304
+ Object.assign(Kywi.DisplayObject.Form._hooks, methods);
1305
+ }
1306
+ }
1307
+ },
1308
+ getEntity: null,
1309
+ getFeed: null,
1310
+ renderFeed: null,
1311
+ boot() {
1312
+ if (booted) return;
1313
+ Kywi.context = readContext();
1314
+ for (const fn of preInitCallbacks) {
1315
+ fn(Kywi);
1316
+ }
1317
+ const basePath = Kywi.context.basePath === "/" ? "/api/v1" : `${Kywi.context.basePath}/api/v1`;
1318
+ Kywi.getEntity = createEntityFactory(basePath, Kywi.context.csrfToken);
1319
+ Kywi.getFeed = createFeedFactory(basePath, Kywi.context.csrfToken);
1320
+ Kywi.renderFeed = renderFeed;
1321
+ initModules();
1322
+ initFormHooks();
1323
+ bootExperiments({ context: Kywi.context });
1324
+ booted = true;
1325
+ }
1326
+ };
1327
+ if (typeof globalThis !== "undefined") {
1328
+ globalThis.Kywi = Kywi;
1329
+ }
1330
+ if (typeof document !== "undefined") {
1331
+ if (document.readyState === "loading") {
1332
+ document.addEventListener("DOMContentLoaded", () => Kywi.boot());
1333
+ } else {
1334
+ Kywi.boot();
1335
+ }
1336
+ }
1337
+ return __toCommonJS(src_exports);
1338
+ })();
1339
+ //# sourceMappingURL=kywi.js.map