@redacto.io/consent-sdk-js 1.0.0 → 1.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.
package/dist/redacto.js CHANGED
@@ -216,7 +216,7 @@ function decodeTokenSafely(token) {
216
216
  // src/api/sandbox.ts
217
217
  var SANDBOX_TOKEN_HEADER = "X-Consent-Token";
218
218
  var resolveSandboxAuth = (options) => {
219
- var _a, _b, _c, _d, _e;
219
+ var _a, _b, _c, _d, _e, _f;
220
220
  const token = (_a = options.token) == null ? void 0 : _a.trim();
221
221
  if (!token) {
222
222
  return null;
@@ -224,16 +224,17 @@ var resolveSandboxAuth = (options) => {
224
224
  const organisationUuid = (_b = options.organisationUuid) == null ? void 0 : _b.trim();
225
225
  const workspaceUuid = (_c = options.workspaceUuid) == null ? void 0 : _c.trim();
226
226
  const ucic = (_d = options.sandboxUcic) == null ? void 0 : _d.trim();
227
- const subject = (_e = options.sandboxSubject) == null ? void 0 : _e.trim();
227
+ const email = (_e = options.sandboxEmail) == null ? void 0 : _e.trim();
228
+ const mobile = (_f = options.sandboxMobile) == null ? void 0 : _f.trim();
228
229
  if (!organisationUuid || !workspaceUuid) {
229
230
  throw new Error(
230
231
  "Sandbox mode requires organisationUuid and workspaceUuid"
231
232
  );
232
233
  }
233
- if (!ucic && !subject) {
234
+ if (!ucic && !email && !mobile) {
234
235
  throw new Error("Sandbox mode requires a UCIC, email, or mobile");
235
236
  }
236
- return { token, ucic, subject: subject != null ? subject : "", organisationUuid, workspaceUuid };
237
+ return { token, ucic, email, mobile, organisationUuid, workspaceUuid };
237
238
  };
238
239
  var resolveRequestAuth = (accessToken, options) => {
239
240
  const sandbox = resolveSandboxAuth(options);
@@ -265,10 +266,17 @@ var buildAuthHeaders = (accessToken, sandbox) => {
265
266
  return { Authorization: `Bearer ${accessToken}` };
266
267
  };
267
268
  var sandboxBodyIdentity = (sandbox) => {
269
+ const payload = {};
268
270
  if (sandbox.ucic) {
269
- return { org_user_id: sandbox.ucic };
271
+ payload.org_user_id = sandbox.ucic;
270
272
  }
271
- return sandbox.subject.includes("@") ? { primary_email: sandbox.subject } : { primary_mobile: sandbox.subject };
273
+ if (sandbox.email) {
274
+ payload.primary_email = sandbox.email;
275
+ }
276
+ if (sandbox.mobile) {
277
+ payload.primary_mobile = sandbox.mobile;
278
+ }
279
+ return payload;
272
280
  };
273
281
  var sandboxQueryIdentity = (sandbox) => sandboxBodyIdentity(sandbox);
274
282
 
@@ -354,7 +362,7 @@ var fetchConsentContent = async ({
354
362
  sandboxParams
355
363
  );
356
364
  const apiBaseUrl = baseUrl || BASE_URL;
357
- const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.subject}` : accessToken;
365
+ const cacheIdentity = sandbox ? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.email || sandbox.mobile}` : accessToken;
358
366
  const cacheKey = `${cacheIdentity}-${noticeId}-${validate_against}-${language}-${specific_uuid || ""}-${include_fully_consented_data}`;
359
367
  if (validate_against === "all" && !specific_uuid) {
360
368
  const cached = getCachedData(cacheKey);
@@ -366,7 +374,7 @@ var fetchConsentContent = async ({
366
374
  if (include_fully_consented_data) url.searchParams.append("include_fully_consented_data", "true");
367
375
  if (sandbox) {
368
376
  for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
369
- url.searchParams.append(key, value);
377
+ if (value) url.searchParams.append(key, value);
370
378
  }
371
379
  }
372
380
  const response = await fetch(url.toString(), {
@@ -602,7 +610,7 @@ var fetchTTSAudioUrls = async ({
602
610
  );
603
611
  if (sandbox) {
604
612
  for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
605
- url.searchParams.append(key, value);
613
+ if (value) url.searchParams.append(key, value);
606
614
  }
607
615
  }
608
616
  const response = await fetch(url.toString(), {
@@ -1317,6 +1325,32 @@ var styles = {
1317
1325
  margin: 0,
1318
1326
  boxSizing: "border-box"
1319
1327
  },
1328
+ /**
1329
+ * The main notice footer. Wide: one row with decline pushed left and the
1330
+ * accepts grouped right. Narrow: a full-width stack. Review mode keeps
1331
+ * `bottomSection` — it collapses to a single button.
1332
+ */
1333
+ actionButtonRow: {
1334
+ display: "flex",
1335
+ flexDirection: "row",
1336
+ alignItems: "center",
1337
+ gap: "12px",
1338
+ paddingTop: "15px",
1339
+ borderTop: "none",
1340
+ backgroundColor: "#ffffff",
1341
+ margin: 0,
1342
+ boxSizing: "border-box"
1343
+ },
1344
+ actionButtonStack: {
1345
+ display: "flex",
1346
+ flexDirection: "column",
1347
+ gap: "12px",
1348
+ paddingTop: "15px",
1349
+ borderTop: "none",
1350
+ backgroundColor: "#ffffff",
1351
+ margin: 0,
1352
+ boxSizing: "border-box"
1353
+ },
1320
1354
  button: {
1321
1355
  width: "100%",
1322
1356
  borderRadius: "8px",
@@ -1403,6 +1437,51 @@ var styles = {
1403
1437
  }
1404
1438
  };
1405
1439
 
1440
+ // src/constants.ts
1441
+ var DEFAULT_LOGO_POSITION = "left";
1442
+ var DEFAULT_ACCEPT_ALL_BUTTON_TEXT = "Accept All";
1443
+ var DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT = "Accept Selected";
1444
+ var DEFAULT_DECLINE_BUTTON_TEXT = "Decline";
1445
+ var ACCEPT_ALL_BUTTON_ID = "accept-all-button-text";
1446
+ var ACCEPT_SELECTED_BUTTON_ID = "confirm-button-text";
1447
+ var DECLINE_BUTTON_ID = "decline-button-text";
1448
+ var SUBMITTING_BUTTON_TEXT = "Submitting...";
1449
+ var LOGO_POSITION_JUSTIFY = {
1450
+ left: "flex-start",
1451
+ center: "center",
1452
+ right: "flex-end"
1453
+ };
1454
+ var REQUIRED_MARKER_GLYPH = "*";
1455
+ var REQUIRED_MARKER_ARIA_LABEL = "required";
1456
+ var REQUIRED_MARKER_COLOR = "red";
1457
+ var REQUIRED_MARKER_GAP = "4px";
1458
+ var REQUIRED_MARKER_STYLE = {
1459
+ color: REQUIRED_MARKER_COLOR,
1460
+ marginLeft: REQUIRED_MARKER_GAP
1461
+ };
1462
+ var DPO_AUDIO_ELEMENT_MAP = [
1463
+ { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1464
+ { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1465
+ { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1466
+ { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1467
+ { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1468
+ { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1469
+ { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1470
+ { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1471
+ ];
1472
+
1473
+ // src/core/required-marker.ts
1474
+ function createRequiredMarker(label = REQUIRED_MARKER_ARIA_LABEL) {
1475
+ return el(
1476
+ "span",
1477
+ { style: REQUIRED_MARKER_STYLE, "aria-label": label },
1478
+ REQUIRED_MARKER_GLYPH
1479
+ );
1480
+ }
1481
+ function hasRequiredDataElement(purpose) {
1482
+ return purpose.data_elements.some((element) => element.required);
1483
+ }
1484
+
1406
1485
  // src/modal/purposes.ts
1407
1486
  function renderModalPurposes(purposes, options) {
1408
1487
  var _a, _b;
@@ -1468,6 +1547,9 @@ function renderModalPurposes(purposes, options) {
1468
1547
  }
1469
1548
  });
1470
1549
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
1550
+ if (hasRequiredDataElement(purpose)) {
1551
+ titleEl.appendChild(createRequiredMarker());
1552
+ }
1471
1553
  titleElements.set(purpose.uuid, titleEl);
1472
1554
  const descEl = el("h3", {
1473
1555
  id: `purpose-description-${purpose.uuid}`,
@@ -1535,9 +1617,7 @@ function renderModalPurposes(purposes, options) {
1535
1617
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
1536
1618
  dataElementLabels.set(combinedId, labelSpan);
1537
1619
  if (dataElement.required) {
1538
- const asterisk = el("span", { style: { color: "red", marginLeft: "4px" }, "aria-label": "required" });
1539
- asterisk.textContent = "*";
1540
- labelSpan.appendChild(asterisk);
1620
+ labelSpan.appendChild(createRequiredMarker());
1541
1621
  }
1542
1622
  let deRightElement;
1543
1623
  if (!isAlreadyConsented) {
@@ -1906,24 +1986,6 @@ var updateConsentState = (updates) => {
1906
1986
  }
1907
1987
  };
1908
1988
 
1909
- // src/constants.ts
1910
- var DEFAULT_LOGO_POSITION = "left";
1911
- var LOGO_POSITION_JUSTIFY = {
1912
- left: "flex-start",
1913
- center: "center",
1914
- right: "flex-end"
1915
- };
1916
- var DPO_AUDIO_ELEMENT_MAP = [
1917
- { audioKey: "grievance_text_audio_url", elementId: "dpo-grievance-text" },
1918
- { audioKey: "grievance_anchor_text_audio_url", elementId: "dpo-grievance-link" },
1919
- { audioKey: "grievance_email_connector_text_audio_url", elementId: "dpo-grievance-email-connector" },
1920
- { audioKey: "grievance_email_audio_url", elementId: "dpo-grievance-email" },
1921
- { audioKey: "dp_board_text_audio_url", elementId: "dpo-dp-board-text" },
1922
- { audioKey: "dp_board_anchor_text_audio_url", elementId: "dpo-dp-board-link" },
1923
- { audioKey: "dpo_text_audio_url", elementId: "dpo-dpo-text" },
1924
- { audioKey: "dpo_anchor_text_audio_url", elementId: "dpo-dpo-link" }
1925
- ];
1926
-
1927
1989
  // src/modal/tts.ts
1928
1990
  var TTSManager = class {
1929
1991
  /**
@@ -1977,7 +2039,8 @@ var TTSManager = class {
1977
2039
  signal: this.abortController.signal,
1978
2040
  token: params.token,
1979
2041
  sandboxUcic: params.sandboxUcic,
1980
- sandboxSubject: params.sandboxSubject,
2042
+ sandboxEmail: params.sandboxEmail,
2043
+ sandboxMobile: params.sandboxMobile,
1981
2044
  organisationUuid: params.organisationUuid,
1982
2045
  workspaceUuid: params.workspaceUuid
1983
2046
  });
@@ -2033,8 +2096,9 @@ var TTSManager = class {
2033
2096
  push(audioUrl, mapping.elementId);
2034
2097
  }
2035
2098
  }
2036
- push(urls.confirm_button_text_audio_url, "confirm-button-text");
2037
- push(urls.decline_button_text_audio_url, "decline-button-text");
2099
+ push(urls.accept_all_button_text_audio_url, ACCEPT_ALL_BUTTON_ID);
2100
+ push(urls.confirm_button_text_audio_url, ACCEPT_SELECTED_BUTTON_ID);
2101
+ push(urls.decline_button_text_audio_url, DECLINE_BUTTON_ID);
2038
2102
  this.audioSequence = audioSeq;
2039
2103
  this.elementIdSequence = elementSeq;
2040
2104
  }
@@ -2925,14 +2989,17 @@ function renderVerificationScreen(options) {
2925
2989
 
2926
2990
  // src/modal/colors.ts
2927
2991
  function resolveColors(settings, activeConfig) {
2928
- var _a, _b, _c, _d, _e, _f, _g, _h;
2992
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
2929
2993
  const primaryColor = (activeConfig == null ? void 0 : activeConfig.primary_color) || "#4f87ff";
2930
2994
  const secondaryColor = (activeConfig == null ? void 0 : activeConfig.secondary_color) || "#4f87ff";
2931
2995
  return {
2932
2996
  acceptBg: ((_b = (_a = settings == null ? void 0 : settings.button) == null ? void 0 : _a.accept) == null ? void 0 : _b.backgroundColor) || primaryColor,
2933
2997
  acceptText: ((_d = (_c = settings == null ? void 0 : settings.button) == null ? void 0 : _c.accept) == null ? void 0 : _d.textColor) || "#ffffff",
2934
- declineBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.decline) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2935
- declineText: ((_h = (_g = settings == null ? void 0 : settings.button) == null ? void 0 : _g.decline) == null ? void 0 : _h.textColor) || "#000000",
2998
+ acceptShortcutBg: ((_f = (_e = settings == null ? void 0 : settings.button) == null ? void 0 : _e.acceptAll) == null ? void 0 : _f.backgroundColor) || "#ffffff",
2999
+ acceptShortcutText: ((_h = (_g = settings == null ? void 0 : settings.button) == null ? void 0 : _g.acceptAll) == null ? void 0 : _h.textColor) || primaryColor,
3000
+ acceptShortcutBorder: primaryColor,
3001
+ declineBg: ((_j = (_i = settings == null ? void 0 : settings.button) == null ? void 0 : _i.decline) == null ? void 0 : _j.backgroundColor) || "#ffffff",
3002
+ declineText: ((_l = (_k = settings == null ? void 0 : settings.button) == null ? void 0 : _k.decline) == null ? void 0 : _l.textColor) || "#000000",
2936
3003
  declineBorder: (settings == null ? void 0 : settings.borderColor) || "#d0d5dd",
2937
3004
  link: (settings == null ? void 0 : settings.link) || secondaryColor,
2938
3005
  heading: (settings == null ? void 0 : settings.headingColor) || "#323B4B",
@@ -2944,6 +3011,23 @@ function resolveColors(settings, activeConfig) {
2944
3011
  };
2945
3012
  }
2946
3013
 
3014
+ // src/modal/consentSelection.ts
3015
+ var dataElementKey = (purposeUuid, elementUuid) => `${purposeUuid}-${elementUuid}`;
3016
+ var buildSelectionForMode = (purposes, mode, current) => {
3017
+ if (mode === "selected") {
3018
+ return current;
3019
+ }
3020
+ const selectedPurposes = {};
3021
+ const selectedDataElements = {};
3022
+ purposes.forEach((purpose) => {
3023
+ selectedPurposes[purpose.uuid] = true;
3024
+ purpose.data_elements.forEach((element) => {
3025
+ selectedDataElements[dataElementKey(purpose.uuid, element.uuid)] = element.enabled;
3026
+ });
3027
+ });
3028
+ return { selectedPurposes, selectedDataElements };
3029
+ };
3030
+
2947
3031
  // src/modal/layout.ts
2948
3032
  function resolveLogoPosition(raw) {
2949
3033
  return raw === "center" || raw === "right" ? raw : DEFAULT_LOGO_POSITION;
@@ -2976,6 +3060,7 @@ var ModalController = class {
2976
3060
  this.visibilityChangeHandler = null;
2977
3061
  // DOM refs for targeted updates
2978
3062
  this.acceptBtn = null;
3063
+ this.acceptAllBtn = null;
2979
3064
  this.declineBtn = null;
2980
3065
  this.middleSection = null;
2981
3066
  this.errorEl = null;
@@ -3058,18 +3143,24 @@ var ModalController = class {
3058
3143
  // ─── Private ───
3059
3144
  /**
3060
3145
  * Sandbox auth options to forward to every API call. When `token` is set the
3061
- * API layer sends the single `X-Consent-Token` header, injects the subject
3062
- * into each request's payload, and reads org/workspace from these fields
3063
- * instead of decoding the JWT. Empty in the normal JWT flow.
3146
+ * API layer sends the single `X-Consent-Token` header, carries every supplied
3147
+ * identifier in each request's payload, and reads org/workspace from these
3148
+ * fields instead of decoding the JWT. Empty in the normal JWT flow.
3064
3149
  */
3065
3150
  sandboxParams() {
3151
+ var _a;
3152
+ const legacySubject = (_a = this.options.sandboxSubject) == null ? void 0 : _a.trim();
3153
+ const legacyEmail = (legacySubject == null ? void 0 : legacySubject.includes("@")) ? legacySubject : void 0;
3154
+ const legacyMobile = legacySubject && !legacySubject.includes("@") ? legacySubject : void 0;
3066
3155
  return {
3067
3156
  token: this.options.token,
3068
- // The static token carries no identity; read the acting principal from the
3069
- // optional ucic/email/mobile. UCIC takes precedence server-side.
3070
- // sandboxUcic / sandboxSubject stay as fallbacks.
3157
+ // The static token carries no identity; carry every supplied identifier
3158
+ // (ucic/email/mobile) so the server can pick the principal by precedence
3159
+ // (UCIC > email > mobile) and keep the rest as the sandbox contact.
3160
+ // sandboxUcic / sandboxEmail / sandboxMobile stay as fallbacks.
3071
3161
  sandboxUcic: this.options.ucic || this.options.sandboxUcic,
3072
- sandboxSubject: this.options.email || this.options.mobile || this.options.sandboxSubject,
3162
+ sandboxEmail: this.options.email || this.options.sandboxEmail || legacyEmail,
3163
+ sandboxMobile: this.options.mobile || this.options.sandboxMobile || legacyMobile,
3073
3164
  organisationUuid: this.options.organisationUuid,
3074
3165
  workspaceUuid: this.options.workspaceUuid
3075
3166
  };
@@ -3670,24 +3761,57 @@ var ModalController = class {
3670
3761
  display: "none"
3671
3762
  }
3672
3763
  });
3673
- const acceptBtnText = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", activeConfig.confirm_button_text);
3764
+ const acceptBtnText = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText(
3765
+ "confirm_button_text",
3766
+ activeConfig.confirm_button_text || DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT
3767
+ );
3768
+ const gridButtonBox = {
3769
+ fontSize: isMobile ? "14px" : "16px",
3770
+ padding: state.isFullyConsentedReviewMode ? isMobile ? "10px 20px" : "9px 45px" : "9px 20px",
3771
+ width: state.isFullyConsentedReviewMode || isMobile ? "100%" : "auto",
3772
+ whiteSpace: "normal",
3773
+ wordBreak: "break-word"
3774
+ };
3674
3775
  const acceptStyle = {
3675
3776
  ...styles.button,
3676
- ...styles.acceptButton,
3677
- backgroundColor: colors.acceptBg,
3678
- color: colors.acceptText,
3777
+ ...state.isFullyConsentedReviewMode ? { ...styles.acceptButton, backgroundColor: colors.acceptBg, color: colors.acceptText } : {
3778
+ ...styles.cancelButton,
3779
+ backgroundColor: "#ffffff",
3780
+ color: colors.acceptShortcutText,
3781
+ borderColor: colors.acceptShortcutBorder
3782
+ },
3679
3783
  borderRadius: colors.borderRadius,
3680
- fontSize: isMobile ? "14px" : "16px",
3681
- padding: isMobile ? "10px 20px" : "9px 45px"
3784
+ ...gridButtonBox
3682
3785
  };
3683
3786
  this.acceptBtn = el("button", {
3684
- id: "confirm-button-text",
3787
+ id: ACCEPT_SELECTED_BUTTON_ID,
3685
3788
  style: acceptStyle,
3686
3789
  type: "button",
3687
- onClick: () => this.handleAccept()
3790
+ onClick: () => this.handleAccept("selected")
3688
3791
  });
3689
3792
  this.acceptBtn.textContent = acceptBtnText;
3690
- const declineBtnText = this.getTranslatedText("decline_button_text", activeConfig.decline_button_text);
3793
+ const shortcutStyle = {
3794
+ ...styles.button,
3795
+ ...styles.acceptButton,
3796
+ backgroundColor: colors.acceptBg,
3797
+ color: colors.acceptText,
3798
+ borderRadius: colors.borderRadius,
3799
+ ...gridButtonBox
3800
+ };
3801
+ this.acceptAllBtn = el("button", {
3802
+ id: ACCEPT_ALL_BUTTON_ID,
3803
+ style: shortcutStyle,
3804
+ type: "button",
3805
+ onClick: () => this.handleAccept("all")
3806
+ });
3807
+ this.acceptAllBtn.textContent = this.getTranslatedText(
3808
+ "accept_all_button_text",
3809
+ activeConfig.accept_all_button_text || DEFAULT_ACCEPT_ALL_BUTTON_TEXT
3810
+ );
3811
+ const declineBtnText = this.getTranslatedText(
3812
+ "decline_button_text",
3813
+ activeConfig.decline_button_text || DEFAULT_DECLINE_BUTTON_TEXT
3814
+ );
3691
3815
  const declineStyle = {
3692
3816
  ...styles.button,
3693
3817
  ...styles.cancelButton,
@@ -3695,11 +3819,13 @@ var ModalController = class {
3695
3819
  color: colors.declineText,
3696
3820
  borderColor: colors.declineBorder,
3697
3821
  borderRadius: colors.borderRadius,
3698
- fontSize: isMobile ? "14px" : "16px",
3699
- padding: isMobile ? "10px 20px" : "9px 45px"
3822
+ ...gridButtonBox,
3823
+ // Pushes everything after decline to the right edge, which is the whole
3824
+ // of the wide layout. Inert in the stack and in review mode.
3825
+ marginRight: state.isFullyConsentedReviewMode || isMobile ? "0" : "auto"
3700
3826
  };
3701
3827
  this.declineBtn = el("button", {
3702
- id: "decline-button-text",
3828
+ id: DECLINE_BUTTON_ID,
3703
3829
  style: declineStyle,
3704
3830
  type: "button",
3705
3831
  onClick: () => this.handleDecline()
@@ -3707,12 +3833,19 @@ var ModalController = class {
3707
3833
  if (!state.isFullyConsentedReviewMode) {
3708
3834
  this.declineBtn.textContent = declineBtnText;
3709
3835
  }
3710
- const bottomSection = el("div", { style: styles.bottomSection });
3711
- if (!state.isFullyConsentedReviewMode) {
3836
+ const bottomSection = el("div", {
3837
+ style: state.isFullyConsentedReviewMode ? styles.bottomSection : isMobile ? styles.actionButtonStack : styles.actionButtonRow
3838
+ });
3839
+ if (state.isFullyConsentedReviewMode) {
3840
+ bottomSection.appendChild(this.acceptBtn);
3841
+ } else if (isMobile) {
3842
+ bottomSection.appendChild(this.acceptAllBtn);
3712
3843
  bottomSection.appendChild(this.acceptBtn);
3713
3844
  bottomSection.appendChild(this.declineBtn);
3714
3845
  } else {
3846
+ bottomSection.appendChild(this.declineBtn);
3715
3847
  bottomSection.appendChild(this.acceptBtn);
3848
+ bottomSection.appendChild(this.acceptAllBtn);
3716
3849
  }
3717
3850
  const contentWrapper = el(
3718
3851
  "div",
@@ -3868,7 +4001,12 @@ var ModalController = class {
3868
4001
  selectedPurposes: { ...state.selectedPurposes, [purposeUuid]: shouldCheckPurpose }
3869
4002
  });
3870
4003
  }
3871
- async handleAccept() {
4004
+ /**
4005
+ * Shared by all three accept buttons. The mode only decides which
4006
+ * purposes/data elements are submitted — everything downstream, including
4007
+ * the host's `onAccept` callback, is identical.
4008
+ */
4009
+ async handleAccept(mode = "selected") {
3872
4010
  var _a, _b, _c;
3873
4011
  const state = this.store.getState();
3874
4012
  if (state.isSubmitting) return;
@@ -3887,6 +4025,16 @@ var ModalController = class {
3887
4025
  if (!content) return;
3888
4026
  this.abortController = new AbortController();
3889
4027
  this.store.setState({ isSubmitting: true, isSubmittingAccept: true, errorMessage: null });
4028
+ const selection = buildSelectionForMode(content.purposes, mode, {
4029
+ selectedPurposes: state.selectedPurposes,
4030
+ selectedDataElements: state.selectedDataElements
4031
+ });
4032
+ if (mode !== "selected") {
4033
+ this.store.setState({
4034
+ selectedPurposes: selection.selectedPurposes,
4035
+ selectedDataElements: selection.selectedDataElements
4036
+ });
4037
+ }
3890
4038
  try {
3891
4039
  const purposes = content.purposes.map((purpose) => {
3892
4040
  const dataElements = purpose.data_elements.filter((de) => de.enabled).map((de) => ({
@@ -3894,13 +4042,13 @@ var ModalController = class {
3894
4042
  name: de.name,
3895
4043
  enabled: de.enabled,
3896
4044
  required: de.required,
3897
- selected: state.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
4045
+ selected: selection.selectedDataElements[`${purpose.uuid}-${de.uuid}`] || false
3898
4046
  }));
3899
4047
  return {
3900
4048
  uuid: purpose.uuid,
3901
4049
  name: purpose.name,
3902
4050
  description: purpose.description,
3903
- selected: state.selectedPurposes[purpose.uuid] || false,
4051
+ selected: selection.selectedPurposes[purpose.uuid] || false,
3904
4052
  data_elements: dataElements
3905
4053
  };
3906
4054
  });
@@ -3962,11 +4110,16 @@ var ModalController = class {
3962
4110
  }
3963
4111
  updateAcceptButtonState(state) {
3964
4112
  if (!this.acceptBtn || state.isFullyConsentedReviewMode) return;
4113
+ const submitting = state.isSubmittingAccept || state.isSubmittingDecline;
3965
4114
  const allRequiredChecked = this.areAllRequiredChecked(state);
3966
- const disabled = !allRequiredChecked || state.isSubmittingAccept || state.isSubmittingDecline;
3967
- this.acceptBtn.disabled = disabled;
3968
- this.acceptBtn.style.opacity = disabled ? "0.5" : "1";
3969
- this.acceptBtn.style.cursor = disabled ? "not-allowed" : "pointer";
4115
+ const setDisabled = (btn, disabled) => {
4116
+ if (!btn) return;
4117
+ btn.disabled = disabled;
4118
+ btn.style.opacity = disabled ? "0.5" : "1";
4119
+ btn.style.cursor = disabled ? "not-allowed" : "pointer";
4120
+ };
4121
+ setDisabled(this.acceptBtn, !allRequiredChecked || submitting);
4122
+ setDisabled(this.acceptAllBtn, submitting);
3970
4123
  }
3971
4124
  updateCollapseState(state) {
3972
4125
  const updateRefs = (refs) => {
@@ -4339,25 +4492,34 @@ var ModalController = class {
4339
4492
  }
4340
4493
  updateButtonStates(state) {
4341
4494
  var _a;
4342
- if (this.acceptBtn) {
4343
- if (state.isSubmittingAccept) {
4344
- this.acceptBtn.textContent = "Submitting...";
4345
- this.acceptBtn.disabled = true;
4346
- this.acceptBtn.style.opacity = "0.7";
4347
- this.acceptBtn.style.cursor = "not-allowed";
4348
- } else {
4349
- const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4350
- this.acceptBtn.textContent = state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText("confirm_button_text", (content == null ? void 0 : content.confirm_button_text) || "Accept");
4351
- this.acceptBtn.disabled = false;
4352
- this.acceptBtn.style.opacity = "1";
4353
- this.acceptBtn.style.cursor = "pointer";
4354
- }
4355
- }
4495
+ const content = (_a = state.content) == null ? void 0 : _a.detail.active_config;
4496
+ const applySubmittingState = (btn, idleLabel) => {
4497
+ if (!btn) return;
4498
+ btn.textContent = state.isSubmittingAccept ? SUBMITTING_BUTTON_TEXT : idleLabel;
4499
+ btn.disabled = state.isSubmittingAccept;
4500
+ btn.style.opacity = state.isSubmittingAccept ? "0.7" : "1";
4501
+ btn.style.cursor = state.isSubmittingAccept ? "not-allowed" : "pointer";
4502
+ };
4503
+ applySubmittingState(
4504
+ this.acceptBtn,
4505
+ state.isFullyConsentedReviewMode ? this.options.reviewModeButtonText || "Continue" : this.getTranslatedText(
4506
+ "confirm_button_text",
4507
+ (content == null ? void 0 : content.confirm_button_text) || DEFAULT_ACCEPT_SELECTED_BUTTON_TEXT
4508
+ )
4509
+ );
4510
+ applySubmittingState(
4511
+ this.acceptAllBtn,
4512
+ this.getTranslatedText(
4513
+ "accept_all_button_text",
4514
+ (content == null ? void 0 : content.accept_all_button_text) || DEFAULT_ACCEPT_ALL_BUTTON_TEXT
4515
+ )
4516
+ );
4356
4517
  if (this.declineBtn) {
4357
4518
  this.declineBtn.disabled = state.isSubmitting;
4358
4519
  this.declineBtn.style.opacity = state.isSubmitting ? "0.7" : "1";
4359
4520
  this.declineBtn.style.cursor = state.isSubmitting ? "not-allowed" : "pointer";
4360
4521
  }
4522
+ this.updateAcceptButtonState(state);
4361
4523
  }
4362
4524
  };
4363
4525
 
@@ -4558,6 +4720,9 @@ function renderInlinePurposes(purposes, options) {
4558
4720
  }
4559
4721
  });
4560
4722
  titleEl.textContent = getTranslatedText("purposes.name", purpose.name, purpose.uuid);
4723
+ if (hasRequiredDataElement(purpose)) {
4724
+ titleEl.appendChild(createRequiredMarker());
4725
+ }
4561
4726
  titleElements.set(purpose.uuid, titleEl);
4562
4727
  const descEl = el("h3", {
4563
4728
  style: {
@@ -4610,12 +4775,7 @@ function renderInlinePurposes(purposes, options) {
4610
4775
  labelSpan.textContent = getTranslatedText("data_elements.name", dataElement.name, dataElement.uuid);
4611
4776
  dataElementLabels.set(combinedId, labelSpan);
4612
4777
  if (dataElement.required) {
4613
- const asterisk = el("span", {
4614
- style: { color: "red", marginLeft: "4px" },
4615
- "aria-label": "required"
4616
- });
4617
- asterisk.textContent = "*";
4618
- labelSpan.appendChild(asterisk);
4778
+ labelSpan.appendChild(createRequiredMarker());
4619
4779
  }
4620
4780
  const deCheckbox = el("input", {
4621
4781
  type: "checkbox",