@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.esm.js CHANGED
@@ -728,116 +728,45 @@ function deleteCookie(name) {
728
728
  document.cookie = `${name}=; Path=/; Max-Age=0`;
729
729
  }
730
730
 
731
- // src/audience/transparency-styles.ts
732
- var STYLE_ELEMENT_ID = "kywi-transparency-styles";
733
- var CSS = `
734
- .kywi-transparency-bar,
735
- .kywi-transparency-bar * {
736
- box-sizing: border-box;
737
- }
738
- .kywi-transparency-bar {
739
- position: fixed;
740
- left: 0;
741
- right: 0;
742
- bottom: 0;
743
- z-index: 2147483000;
744
- display: flex;
745
- flex-wrap: wrap;
746
- align-items: center;
747
- gap: 12px;
748
- margin: 0;
749
- padding: 12px 16px;
750
- background: rgba(23, 23, 28, 0.96);
751
- color: #f4f4f5;
752
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
753
- font-size: 13px;
754
- line-height: 1.4;
755
- border-top: 1px solid rgba(255, 255, 255, 0.12);
756
- box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.25);
757
- animation: kywi-transparency-in 220ms ease-out;
758
- }
759
- @media (prefers-reduced-motion: reduce) {
760
- .kywi-transparency-bar { animation: none; }
761
- }
762
- @keyframes kywi-transparency-in {
763
- from { transform: translateY(100%); opacity: 0; }
764
- to { transform: translateY(0); opacity: 1; }
765
- }
766
- .kywi-transparency-text {
767
- flex: 1 1 240px;
768
- color: #f4f4f5;
769
- }
770
- .kywi-transparency-bar button {
771
- appearance: none;
772
- -webkit-appearance: none;
773
- font-family: inherit;
774
- font-size: 12px;
775
- line-height: 1.4;
776
- cursor: pointer;
777
- border-radius: 999px;
778
- transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
779
- }
780
- .kywi-transparency-opt-out,
781
- .kywi-transparency-opt-in {
782
- flex: 0 0 auto;
783
- padding: 6px 14px;
784
- border: 1px solid rgba(255, 255, 255, 0.35);
785
- background: transparent;
786
- color: #f4f4f5;
787
- font-weight: 600;
788
- }
789
- .kywi-transparency-opt-out:hover,
790
- .kywi-transparency-opt-in:hover {
791
- background: rgba(255, 255, 255, 0.12);
792
- border-color: rgba(255, 255, 255, 0.5);
793
- }
794
- .kywi-transparency-dismiss {
795
- flex: 0 0 auto;
796
- width: 26px;
797
- height: 26px;
798
- padding: 0;
799
- border: 1px solid transparent;
800
- background: transparent;
801
- color: #d4d4d8;
802
- font-size: 13px;
803
- }
804
- .kywi-transparency-dismiss:hover {
805
- background: rgba(255, 255, 255, 0.1);
806
- color: #f4f4f5;
807
- }
808
- .kywi-transparency-bar button:focus-visible {
809
- outline: 2px solid #7dd3fc;
810
- outline-offset: 2px;
811
- }
812
- @media (max-width: 480px) {
813
- .kywi-transparency-bar {
814
- padding: 10px 12px;
815
- }
816
- .kywi-transparency-text {
817
- flex-basis: 100%;
818
- }
819
- }
820
- `;
821
- function ensureTransparencyStyles() {
822
- if (typeof document === "undefined") return;
823
- if (document.getElementById(STYLE_ELEMENT_ID)) return;
824
- const style = document.createElement("style");
825
- style.id = STYLE_ELEMENT_ID;
826
- style.textContent = CSS;
827
- document.head.appendChild(style);
828
- }
829
-
830
731
  // src/audience/transparency.ts
