@kywi-software/js 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/kywi.js CHANGED
@@ -23,7 +23,9 @@ var Kywi = (() => {
23
23
  __export(src_exports, {
24
24
  Kywi: () => Kywi,
25
25
  bootAudienceEngine: () => bootAudienceEngine,
26
- bootExperiments: () => bootExperiments
26
+ bootExperiments: () => bootExperiments,
27
+ openTransparencyPanel: () => openTransparencyPanel,
28
+ renderTransparencyPanel: () => renderTransparencyPanel
27
29
  });
28
30
 
29
31
  // src/context.ts
@@ -756,116 +758,45 @@ var Kywi = (() => {
756
758
  document.cookie = `${name}=; Path=/; Max-Age=0`;
757
759
  }
758
760
 
759
- // src/audience/transparency-styles.ts
760
- var STYLE_ELEMENT_ID = "kywi-transparency-styles";
761
- var CSS = `
762
- .kywi-transparency-bar,
763
- .kywi-transparency-bar * {
764
- box-sizing: border-box;
765
- }
766
- .kywi-transparency-bar {
767
- position: fixed;
768
- left: 0;
769
- right: 0;
770
- bottom: 0;
771
- z-index: 2147483000;
772
- display: flex;
773
- flex-wrap: wrap;
774
- align-items: center;
775
- gap: 12px;
776
- margin: 0;
777
- padding: 12px 16px;
778
- background: rgba(23, 23, 28, 0.96);
779
- color: #f4f4f5;
780
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
781
- font-size: 13px;
782
- line-height: 1.4;
783
- border-top: 1px solid rgba(255, 255, 255, 0.12);
784
- box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.25);
785
- animation: kywi-transparency-in 220ms ease-out;
786
- }
787
- @media (prefers-reduced-motion: reduce) {
788
- .kywi-transparency-bar { animation: none; }
789
- }
790
- @keyframes kywi-transparency-in {
791
- from { transform: translateY(100%); opacity: 0; }
792
- to { transform: translateY(0); opacity: 1; }
793
- }
794
- .kywi-transparency-text {
795
- flex: 1 1 240px;
796
- color: #f4f4f5;
797
- }
798
- .kywi-transparency-bar button {
799
- appearance: none;
800
- -webkit-appearance: none;
801
- font-family: inherit;
802
- font-size: 12px;
803
- line-height: 1.4;
804
- cursor: pointer;
805
- border-radius: 999px;
806
- transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
807
- }
808
- .kywi-transparency-opt-out,
809
- .kywi-transparency-opt-in {
810
- flex: 0 0 auto;
811
- padding: 6px 14px;
812
- border: 1px solid rgba(255, 255, 255, 0.35);
813
- background: transparent;
814
- color: #f4f4f5;
815
- font-weight: 600;
816
- }
817
- .kywi-transparency-opt-out:hover,
818
- .kywi-transparency-opt-in:hover {
819
- background: rgba(255, 255, 255, 0.12);
820
- border-color: rgba(255, 255, 255, 0.5);
821
- }
822
- .kywi-transparency-dismiss {
823
- flex: 0 0 auto;
824
- width: 26px;
825
- height: 26px;
826
- padding: 0;
827
- border: 1px solid transparent;
828
- background: transparent;
829
- color: #d4d4d8;
830
- font-size: 13px;
831
- }
832
- .kywi-transparency-dismiss:hover {
833
- background: rgba(255, 255, 255, 0.1);
834
- color: #f4f4f5;
835
- }
836
- .kywi-transparency-bar button:focus-visible {
837
- outline: 2px solid #7dd3fc;
838
- outline-offset: 2px;
839
- }
840
- @media (max-width: 480px) {
841
- .kywi-transparency-bar {
842
- padding: 10px 12px;
843
- }
844
- .kywi-transparency-text {
845
- flex-basis: 100%;
846
- }
847
- }
848
- `;
849
- function ensureTransparencyStyles() {
850
- if (typeof document === "undefined") return;
851
- if (document.getElementById(STYLE_ELEMENT_ID)) return;
852
- const style = document.createElement("style");
853
- style.id = STYLE_ELEMENT_ID;
854
- style.textContent = CSS;
855
- document.head.appendChild(style);
856
- }
857
-
858
761
  // src/audience/transparency.ts
859
- var DISMISSED_KEY = "kywi_transparency_dismissed";
860
- function isDismissed() {
861
- return localStorage.getItem(DISMISSED_KEY) === "1";
762
+ var COLLAPSED_KEY = "kywi_transparency_collapsed";
763
+ var LEGACY_DISMISSED_KEY = "kywi_transparency_dismissed";
764
+ var ROOT_ID = "kywi-transparency";
765
+ var PANEL_ID = "kywi-transparency-panel";
766
+ var TITLE_ID = "kywi-transparency-title";
767
+ var SWITCHER_LABEL_ID = "kywi-transparency-switcher-label";
768
+ var DEFAULT_API_BASE = "/api/v1";
769
+ var lastState = null;
770
+ var forcedOpen = false;
771
+ function readStorage(key) {
772
+ try {
773
+ return localStorage.getItem(key);
774
+ } catch {
775
+ return null;
776
+ }
862
777
  }
863
- function dismiss() {
864
- localStorage.setItem(DISMISSED_KEY, "1");
865
- removeBar();
778
+ function writeStorage(key, value) {
779
+ try {
780
+ localStorage.setItem(key, value);
781
+ } catch {
782
+ }
866
783
  }
867
- function removeBar() {
868
- document.querySelector(".kywi-transparency-bar")?.remove();
784
+ function removeStorage(key) {
785
+ try {
786
+ localStorage.removeItem(key);
787
+ } catch {
788
+ }
789
+ }
790
+ function isCollapsed() {
791
+ if (readStorage(LEGACY_DISMISSED_KEY) === "1") {
792
+ removeStorage(LEGACY_DISMISSED_KEY);
793
+ writeStorage(COLLAPSED_KEY, "1");
794
+ return true;
795
+ }
796
+ return readStorage(COLLAPSED_KEY) !== "0";
797
+ }
798
+ function setCollapsed(collapsed) {
799
+ writeStorage(COLLAPSED_KEY, collapsed ? "1" : "0");
869
800
  }
870
801
  function optOut() {
871
802
  setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
@@ -874,53 +805,199 @@ var Kywi = (() => {
874
805
  function optIn() {
875
806
  deleteCookie(COOKIE_NAMES.OPTOUT);
876
807
  }
877
- function createButton(className, text, onClick) {
808
+ function resetToAutomatic() {
809
+ deleteCookie(COOKIE_NAMES.AUDIENCE);
810
+ }
811
+ function hasPin() {
812
+ return readCookies().has(COOKIE_NAMES.AUDIENCE);
813
+ }
814
+ async function pinAudience(audienceId, opts = {}) {
815
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
816
+ try {
817
+ await fetch(`${apiBase}/kywi/audience`, {
818
+ method: "POST",
819
+ headers: { "Content-Type": "application/json", ...opts.csrfToken ? { "X-CSRF-Token": opts.csrfToken } : {} },
820
+ body: JSON.stringify({ audienceId })
821
+ });
822
+ } catch {
823
+ }
824
+ }
825
+ function reload() {
826
+ try {
827
+ window.location.reload();
828
+ } catch {
829
+ }
830
+ }
831
+ function removeSurface() {
832
+ document.getElementById(ROOT_ID)?.remove();
833
+ document.querySelector(".kywi-transparency-bar")?.remove();
834
+ }
835
+ function button(className, text, onClick) {
878
836
  const btn = document.createElement("button");
879
- btn.className = className;
880
837
  btn.type = "button";
838
+ btn.className = className;
881
839
  btn.textContent = text;
882
840
  btn.addEventListener("click", onClick);
883
841
  return btn;
884
842
  }
885
- function renderTransparencyBar(state) {
886
- removeBar();
887
- if (isDismissed()) return;
888
- if (state.isPersonalized && !state.isOptedOut || state.isOptedOut && state.hasMatchAvailable) {
889
- ensureTransparencyStyles();
890
- }
891
- if (state.isPersonalized && !state.isOptedOut) {
892
- const bar = document.createElement("div");
893
- bar.className = "kywi-transparency-bar kywi-transparency-active";
894
- bar.setAttribute("role", "status");
895
- const text = document.createElement("span");
896
- text.className = "kywi-transparency-text";
897
- text.textContent = "This page has been personalized based on your browsing context.";
898
- bar.appendChild(text);
899
- bar.appendChild(createButton("kywi-transparency-opt-out", "Opt out", () => {
900
- optOut();
901
- window.location.reload();
902
- }));
903
- const dismissBtn = createButton("kywi-transparency-dismiss", "\u2715", () => dismiss());
904
- dismissBtn.setAttribute("aria-label", "Dismiss");
905
- bar.appendChild(dismissBtn);
906
- document.body.appendChild(bar);
907
- } else if (state.isOptedOut && state.hasMatchAvailable) {
908
- const bar = document.createElement("div");
909
- bar.className = "kywi-transparency-bar kywi-transparency-opted-out";
910
- bar.setAttribute("role", "status");
911
- const text = document.createElement("span");
912
- text.className = "kywi-transparency-text";
913
- text.textContent = "A personalized experience is available.";
914
- bar.appendChild(text);
915
- bar.appendChild(createButton("kywi-transparency-opt-in", "Enable", () => {
843
+ function shouldRenderAutomatically(state) {
844
+ if (state.enabled === false) return false;
845
+ return state.isPersonalized && !state.isOptedOut || state.isOptedOut && state.hasMatchAvailable;
846
+ }
847
+ function audienceName(state) {
848
+ if (!state.audienceId) return null;
849
+ return state.audiences?.find((a) => a.id === state.audienceId)?.name ?? null;
850
+ }
851
+ function renderTransparencyPanel(state) {
852
+ lastState = state;
853
+ if (typeof document === "undefined") return;
854
+ removeSurface();
855
+ if (!forcedOpen && !shouldRenderAutomatically(state)) return;
856
+ const active = state.isPersonalized && !state.isOptedOut;
857
+ const collapsed = forcedOpen ? false : isCollapsed();
858
+ const root = document.createElement("div");
859
+ root.id = ROOT_ID;
860
+ root.className = `kywi-transparency ${active ? "kywi-transparency-active" : "kywi-transparency-opted-out"}`;
861
+ root.dataset.state = collapsed ? "collapsed" : "expanded";
862
+ const pill = document.createElement("button");
863
+ pill.type = "button";
864
+ pill.className = "kywi-transparency-pill";
865
+ pill.setAttribute("aria-expanded", String(!collapsed));
866
+ pill.setAttribute("aria-controls", PANEL_ID);
867
+ const pillIcon = document.createElement("span");
868
+ pillIcon.className = "kywi-transparency-pill-icon";
869
+ pillIcon.setAttribute("aria-hidden", "true");
870
+ pillIcon.textContent = "\u2726";
871
+ const pillLabel = document.createElement("span");
872
+ pillLabel.className = "kywi-transparency-pill-label";
873
+ pillLabel.textContent = active ? "Personalized" : "Personalization off";
874
+ pill.append(pillIcon, pillLabel);
875
+ pill.addEventListener("click", () => expandPanel());
876
+ pill.hidden = !collapsed;
877
+ root.appendChild(pill);
878
+ const panel = document.createElement("section");
879
+ panel.id = PANEL_ID;
880
+ panel.className = "kywi-transparency-panel";
881
+ panel.setAttribute("role", "dialog");
882
+ panel.setAttribute("aria-labelledby", TITLE_ID);
883
+ panel.hidden = collapsed;
884
+ const header = document.createElement("div");
885
+ header.className = "kywi-transparency-header";
886
+ const title = document.createElement("h2");
887
+ title.id = TITLE_ID;
888
+ title.className = "kywi-transparency-title";
889
+ const name = audienceName(state);
890
+ title.textContent = !active ? "Personalization is off" : name ? "This page is personalized for:" : "This page is personalized";
891
+ header.appendChild(title);
892
+ const collapseBtn = button("kywi-transparency-collapse", "\u2715", () => collapsePanel());
893
+ collapseBtn.setAttribute("aria-label", "Collapse");
894
+ header.appendChild(collapseBtn);
895
+ panel.appendChild(header);
896
+ if (active) {
897
+ const audience = document.createElement("p");
898
+ audience.className = "kywi-transparency-audience";
899
+ if (name) {
900
+ const strong = document.createElement("strong");
901
+ strong.textContent = name;
902
+ audience.appendChild(strong);
903
+ } else {
904
+ audience.textContent = "Based on your browsing context.";
905
+ }
906
+ panel.appendChild(audience);
907
+ }
908
+ const note = document.createElement("p");
909
+ note.className = "kywi-transparency-note";
910
+ note.textContent = active ? "Parts of this page are chosen to match this audience. You can switch, reset, or turn personalization off." : "Personalization is turned off, so you are seeing the default version of this page.";
911
+ panel.appendChild(note);
912
+ const options = state.audiences ?? [];
913
+ if (active && options.length > 0) {
914
+ const switcher = document.createElement("div");
915
+ switcher.className = "kywi-transparency-switcher";
916
+ const switcherLabel = document.createElement("span");
917
+ switcherLabel.id = SWITCHER_LABEL_ID;
918
+ switcherLabel.className = "kywi-transparency-switcher-label";
919
+ switcherLabel.textContent = "See this page as";
920
+ switcher.appendChild(switcherLabel);
921
+ const group = document.createElement("div");
922
+ group.className = "kywi-transparency-options";
923
+ group.setAttribute("role", "group");
924
+ group.setAttribute("aria-labelledby", SWITCHER_LABEL_ID);
925
+ for (const option of options) {
926
+ const isCurrent = option.id === state.audienceId;
927
+ const optionBtn = button("kywi-transparency-option", option.name, () => {
928
+ if (isCurrent) return;
929
+ void pinAudience(option.id, { apiBase: state.apiBase, csrfToken: state.csrfToken }).then(reload);
930
+ });
931
+ optionBtn.dataset.kywiAudienceId = option.id;
932
+ optionBtn.setAttribute("aria-pressed", String(isCurrent));
933
+ group.appendChild(optionBtn);
934
+ }
935
+ switcher.appendChild(group);
936
+ panel.appendChild(switcher);
937
+ }
938
+ const actions = document.createElement("div");
939
+ actions.className = "kywi-transparency-actions";
940
+ if (state.isOptedOut) {
941
+ actions.appendChild(button("kywi-transparency-opt-in", "Enable personalization", () => {
916
942
  optIn();
917
- window.location.reload();
943
+ reload();
944
+ }));
945
+ } else {
946
+ if (hasPin()) {
947
+ actions.appendChild(button("kywi-transparency-reset", "Reset to automatic", () => {
948
+ resetToAutomatic();
949
+ reload();
950
+ }));
951
+ }
952
+ actions.appendChild(button("kywi-transparency-opt-out", "Turn off personalization", () => {
953
+ optOut();
954
+ reload();
918
955
  }));
919
- const dismissBtn = createButton("kywi-transparency-dismiss", "\u2715", () => dismiss());
920
- dismissBtn.setAttribute("aria-label", "Dismiss");
921
- bar.appendChild(dismissBtn);
922
- document.body.appendChild(bar);
923
956
  }
957
+ panel.appendChild(actions);
958
+ panel.addEventListener("keydown", (e) => {
959
+ if (e.key === "Escape") collapsePanel();
960
+ });
961
+ root.appendChild(panel);
962
+ document.body.appendChild(root);
963
+ }
964
+ function currentSurface() {
965
+ const root = document.getElementById(ROOT_ID);
966
+ if (!root) return null;
967
+ const pill = root.querySelector(".kywi-transparency-pill");
968
+ const panel = root.querySelector(".kywi-transparency-panel");
969
+ if (!pill || !panel) return null;
970
+ return { root, pill, panel };
971
+ }
972
+ function setExpanded(expanded) {
973
+ const surface = currentSurface();
974
+ if (!surface) return;
975
+ surface.root.dataset.state = expanded ? "expanded" : "collapsed";
976
+ surface.pill.hidden = expanded;
977
+ surface.pill.setAttribute("aria-expanded", String(expanded));
978
+ surface.panel.hidden = !expanded;
979
+ const focusTarget = expanded ? surface.panel.querySelector(".kywi-transparency-collapse") : surface.pill;
980
+ focusTarget?.focus();
981
+ }
982
+ function expandPanel() {
983
+ setCollapsed(false);
984
+ setExpanded(true);
985
+ }
986
+ function collapsePanel() {
987
+ forcedOpen = false;
988
+ setCollapsed(true);
989
+ setExpanded(false);
990
+ }
991
+ function openTransparencyPanel() {
992
+ if (typeof document === "undefined") return;
993
+ forcedOpen = true;
994
+ const state = lastState ?? {
995
+ isPersonalized: false,
996
+ isOptedOut: readCookies().has(COOKIE_NAMES.OPTOUT),
997
+ hasMatchAvailable: false
998
+ };
999
+ renderTransparencyPanel(state);
1000
+ currentSurface()?.panel.querySelector(".kywi-transparency-collapse")?.focus();
924
1001
  }
925
1002
 
926
1003
  // src/audience/preview.ts
@@ -942,7 +1019,7 @@ var Kywi = (() => {
942
1019
  deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
943
1020
  }
944
1021
  }
945
- function renderPreviewBanner(audienceName) {
1022
+ function renderPreviewBanner(audienceName2) {
946
1023
  document.querySelector(".kywi-preview-banner")?.remove();
947
1024
  const banner = document.createElement("div");
948
1025
  banner.className = "kywi-preview-banner";
@@ -951,7 +1028,7 @@ var Kywi = (() => {
951
1028
  text.className = "kywi-preview-text";
952
1029
  text.textContent = "Previewing as: ";
953
1030
  const strong = document.createElement("strong");
954
- strong.textContent = audienceName;
1031
+ strong.textContent = audienceName2;
955
1032
  text.appendChild(strong);
956
1033
  banner.appendChild(text);
957
1034
  const exitBtn = document.createElement("button");
@@ -1045,13 +1122,50 @@ var Kywi = (() => {
1045
1122
  // src/audience/selfid-widget.ts
1046
1123
  var SHOWN_KEY = "kywi_selfid_shown";
1047
1124
  var MS_PER_DAY = 864e5;
1048
- var DISMISSIBLE_MODES = /* @__PURE__ */ new Set(["hello_bar_top", "hello_bar_bottom", "drawer"]);
1125
+ var DISMISSIBLE_MODES = /* @__PURE__ */ new Set(["modal", "slide-in", "hello_bar_top", "hello_bar_bottom", "drawer"]);
1126
+ var OVERLAY_MODES = DISMISSIBLE_MODES;
1127
+ var SELFID_MOUNT_SELECTOR = "[data-kywi-selfid-mount]";
1128
+ var HELLO_BAR_TOP_CLASS = "kywi-selfid-hello-bar-top-open";
1129
+ var HELLO_BAR_HEIGHT_PROPERTY = "--kywi-selfid-hello-bar-height";
1130
+ var MODAL_LABEL_ID = "kywi-selfid-headline";
1131
+ function resolveSelfIdMount(displayMode) {
1132
+ if (typeof document === "undefined") return null;
1133
+ if (displayMode && displayMode !== "inline") return null;
1134
+ return document.querySelector(SELFID_MOUNT_SELECTOR);
1135
+ }
1049
1136
  function normalizeOption(opt) {
1050
1137
  return typeof opt === "string" ? { value: opt, label: opt } : opt;
1051
1138
  }
1139
+ var FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
1140
+ function focusableElements(root) {
1141
+ return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => !el.hidden);
1142
+ }
1143
+ function trapTab(root, e) {
1144
+ const focusable = focusableElements(root);
1145
+ if (focusable.length === 0) {
1146
+ e.preventDefault();
1147
+ root.focus();
1148
+ return;
1149
+ }
1150
+ const first = focusable[0];
1151
+ const last = focusable[focusable.length - 1];
1152
+ const active = document.activeElement;
1153
+ const inside = active !== null && root.contains(active) && active !== root;
1154
+ if (e.shiftKey) {
1155
+ if (!inside || active === first) {
1156
+ e.preventDefault();
1157
+ last.focus();
1158
+ }
1159
+ } else if (!inside || active === last) {
1160
+ e.preventDefault();
1161
+ first.focus();
1162
+ }
1163
+ }
1052
1164
  function createSelfIdWidget(config) {
1053
1165
  const frequency = config.frequency ?? "once";
1054
1166
  const displayMode = config.displayMode ?? "inline";
1167
+ const isModal = displayMode === "modal";
1168
+ let cleanups = [];
1055
1169
  function shouldShow() {
1056
1170
  if (frequency === "always") return true;
1057
1171
  if (frequency === "session") return false;
@@ -1072,26 +1186,86 @@ var Kywi = (() => {
1072
1186
  markShown();
1073
1187
  }
1074
1188
  }
1189
+ function teardown(container) {
1190
+ for (const undo of cleanups) undo();
1191
+ cleanups = [];
1192
+ container.replaceChildren();
1193
+ container.className = "";
1194
+ container.removeAttribute("role");
1195
+ container.removeAttribute("aria-modal");
1196
+ container.removeAttribute("aria-label");
1197
+ container.removeAttribute("aria-labelledby");
1198
+ container.removeAttribute("tabindex");
1199
+ container.hidden = true;
1200
+ }
1201
+ function dismiss(container) {
1202
+ markShown();
1203
+ teardown(container);
1204
+ }
1075
1205
  function render(container) {
1206
+ for (const undo of cleanups) undo();
1207
+ cleanups = [];
1076
1208
  container.replaceChildren();
1209
+ container.hidden = false;
1077
1210
  const displayModeClass = displayMode.replace(/_/g, "-");
1078
1211
  container.className = `kywi-selfid-widget kywi-selfid-${displayModeClass}`;
1212
+ if (isModal) {
1213
+ const backdrop = document.createElement("div");
1214
+ backdrop.className = "kywi-selfid-backdrop";
1215
+ backdrop.addEventListener("click", () => dismiss(container));
1216
+ (container.parentElement ?? document.body).appendChild(backdrop);
1217
+ cleanups.push(() => backdrop.remove());
1218
+ const previousOverflow = document.body.style.overflow;
1219
+ document.body.style.overflow = "hidden";
1220
+ cleanups.push(() => {
1221
+ document.body.style.overflow = previousOverflow;
1222
+ });
1223
+ const onKeydown = (e) => {
1224
+ if (e.key === "Escape") {
1225
+ e.preventDefault();
1226
+ dismiss(container);
1227
+ return;
1228
+ }
1229
+ if (e.key === "Tab") trapTab(container, e);
1230
+ };
1231
+ document.addEventListener("keydown", onKeydown, true);
1232
+ cleanups.push(() => document.removeEventListener("keydown", onKeydown, true));
1233
+ container.setAttribute("role", "dialog");
1234
+ container.setAttribute("aria-modal", "true");
1235
+ if (config.headline) {
1236
+ container.setAttribute("aria-labelledby", MODAL_LABEL_ID);
1237
+ } else {
1238
+ container.setAttribute("aria-label", "Personalize your experience");
1239
+ }
1240
+ container.tabIndex = -1;
1241
+ }
1242
+ if (displayMode === "hello_bar_top") {
1243
+ document.body.classList.add(HELLO_BAR_TOP_CLASS);
1244
+ const syncHeight = () => {
1245
+ document.documentElement.style.setProperty(HELLO_BAR_HEIGHT_PROPERTY, `${container.offsetHeight}px`);
1246
+ };
1247
+ window.addEventListener("resize", syncHeight);
1248
+ cleanups.push(() => {
1249
+ document.body.classList.remove(HELLO_BAR_TOP_CLASS);
1250
+ document.documentElement.style.removeProperty(HELLO_BAR_HEIGHT_PROPERTY);
1251
+ window.removeEventListener("resize", syncHeight);
1252
+ });
1253
+ queueMicrotask(syncHeight);
1254
+ }
1079
1255
  if (DISMISSIBLE_MODES.has(displayMode)) {
1080
1256
  const dismissBtn = document.createElement("button");
1081
1257
  dismissBtn.type = "button";
1082
1258
  dismissBtn.className = "kywi-selfid-dismiss";
1083
1259
  dismissBtn.setAttribute("aria-label", "Dismiss");
1084
1260
  dismissBtn.textContent = "\xD7";
1085
- dismissBtn.addEventListener("click", () => {
1086
- markShown();
1087
- container.replaceChildren();
1088
- });
1261
+ dismissBtn.addEventListener("click", () => dismiss(container));
1089
1262
  container.appendChild(dismissBtn);
1090
1263
  }
1091
1264
  if (config.headline) {
1092
1265
  const h = document.createElement("h3");
1093
1266
  h.className = "kywi-selfid-headline";
1094
1267
  h.textContent = config.headline;
1268
+ if (isModal) h.id = MODAL_LABEL_ID;
1095
1269
  container.appendChild(h);
1096
1270
  }
1097
1271
  if (config.subheadline) {
@@ -1152,10 +1326,7 @@ var Kywi = (() => {
1152
1326
  skipBtn.type = "button";
1153
1327
  skipBtn.className = "kywi-selfid-skip";
1154
1328
  skipBtn.textContent = config.skipLabel;
1155
- skipBtn.addEventListener("click", () => {
1156
- markShown();
1157
- container.replaceChildren();
1158
- });
1329
+ skipBtn.addEventListener("click", () => dismiss(container));
1159
1330
  actions.appendChild(skipBtn);
1160
1331
  }
1161
1332
  form.appendChild(actions);
@@ -1167,8 +1338,10 @@ var Kywi = (() => {
1167
1338
  responses[key] = String(value);
1168
1339
  });
1169
1340
  handleSubmit(responses);
1341
+ if (OVERLAY_MODES.has(displayMode)) teardown(container);
1170
1342
  });
1171
1343
  container.appendChild(form);
1344
+ if (isModal) container.focus();
1172
1345
  }
1173
1346
  return { shouldShow, markShown, render, handleSubmit };
1174
1347
  }
@@ -1177,39 +1350,30 @@ var Kywi = (() => {
1177
1350
  function isOptedOut() {
1178
1351
  return readCookies().has(COOKIE_NAMES.OPTOUT);
1179
1352
  }
1180
- function createButton2(className, text, onClick) {
1181
- const btn = document.createElement("button");
1182
- btn.className = className;
1183
- btn.type = "button";
1184
- btn.textContent = text;
1185
- btn.addEventListener("click", onClick);
1186
- return btn;
1187
- }
1188
- function renderPersonalizationBadge() {
1353
+ function renderPersonalizationBadge(info = {}) {
1189
1354
  const containers = document.querySelectorAll("[data-kywi-personalization-badge]");
1190
1355
  if (containers.length === 0) return;
1356
+ const optedOut = isOptedOut();
1191
1357
  for (const container of Array.from(containers)) {
1192
- const existing = container.querySelector(".kywi-personalization-badge");
1193
- if (existing) existing.remove();
1358
+ container.querySelector(".kywi-personalization-badge")?.remove();
1194
1359
  const badge = document.createElement("div");
1195
1360
  badge.className = "kywi-personalization-badge";
1196
- const optedOut = isOptedOut();
1361
+ const trigger = document.createElement("button");
1362
+ trigger.type = "button";
1363
+ trigger.className = "kywi-badge-open";
1364
+ trigger.setAttribute("aria-label", "Personalization details");
1197
1365
  const status = document.createElement("span");
1198
1366
  status.className = "kywi-badge-status";
1199
- status.textContent = optedOut ? "Personalization disabled" : "Personalization active";
1200
- badge.appendChild(status);
1201
- if (optedOut) {
1202
- badge.appendChild(createButton2("kywi-badge-opt-in", "Opt in", () => {
1203
- deleteCookie(COOKIE_NAMES.OPTOUT);
1204
- renderPersonalizationBadge();
1205
- }));
1206
- } else {
1207
- badge.appendChild(createButton2("kywi-badge-opt-out", "Opt out", () => {
1208
- setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
1209
- deleteCookie(COOKIE_NAMES.AUDIENCE);
1210
- renderPersonalizationBadge();
1211
- }));
1367
+ status.textContent = optedOut ? "Personalization off" : "Personalization active";
1368
+ trigger.appendChild(status);
1369
+ if (!optedOut && info.audienceName) {
1370
+ const audience = document.createElement("span");
1371
+ audience.className = "kywi-badge-audience";
1372
+ audience.textContent = info.audienceName;
1373
+ trigger.appendChild(audience);
1212
1374
  }
1375
+ trigger.addEventListener("click", () => openTransparencyPanel());
1376
+ badge.appendChild(trigger);
1213
1377
  container.appendChild(badge);
1214
1378
  }
1215
1379
  }
@@ -1227,10 +1391,12 @@ var Kywi = (() => {
1227
1391
  checkPreviewInit();
1228
1392
  const serverMeta = readServerMeta();
1229
1393
  const previewId = getPreviewAudienceId();
1394
+ const activeAudiences = audiences.filter((a) => a.status === "active").map((a) => ({ id: a.id, name: a.name }));
1395
+ const nameOf = (id) => id ? activeAudiences.find((a) => a.id === id)?.name ?? null : null;
1230
1396
  if (previewId) {
1231
1397
  patchVariantContainers(previewId);
1232
- renderPreviewBanner(previewId);
1233
- renderPersonalizationBadge();
1398
+ renderPreviewBanner(nameOf(previewId) ?? previewId);
1399
+ renderPersonalizationBadge({ audienceName: nameOf(previewId) });
1234
1400
  populateDataLayer({ visitorId: serverMeta.visitorId, audienceId: previewId, experimentId: null, variantId: null, isPreview: true, isOptedOut: false });
1235
1401
  fireKywiReady();
1236
1402
  return;
@@ -1246,12 +1412,24 @@ var Kywi = (() => {
1246
1412
  merged.adapters = { ...merged.adapters, ...adapterResults };
1247
1413
  }
1248
1414
  const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1249
- patchVariantContainers(result.winningAudienceId);
1250
- renderTransparencyBar({ isPersonalized: result.winningAudienceId !== null, isOptedOut: result.isOptedOut, hasMatchAvailable: result.allMatchedIds.length > 0 });
1251
- renderPersonalizationBadge();
1252
- populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: result.winningAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1415
+ const pinnedId = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1416
+ const isPinned = !result.isOptedOut && pinnedId !== null && activeAudiences.some((a) => a.id === pinnedId);
1417
+ const effectiveAudienceId = isPinned ? pinnedId : result.winningAudienceId;
1418
+ patchVariantContainers(effectiveAudienceId);
1419
+ renderTransparencyPanel({
1420
+ isPersonalized: effectiveAudienceId !== null,
1421
+ isOptedOut: result.isOptedOut,
1422
+ hasMatchAvailable: result.allMatchedIds.length > 0,
1423
+ audienceId: effectiveAudienceId,
1424
+ audiences: activeAudiences,
1425
+ apiBase,
1426
+ csrfToken: Kywi.context?.csrfToken ?? null,
1427
+ enabled: config.transparencyNotice !== false
1428
+ });
1429
+ renderPersonalizationBadge({ audienceName: nameOf(effectiveAudienceId) });
1430
+ populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: effectiveAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1253
1431
  fireKywiReady();
1254
- if (result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1432
+ if (!isPinned && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1255
1433
  fetch(`${apiBase}/kywi/audience`, {
1256
1434
  method: "POST",
1257
1435
  headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
@@ -1263,6 +1441,11 @@ var Kywi = (() => {
1263
1441
  const widget = createSelfIdWidget(config.selfIdWidget);
1264
1442
  if (widget.shouldShow()) {
1265
1443
  const doRender = () => {
1444
+ const mount = resolveSelfIdMount(config.selfIdWidget.displayMode);
1445
+ if (mount) {
1446
+ widget.render(mount);
1447
+ return;
1448
+ }
1266
1449
  const existing = document.getElementById("kywi-selfid-container");
1267
1450
  const container = existing ?? document.createElement("div");
1268
1451
  if (!container.id) {