831
- var DISMISSED_KEY = "kywi_transparency_dismissed";
832
- function isDismissed() {
833
- return localStorage.getItem(DISMISSED_KEY) === "1";
732
+ var COLLAPSED_KEY = "kywi_transparency_collapsed";
733
+ var LEGACY_DISMISSED_KEY = "kywi_transparency_dismissed";
734
+ var ROOT_ID = "kywi-transparency";
735
+ var PANEL_ID = "kywi-transparency-panel";
736
+ var TITLE_ID = "kywi-transparency-title";
737
+ var SWITCHER_LABEL_ID = "kywi-transparency-switcher-label";
738
+ var DEFAULT_API_BASE = "/api/v1";
739
+ var lastState = null;
740
+ var forcedOpen = false;
741
+ function readStorage(key) {
742
+ try {
743
+ return localStorage.getItem(key);
744
+ } catch {
745
+ return null;
746
+ }
834
747
  }
835
- function dismiss() {
836
- localStorage.setItem(DISMISSED_KEY, "1");
837
- removeBar();
748
+ function writeStorage(key, value) {
749
+ try {
750
+ localStorage.setItem(key, value);
751
+ } catch {
752
+ }
838
753
  }
839
- function removeBar() {
840
- document.querySelector(".kywi-transparency-bar")?.remove();
754
+ function removeStorage(key) {
755
+ try {
756
+ localStorage.removeItem(key);
757
+ } catch {
758
+ }
759
+ }
760
+ function isCollapsed() {
761
+ if (readStorage(LEGACY_DISMISSED_KEY) === "1") {
762
+ removeStorage(LEGACY_DISMISSED_KEY);
763
+ writeStorage(COLLAPSED_KEY, "1");
764
+ return true;
765
+ }
766
+ return readStorage(COLLAPSED_KEY) !== "0";
767
+ }
768
+ function setCollapsed(collapsed) {
769
+ writeStorage(COLLAPSED_KEY, collapsed ? "1" : "0");
841
770
  }
842
771
  function optOut() {
843
772
  setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
@@ -846,53 +775,199 @@ function optOut() {
846
775
  function optIn() {
847
776
  deleteCookie(COOKIE_NAMES.OPTOUT);
848
777
  }
849
- function createButton(className, text, onClick) {
778
+ function resetToAutomatic() {
779
+ deleteCookie(COOKIE_NAMES.AUDIENCE);
780
+ }
781
+ function hasPin() {
782
+ return readCookies().has(COOKIE_NAMES.AUDIENCE);
783
+ }
784
+ async function pinAudience(audienceId, opts = {}) {
785
+ const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
786
+ try {
787
+ await fetch(`${apiBase}/kywi/audience`, {
788
+ method: "POST",
789
+ headers: { "Content-Type": "application/json", ...opts.csrfToken ? { "X-CSRF-Token": opts.csrfToken } : {} },
790
+ body: JSON.stringify({ audienceId })
791
+ });
792
+ } catch {
793
+ }
794
+ }
795
+ function reload() {
796
+ try {
797
+ window.location.reload();
798
+ } catch {
799
+ }
800
+ }
801
+ function removeSurface() {
802
+ document.getElementById(ROOT_ID)?.remove();
803
+ document.querySelector(".kywi-transparency-bar")?.remove();
804
+ }
805
+ function button(className, text, onClick) {
850
806
  const btn = document.createElement("button");
851
- btn.className = className;
852
807
  btn.type = "button";
808
+ btn.className = className;
853
809
  btn.textContent = text;
854
810
  btn.addEventListener("click", onClick);
855
811
  return btn;
856
812
  }
857
- function renderTransparencyBar(state) {
858
- removeBar();
859
- if (isDismissed()) return;
860
- if (state.isPersonalized && !state.isOptedOut || state.isOptedOut && state.hasMatchAvailable) {
861
- ensureTransparencyStyles();
862
- }
863
- if (state.isPersonalized && !state.isOptedOut) {
864
- const bar = document.createElement("div");
865
- bar.className = "kywi-transparency-bar kywi-transparency-active";
866
- bar.setAttribute("role", "status");
867
- const text = document.createElement("span");
868
- text.className = "kywi-transparency-text";
869
- text.textContent = "This page has been personalized based on your browsing context.";
870
- bar.appendChild(text);
871
- bar.appendChild(createButton("kywi-transparency-opt-out", "Opt out", () => {
872
- optOut();
873
- window.location.reload();
874
- }));
875
- const dismissBtn = createButton("kywi-transparency-dismiss", "\u2715", () => dismiss());
876
- dismissBtn.setAttribute("aria-label", "Dismiss");
877
- bar.appendChild(dismissBtn);
878
- document.body.appendChild(bar);
879
- } else if (state.isOptedOut && state.hasMatchAvailable) {
880
- const bar = document.createElement("div");
881
- bar.className = "kywi-transparency-bar kywi-transparency-opted-out";
882
- bar.setAttribute("role", "status");
883
- const text = document.createElement("span");
884
- text.className = "kywi-transparency-text";
885
- text.textContent = "A personalized experience is available.";
886
- bar.appendChild(text);
887
- bar.appendChild(createButton("kywi-transparency-opt-in", "Enable", () => {
813
+ function shouldRenderAutomatically(state) {
814
+ if (state.enabled === false) return false;
815
+ return state.isPersonalized && !state.isOptedOut || state.isOptedOut && state.hasMatchAvailable;
816
+ }
817
+ function audienceName(state) {
818
+ if (!state.audienceId) return null;
819
+ return state.audiences?.find((a) => a.id === state.audienceId)?.name ?? null;
820
+ }
821
+ function renderTransparencyPanel(state) {
822
+ lastState = state;
823
+ if (typeof document === "undefined") return;
824
+ removeSurface();
825
+ if (!forcedOpen && !shouldRenderAutomatically(state)) return;
826
+ const active = state.isPersonalized && !state.isOptedOut;
827
+ const collapsed = forcedOpen ? false : isCollapsed();
828
+ const root = document.createElement("div");
829
+ root.id = ROOT_ID;
830
+ root.className = `kywi-transparency ${active ? "kywi-transparency-active" : "kywi-transparency-opted-out"}`;
831
+ root.dataset.state = collapsed ? "collapsed" : "expanded";
832
+ const pill = document.createElement("button");
833
+ pill.type = "button";
834
+ pill.className = "kywi-transparency-pill";
835
+ pill.setAttribute("aria-expanded", String(!collapsed));
836
+ pill.setAttribute("aria-controls", PANEL_ID);
837
+ const pillIcon = document.createElement("span");
838
+ pillIcon.className = "kywi-transparency-pill-icon";
839
+ pillIcon.setAttribute("aria-hidden", "true");
840
+ pillIcon.textContent = "\u2726";
841
+ const pillLabel = document.createElement("span");
842
+ pillLabel.className = "kywi-transparency-pill-label";
843
+ pillLabel.textContent = active ? "Personalized" : "Personalization off";
844
+ pill.append(pillIcon, pillLabel);
845
+ pill.addEventListener("click", () => expandPanel());
846
+ pill.hidden = !collapsed;
847
+ root.appendChild(pill);
848
+ const panel = document.createElement("section");
849
+ panel.id = PANEL_ID;
850
+ panel.className = "kywi-transparency-panel";
851
+ panel.setAttribute("role", "dialog");
852
+ panel.setAttribute("aria-labelledby", TITLE_ID);
853
+ panel.hidden = collapsed;
854
+ const header = document.createElement("div");
855
+ header.className = "kywi-transparency-header";
856
+ const title = document.createElement("h2");
857
+ title.id = TITLE_ID;
858
+ title.className = "kywi-transparency-title";
859
+ const name = audienceName(state);
860
+ title.textContent = !active ? "Personalization is off" : name ? "This page is personalized for:" : "This page is personalized";
861
+ header.appendChild(title);
862
+ const collapseBtn = button("kywi-transparency-collapse", "\u2715", () => collapsePanel());
863
+ collapseBtn.setAttribute("aria-label", "Collapse");
864
+ header.appendChild(collapseBtn);
865
+ panel.appendChild(header);
866
+ if (active) {
867
+ const audience = document.createElement("p");
868
+ audience.className = "kywi-transparency-audience";
869
+ if (name) {
870
+ const strong = document.createElement("strong");
871
+ strong.textContent = name;
872
+ audience.appendChild(strong);
873
+ } else {
874
+ audience.textContent = "Based on your browsing context.";
875
+ }
876
+ panel.appendChild(audience);
877
+ }
878
+ const note = document.createElement("p");
879
+ note.className = "kywi-transparency-note";
880
+ 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.";
881
+ panel.appendChild(note);
882
+ const options = state.audiences ?? [];
883
+ if (active && options.length > 0) {
884
+ const switcher = document.createElement("div");
885
+ switcher.className = "kywi-transparency-switcher";
886
+ const switcherLabel = document.createElement("span");
887
+ switcherLabel.id = SWITCHER_LABEL_ID;
888
+ switcherLabel.className = "kywi-transparency-switcher-label";
889
+ switcherLabel.textContent = "See this page as";
890
+ switcher.appendChild(switcherLabel);
891
+ const group = document.createElement("div");
892
+ group.className = "kywi-transparency-options";
893
+ group.setAttribute("role", "group");
894
+ group.setAttribute("aria-labelledby", SWITCHER_LABEL_ID);
895
+ for (const option of options) {
896
+ const isCurrent = option.id === state.audienceId;
897
+ const optionBtn = button("kywi-transparency-option", option.name, () => {
898
+ if (isCurrent) return;
899
+ void pinAudience(option.id, { apiBase: state.apiBase, csrfToken: state.csrfToken }).then(reload);
900
+ });
901
+ optionBtn.dataset.kywiAudienceId = option.id;
902
+ optionBtn.setAttribute("aria-pressed", String(isCurrent));
903
+ group.appendChild(optionBtn);
904
+ }
905
+ switcher.appendChild(group);
906
+ panel.appendChild(switcher);
907
+ }
908
+ const actions = document.createElement("div");
909
+ actions.className = "kywi-transparency-actions";
910
+ if (state.isOptedOut) {
911
+ actions.appendChild(button("kywi-transparency-opt-in", "Enable personalization", () => {
888
912
  optIn();
889
- window.location.reload();
913
+ reload();
914
+ }));
915
+ } else {
916
+ if (hasPin()) {
917
+ actions.appendChild(button("kywi-transparency-reset", "Reset to automatic", () => {
918
+ resetToAutomatic();
919
+ reload();
920
+ }));
921
+ }
922
+ actions.appendChild(button("kywi-transparency-opt-out", "Turn off personalization", () => {
923
+ optOut();
924
+ reload();
890
925
  }));
891
- const dismissBtn = createButton("kywi-transparency-dismiss", "\u2715", () => dismiss());
892
- dismissBtn.setAttribute("aria-label", "Dismiss");
893
- bar.appendChild(dismissBtn);
894
- document.body.appendChild(bar);
895
926
  }
927
+ panel.appendChild(actions);
928
+ panel.addEventListener("keydown", (e) => {
929
+ if (e.key === "Escape") collapsePanel();
930
+ });
931
+ root.appendChild(panel);
932
+ document.body.appendChild(root);
933
+ }
934
+ function currentSurface() {
935
+ const root = document.getElementById(ROOT_ID);
936
+ if (!root) return null;
937
+ const pill = root.querySelector(".kywi-transparency-pill");
938
+ const panel = root.querySelector(".kywi-transparency-panel");
939
+ if (!pill || !panel) return null;
940
+ return { root, pill, panel };
941
+ }
942
+ function setExpanded(expanded) {
943
+ const surface = currentSurface();
944
+ if (!surface) return;
945
+ surface.root.dataset.state = expanded ? "expanded" : "collapsed";
946
+ surface.pill.hidden = expanded;
947
+ surface.pill.setAttribute("aria-expanded", String(expanded));
948
+ surface.panel.hidden = !expanded;
949
+ const focusTarget = expanded ? surface.panel.querySelector(".kywi-transparency-collapse") : surface.pill;
950
+ focusTarget?.focus();
951
+ }
952
+ function expandPanel() {
953
+ setCollapsed(false);
954
+ setExpanded(true);
955
+ }
956
+ function collapsePanel() {
957
+ forcedOpen = false;
958
+ setCollapsed(true);
959
+ setExpanded(false);
960
+ }
961
+ function openTransparencyPanel() {
962
+ if (typeof document === "undefined") return;
963
+ forcedOpen = true;
964
+ const state = lastState ?? {
965
+ isPersonalized: false,
966
+ isOptedOut: readCookies().has(COOKIE_NAMES.OPTOUT),
967
+ hasMatchAvailable: false
968
+ };
969
+ renderTransparencyPanel(state);
970
+ currentSurface()?.panel.querySelector(".kywi-transparency-collapse")?.focus();
896
971
  }
897
972
 
898
973
  // src/audience/preview.ts
@@ -914,7 +989,7 @@ function checkPreviewInit() {
914
989
  deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
915
990
  }
916
991
  }
917
- function renderPreviewBanner(audienceName) {
992
+ function renderPreviewBanner(audienceName2) {
918
993
  document.querySelector(".kywi-preview-banner")?.remove();
919
994
  const banner = document.createElement("div");
920
995
  banner.className = "kywi-preview-banner";
@@ -923,7 +998,7 @@ function renderPreviewBanner(audienceName) {
923
998
  text.className = "kywi-preview-text";
924
999
  text.textContent = "Previewing as: ";
925
1000
  const strong = document.createElement("strong");
926
- strong.textContent = audienceName;
1001
+ strong.textContent = audienceName2;
927
1002
  text.appendChild(strong);
928
1003
  banner.appendChild(text);
929
1004
  const exitBtn = document.createElement("button");
@@ -1017,13 +1092,50 @@ function handleSelfIdSubmit(responses, audiences, currentPath) {
1017
1092
  // src/audience/selfid-widget.ts
1018
1093
  var SHOWN_KEY = "kywi_selfid_shown";
1019
1094
  var MS_PER_DAY = 864e5;
1020
- var DISMISSIBLE_MODES = /* @__PURE__ */ new Set(["hello_bar_top", "hello_bar_bottom", "drawer"]);
1095
+ var DISMISSIBLE_MODES = /* @__PURE__ */ new Set(["modal", "slide-in", "hello_bar_top", "hello_bar_bottom", "drawer"]);
1096
+ var OVERLAY_MODES = DISMISSIBLE_MODES;
1097
+ var SELFID_MOUNT_SELECTOR = "[data-kywi-selfid-mount]";
1098
+ var HELLO_BAR_TOP_CLASS = "kywi-selfid-hello-bar-top-open";
1099
+ var HELLO_BAR_HEIGHT_PROPERTY = "--kywi-selfid-hello-bar-height";
1100
+ var MODAL_LABEL_ID = "kywi-selfid-headline";
1101
+ function resolveSelfIdMount(displayMode) {
1102
+ if (typeof document === "undefined") return null;
1103
+ if (displayMode && displayMode !== "inline") return null;
1104
+ return document.querySelector(SELFID_MOUNT_SELECTOR);
1105
+ }
1021
1106
  function normalizeOption(opt) {
1022
1107
  return typeof opt === "string" ? { value: opt, label: opt } : opt;
1023
1108
  }
1109
+ var FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
1110
+ function focusableElements(root) {
1111
+ return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => !el.hidden);
1112
+ }
1113
+ function trapTab(root, e) {
1114
+ const focusable = focusableElements(root);
1115
+ if (focusable.length === 0) {
1116
+ e.preventDefault();
1117
+ root.focus();
1118
+ return;
1119
+ }
1120
+ const first = focusable[0];
1121
+ const last = focusable[focusable.length - 1];
1122
+ const active = document.activeElement;
1123
+ const inside = active !== null && root.contains(active) && active !== root;
1124
+ if (e.shiftKey) {
1125
+ if (!inside || active === first) {
1126
+ e.preventDefault();
1127
+ last.focus();
1128
+ }
1129
+ } else if (!inside || active === last) {
1130
+ e.preventDefault();
1131
+ first.focus();
1132
+ }
1133
+ }
1024
1134
  function createSelfIdWidget(config) {
1025
1135
  const frequency = config.frequency ?? "once";
1026
1136
  const displayMode = config.displayMode ?? "inline";
1137
+ const isModal = displayMode === "modal";
1138
+ let cleanups = [];
1027
1139
  function shouldShow() {
1028
1140
  if (frequency === "always") return true;
1029
1141
  if (frequency === "session") return false;
@@ -1044,26 +1156,86 @@ function createSelfIdWidget(config) {
1044
1156
  markShown();
1045
1157
  }
1046
1158
  }
1159
+ function teardown(container) {
1160
+ for (const undo of cleanups) undo();
1161
+ cleanups = [];
1162
+ container.replaceChildren();
1163
+ container.className = "";
1164
+ container.removeAttribute("role");
1165
+ container.removeAttribute("aria-modal");
1166
+ container.removeAttribute("aria-label");
1167
+ container.removeAttribute("aria-labelledby");
1168
+ container.removeAttribute("tabindex");
1169
+ container.hidden = true;
1170
+ }
1171
+ function dismiss(container) {
1172
+ markShown();
1173
+ teardown(container);
1174
+ }
1047
1175
  function render(container) {
1176
+ for (const undo of cleanups) undo();
1177
+ cleanups = [];
1048
1178
  container.replaceChildren();
1179
+ container.hidden = false;
1049
1180
  const displayModeClass = displayMode.replace(/_/g, "-");
1050
1181
  container.className = `kywi-selfid-widget kywi-selfid-${displayModeClass}`;
1182
+ if (isModal) {
1183
+ const backdrop = document.createElement("div");
1184
+ backdrop.className = "kywi-selfid-backdrop";
1185
+ backdrop.addEventListener("click", () => dismiss(container));
1186
+ (container.parentElement ?? document.body).appendChild(backdrop);
1187
+ cleanups.push(() => backdrop.remove());
1188
+ const previousOverflow = document.body.style.overflow;
1189
+ document.body.style.overflow = "hidden";
1190
+ cleanups.push(() => {
1191
+ document.body.style.overflow = previousOverflow;
1192
+ });
1193
+ const onKeydown = (e) => {
1194
+ if (e.key === "Escape") {
1195
+ e.preventDefault();
1196
+ dismiss(container);
1197
+ return;
1198
+ }
1199
+ if (e.key === "Tab") trapTab(container, e);
1200
+ };
1201
+ document.addEventListener("keydown", onKeydown, true);
1202
+ cleanups.push(() => document.removeEventListener("keydown", onKeydown, true));
1203
+ container.setAttribute("role", "dialog");
1204
+ container.setAttribute("aria-modal", "true");
1205
+ if (config.headline) {
1206
+ container.setAttribute("aria-labelledby", MODAL_LABEL_ID);
1207
+ } else {
1208
+ container.setAttribute("aria-label", "Personalize your experience");
1209
+ }
1210
+ container.tabIndex = -1;
1211
+ }
1212
+ if (displayMode === "hello_bar_top") {
1213
+ document.body.classList.add(HELLO_BAR_TOP_CLASS);
1214
+ const syncHeight = () => {
1215
+ document.documentElement.style.setProperty(HELLO_BAR_HEIGHT_PROPERTY, `${container.offsetHeight}px`);
1216
+ };
1217
+ window.addEventListener("resize", syncHeight);
1218
+ cleanups.push(() => {
1219
+ document.body.classList.remove(HELLO_BAR_TOP_CLASS);
1220
+ document.documentElement.style.removeProperty(HELLO_BAR_HEIGHT_PROPERTY);
1221
+ window.removeEventListener("resize", syncHeight);
1222
+ });
1223
+ queueMicrotask(syncHeight);
1224
+ }
1051
1225
  if (DISMISSIBLE_MODES.has(displayMode)) {
1052
1226
  const dismissBtn = document.createElement("button");
1053
1227
  dismissBtn.type = "button";
1054
1228
  dismissBtn.className = "kywi-selfid-dismiss";
1055
1229
  dismissBtn.setAttribute("aria-label", "Dismiss");
1056
1230
  dismissBtn.textContent = "\xD7";
1057
- dismissBtn.addEventListener("click", () => {
1058
- markShown();
1059
- container.replaceChildren();
1060
- });
1231
+ dismissBtn.addEventListener("click", () => dismiss(container));
1061
1232
  container.appendChild(dismissBtn);
1062
1233
  }
1063
1234
  if (config.headline) {
1064
1235
  const h = document.createElement("h3");
1065
1236
  h.className = "kywi-selfid-headline";
1066
1237
  h.textContent = config.headline;
1238
+ if (isModal) h.id = MODAL_LABEL_ID;
1067
1239
  container.appendChild(h);
1068
1240
  }
1069
1241
  if (config.subheadline) {
@@ -1124,10 +1296,7 @@ function createSelfIdWidget(config) {
1124
1296
  skipBtn.type = "button";
1125
1297
  skipBtn.className = "kywi-selfid-skip";
1126
1298
  skipBtn.textContent = config.skipLabel;
1127
- skipBtn.addEventListener("click", () => {
1128
- markShown();
1129
- container.replaceChildren();
1130
- });
1299
+ skipBtn.addEventListener("click", () => dismiss(container));
1131
1300
  actions.appendChild(skipBtn);
1132
1301
  }
1133
1302
  form.appendChild(actions);
@@ -1139,8 +1308,10 @@ function createSelfIdWidget(config) {
1139
1308
  responses[key] = String(value);
1140
1309
  });
1141
1310
  handleSubmit(responses);
1311
+ if (OVERLAY_MODES.has(displayMode)) teardown(container);
1142
1312
  });
1143
1313
  container.appendChild(form);
1314
+ if (isModal) container.focus();
1144
1315
  }
1145
1316
  return { shouldShow, markShown, render, handleSubmit };
1146
1317
  }
@@ -1149,39 +1320,30 @@ function createSelfIdWidget(config) {
1149
1320
  function isOptedOut() {
1150
1321
  return readCookies().has(COOKIE_NAMES.OPTOUT);
1151
1322
  }
1152
- function createButton2(className, text, onClick) {
1153
- const btn = document.createElement("button");
1154
- btn.className = className;
1155
- btn.type = "button";
1156
- btn.textContent = text;
1157
- btn.addEventListener("click", onClick);
1158
- return btn;
1159
- }
1160
- function renderPersonalizationBadge() {
1323
+ function renderPersonalizationBadge(info = {}) {
1161
1324
  const containers = document.querySelectorAll("[data-kywi-personalization-badge]");
1162
1325
  if (containers.length === 0) return;
1326
+ const optedOut = isOptedOut();
1163
1327
  for (const container of Array.from(containers)) {
1164
- const existing = container.querySelector(".kywi-personalization-badge");
1165
- if (existing) existing.remove();
1328
+ container.querySelector(".kywi-personalization-badge")?.remove();
1166
1329
  const badge = document.createElement("div");
1167
1330
  badge.className = "kywi-personalization-badge";
1168
- const optedOut = isOptedOut();
1331
+ const trigger = document.createElement("button");
1332
+ trigger.type = "button";
1333
+ trigger.className = "kywi-badge-open";
1334
+ trigger.setAttribute("aria-label", "Personalization details");
1169
1335
  const status = document.createElement("span");
1170
1336
  status.className = "kywi-badge-status";
1171
- status.textContent = optedOut ? "Personalization disabled" : "Personalization active";
1172
- badge.appendChild(status);
1173
- if (optedOut) {
1174
- badge.appendChild(createButton2("kywi-badge-opt-in", "Opt in", () => {
1175
- deleteCookie(COOKIE_NAMES.OPTOUT);
1176
- renderPersonalizationBadge();
1177
- }));
1178
- } else {
1179
- badge.appendChild(createButton2("kywi-badge-opt-out", "Opt out", () => {
1180
- setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
1181
- deleteCookie(COOKIE_NAMES.AUDIENCE);
1182
- renderPersonalizationBadge();
1183
- }));
1337
+ status.textContent = optedOut ? "Personalization off" : "Personalization active";
1338
+ trigger.appendChild(status);
1339
+ if (!optedOut && info.audienceName) {
1340
+ const audience = document.createElement("span");
1341
+ audience.className = "kywi-badge-audience";
1342
+ audience.textContent = info.audienceName;
1343
+ trigger.appendChild(audience);
1184
1344
  }
1345
+ trigger.addEventListener("click", () => openTransparencyPanel());
1346
+ badge.appendChild(trigger);
1185
1347
  container.appendChild(badge);
1186
1348
  }
1187
1349
  }
@@ -1199,10 +1361,12 @@ async function bootAudienceEngine(config) {
1199
1361
  checkPreviewInit();
1200
1362
  const serverMeta = readServerMeta();
1201
1363
  const previewId = getPreviewAudienceId();
1364
+ const activeAudiences = audiences.filter((a) => a.status === "active").map((a) => ({ id: a.id, name: a.name }));
1365
+ const nameOf = (id) => id ? activeAudiences.find((a) => a.id === id)?.name ?? null : null;
1202
1366
  if (previewId) {
1203
1367
  patchVariantContainers(previewId);
1204
- renderPreviewBanner(previewId);
1205
- renderPersonalizationBadge();
1368
+ renderPreviewBanner(nameOf(previewId) ?? previewId);
1369
+ renderPersonalizationBadge({ audienceName: nameOf(previewId) });
1206
1370
  populateDataLayer({ visitorId: serverMeta.visitorId, audienceId: previewId, experimentId: null, variantId: null, isPreview: true, isOptedOut: false });
1207
1371
  fireKywiReady();
1208
1372
  return;
@@ -1218,12 +1382,24 @@ async function bootAudienceEngine(config) {
1218
1382
  merged.adapters = { ...merged.adapters, ...adapterResults };
1219
1383
  }
1220
1384
  const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1221
- patchVariantContainers(result.winningAudienceId);
1222
- renderTransparencyBar({ isPersonalized: result.winningAudienceId !== null, isOptedOut: result.isOptedOut, hasMatchAvailable: result.allMatchedIds.length > 0 });
1223
- renderPersonalizationBadge();
1224
- populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: result.winningAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1385
+ const pinnedId = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1386
+ const isPinned = !result.isOptedOut && pinnedId !== null && activeAudiences.some((a) => a.id === pinnedId);
1387
+ const effectiveAudienceId = isPinned ? pinnedId : result.winningAudienceId;
1388
+ patchVariantContainers(effectiveAudienceId);
1389
+ renderTransparencyPanel({
1390
+ isPersonalized: effectiveAudienceId !== null,
1391
+ isOptedOut: result.isOptedOut,
1392
+ hasMatchAvailable: result.allMatchedIds.length > 0,
1393
+ audienceId: effectiveAudienceId,
1394
+ audiences: activeAudiences,
1395
+ apiBase,
1396
+ csrfToken: Kywi.context?.csrfToken ?? null,
1397
+ enabled: config.transparencyNotice !== false
1398
+ });
1399
+ renderPersonalizationBadge({ audienceName: nameOf(effectiveAudienceId) });
1400
+ populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: effectiveAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1225
1401
  fireKywiReady();
1226
- if (result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1402
+ if (!isPinned && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1227
1403
  fetch(`${apiBase}/kywi/audience`, {
1228
1404
  method: "POST",
1229
1405
  headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
@@ -1235,6 +1411,11 @@ async function bootAudienceEngine(config) {
1235
1411
  const widget = createSelfIdWidget(config.selfIdWidget);
1236
1412
  if (widget.shouldShow()) {
1237
1413
  const doRender = () => {
1414
+ const mount = resolveSelfIdMount(config.selfIdWidget.displayMode);
1415
+ if (mount) {
1416
+ widget.render(mount);
1417
+ return;
1418
+ }
1238
1419
  const existing = document.getElementById("kywi-selfid-container");
1239
1420
  const container = existing ?? document.createElement("div");
1240
1421
  if (!container.id) {
@@ -1332,6 +1513,8 @@ if (typeof document !== "undefined") {
1332
1513
  export {
1333
1514
  Kywi,
1334
1515
  bootAudienceEngine,
1335
- bootExperiments
1516
+ bootExperiments,
1517
+ openTransparencyPanel,
1518
+ renderTransparencyPanel
1336
1519
  };
1337
1520
  //# sourceMappingURL=kywi.esm.js.map