@certrev/cert-block 1.1.0 → 1.1.1

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 (42) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -6
  3. package/dist/components/render-cert-block.d.ts.map +1 -1
  4. package/dist/components/render-cert-block.js +3 -6
  5. package/dist/components/render-cert-block.js.map +1 -1
  6. package/dist/modal/certrev-cert-modal.d.ts +34 -1
  7. package/dist/modal/certrev-cert-modal.d.ts.map +1 -1
  8. package/dist/modal/certrev-cert-modal.js +40 -2
  9. package/dist/modal/certrev-cert-modal.js.map +1 -1
  10. package/dist/modal/certrev-cert.entry.d.ts.map +1 -1
  11. package/dist/modal/certrev-cert.entry.js +2 -4
  12. package/dist/modal/certrev-cert.entry.js.map +1 -1
  13. package/dist/modal/certrev-cert.js +4 -4
  14. package/dist/modal/deprecated.d.ts +15 -0
  15. package/dist/modal/deprecated.d.ts.map +1 -0
  16. package/dist/modal/deprecated.js +22 -0
  17. package/dist/modal/deprecated.js.map +1 -0
  18. package/dist/modal/ftc-disclosure.d.ts +7 -3
  19. package/dist/modal/ftc-disclosure.d.ts.map +1 -1
  20. package/dist/modal/ftc-disclosure.js +7 -3
  21. package/dist/modal/ftc-disclosure.js.map +1 -1
  22. package/dist/modal/index.d.ts +4 -3
  23. package/dist/modal/index.d.ts.map +1 -1
  24. package/dist/modal/index.js +4 -3
  25. package/dist/modal/index.js.map +1 -1
  26. package/dist/modal/interactions.d.ts +20 -4
  27. package/dist/modal/interactions.d.ts.map +1 -1
  28. package/dist/modal/interactions.js +34 -7
  29. package/dist/modal/interactions.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/render-cert-block.ts +3 -6
  32. package/src/modal/certrev-cert-modal.ts +45 -2
  33. package/src/modal/certrev-cert.entry.ts +2 -4
  34. package/src/modal/deprecated.ts +25 -0
  35. package/src/modal/ftc-disclosure.ts +7 -3
  36. package/src/modal/index.ts +4 -3
  37. package/src/modal/interactions.ts +37 -7
  38. package/dist/modal/ftc-guard.d.ts +0 -85
  39. package/dist/modal/ftc-guard.d.ts.map +0 -1
  40. package/dist/modal/ftc-guard.js +0 -150
  41. package/dist/modal/ftc-guard.js.map +0 -1
  42. package/src/modal/ftc-guard.ts +0 -174
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public names kept only so a patch release never breaks a compiling integration (the semver
3
+ * contract at the head of CHANGELOG.md). Each one does nothing it used to do; removal is for 2.0.
4
+ */
5
+ /** Dispatched on the document, once per document, on the first call of a removed API, so a caller
6
+ * that still relies on it can see that it does nothing. `detail: { api, removedIn }`. */
7
+ export declare const DEPRECATED_API_EVENT = "certrev:deprecated-api";
8
+ /**
9
+ * @deprecated Removed in 1.1.1: the runtime FTC disclosure guard is gone. This installs nothing,
10
+ * checks nothing and never hides or replaces a face; whether a face shows its disclosure is settled
11
+ * where the face is configured. The first call per document dispatches {@link DEPRECATED_API_EVENT}.
12
+ * Will be deleted in 2.0.
13
+ */
14
+ export declare function installFtcGuard(doc: Document): void;
15
+ //# sourceMappingURL=deprecated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deprecated.d.ts","sourceRoot":"","sources":["../../src/modal/deprecated.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;0FAC0F;AAC1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAA;AAK5D;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAMnD"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Public names kept only so a patch release never breaks a compiling integration (the semver
3
+ * contract at the head of CHANGELOG.md). Each one does nothing it used to do; removal is for 2.0.
4
+ */
5
+ /** Dispatched on the document, once per document, on the first call of a removed API, so a caller
6
+ * that still relies on it can see that it does nothing. `detail: { api, removedIn }`. */
7
+ export const DEPRECATED_API_EVENT = 'certrev:deprecated-api';
8
+ /** Documents that have already been told, so repeated calls dispatch once. */
9
+ const announcedDocs = new WeakSet();
10
+ /**
11
+ * @deprecated Removed in 1.1.1: the runtime FTC disclosure guard is gone. This installs nothing,
12
+ * checks nothing and never hides or replaces a face; whether a face shows its disclosure is settled
13
+ * where the face is configured. The first call per document dispatches {@link DEPRECATED_API_EVENT}.
14
+ * Will be deleted in 2.0.
15
+ */
16
+ export function installFtcGuard(doc) {
17
+ if (announcedDocs.has(doc))
18
+ return;
19
+ announcedDocs.add(doc);
20
+ doc.dispatchEvent(new CustomEvent(DEPRECATED_API_EVENT, { detail: { api: 'installFtcGuard', removedIn: '1.1.1' } }));
21
+ }
22
+ //# sourceMappingURL=deprecated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deprecated.js","sourceRoot":"","sources":["../../src/modal/deprecated.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;0FAC0F;AAC1F,MAAM,CAAC,MAAM,oBAAoB,GAAG,wBAAwB,CAAA;AAE5D,8EAA8E;AAC9E,MAAM,aAAa,GAAG,IAAI,OAAO,EAAY,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAa;IAC5C,IAAI,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAM;IAClC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACtB,GAAG,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,oBAAoB,EAAE,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,CAAC,CACjG,CAAA;AACF,CAAC"}
@@ -4,9 +4,13 @@
4
4
  * module. Legally load-bearing: the curly apostrophe (U+2019) in "article’s"
5
5
  * is byte-exact; a reword is a contract divergence.
6
6
  *
7
- * Kept as its OWN module (imported by both ftc-guard + display-strings, exactly as in portal) so
8
- * the browser bundle emits the same `var` structure as the deployed certrev-cert.js — do not merge
9
- * it into display-strings.ts (that consolidation drops one `var` and breaks byte-parity).
7
+ * Kept as its OWN module (imported by display-strings, exactly as in portal) so the browser bundle
8
+ * emits the same `var` structure as the deployed certrev-cert.js — do not merge it into
9
+ * display-strings.ts (that consolidation drops one `var` and breaks byte-parity).
10
+ *
11
+ * It is the text the faces RENDER, nothing more. Since 1.1.1 nothing in this package checks at
12
+ * runtime that it is present or visible (the `ftc-guard` kill switch is gone): whether a face shows
13
+ * its disclosure is settled where the face is configured, not by hiding or replacing it in the page.
10
14
  */
11
15
  export declare const FTC_DISCLOSURE_LINE: "Independent editorial review of this article\u2019s text and sources. Not a product endorsement, nor medical advice.";
12
16
  //# sourceMappingURL=ftc-disclosure.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ftc-disclosure.d.ts","sourceRoot":"","sources":["../../src/modal/ftc-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,mBAAmB,EAC/B,sHAA0H,CAAC"}
1
+ {"version":3,"file":"ftc-disclosure.d.ts","sourceRoot":"","sources":["../../src/modal/ftc-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,mBAAmB,EAC/B,sHAA0H,CAAC"}
@@ -4,9 +4,13 @@
4
4
  * module. Legally load-bearing: the curly apostrophe (U+2019) in "article’s"
5
5
  * is byte-exact; a reword is a contract divergence.
6
6
  *
7
- * Kept as its OWN module (imported by both ftc-guard + display-strings, exactly as in portal) so
8
- * the browser bundle emits the same `var` structure as the deployed certrev-cert.js — do not merge
9
- * it into display-strings.ts (that consolidation drops one `var` and breaks byte-parity).
7
+ * Kept as its OWN module (imported by display-strings, exactly as in portal) so the browser bundle
8
+ * emits the same `var` structure as the deployed certrev-cert.js — do not merge it into
9
+ * display-strings.ts (that consolidation drops one `var` and breaks byte-parity).
10
+ *
11
+ * It is the text the faces RENDER, nothing more. Since 1.1.1 nothing in this package checks at
12
+ * runtime that it is present or visible (the `ftc-guard` kill switch is gone): whether a face shows
13
+ * its disclosure is settled where the face is configured, not by hiding or replacing it in the page.
10
14
  */
11
15
  export const FTC_DISCLOSURE_LINE = "Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.";
12
16
  //# sourceMappingURL=ftc-disclosure.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ftc-disclosure.js","sourceRoot":"","sources":["../../src/modal/ftc-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAC/B,iHAA0H,CAAC"}
1
+ {"version":3,"file":"ftc-disclosure.js","sourceRoot":"","sources":["../../src/modal/ftc-disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAC/B,iHAA0H,CAAC"}
@@ -6,13 +6,14 @@
6
6
  * storefront (`<script src=".../dist/modal/certrev-cert.js">`) instead of vendoring a copy.
7
7
  *
8
8
  * Importing this module as ESM has the SAME side effect as loading the IIFE: it registers the
9
- * `<certrev-cert-modal>` element and (in a browser) runs placement + interactions + FTC guard +
10
- * badge revalidation. Bundler users: `import '@certrev/cert-block/modal'`.
9
+ * `<certrev-cert-modal>` element and (in a browser) runs placement + interactions + badge
10
+ * revalidation (no FTC guard since 1.1.1). Bundler users: `import '@certrev/cert-block/modal'`.
11
11
  */
12
12
  import "./certrev-cert.entry.js";
13
13
  export { CERT_ISSUER_KID, CERT_ISSUER_PUBLIC_KEY_PEM, } from "./certrev-cert.entry.js";
14
14
  export { CERT_MODAL_TAG } from "./certrev-cert-modal.js";
15
- export { installFtcGuard } from "./ftc-guard.js";
15
+ /** @deprecated Since 1.1.1 a no-op (the runtime FTC guard was removed); deleted in 2.0. */
16
+ export { installFtcGuard } from "./deprecated.js";
16
17
  export { initCertInteractionsOnReady } from "./interactions.js";
17
18
  export { initCertPlacement } from "./placement.js";
18
19
  export { initBadgeRevalidation, type RevalidateDeps, revalidateBadge, } from "./revalidate.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/modal/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,yBAAyB,CAAC;AAKjC,OAAO,EACN,eAAe,EACf,0BAA0B,GAC1B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EACN,qBAAqB,EACrB,KAAK,cAAc,EACnB,eAAe,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/modal/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,yBAAyB,CAAC;AAKjC,OAAO,EACN,eAAe,EACf,0BAA0B,GAC1B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,2FAA2F;AAC3F,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EACN,qBAAqB,EACrB,KAAK,cAAc,EACnB,eAAe,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -6,8 +6,8 @@
6
6
  * storefront (`<script src=".../dist/modal/certrev-cert.js">`) instead of vendoring a copy.
7
7
  *
8
8
  * Importing this module as ESM has the SAME side effect as loading the IIFE: it registers the
9
- * `<certrev-cert-modal>` element and (in a browser) runs placement + interactions + FTC guard +
10
- * badge revalidation. Bundler users: `import '@certrev/cert-block/modal'`.
9
+ * `<certrev-cert-modal>` element and (in a browser) runs placement + interactions + badge
10
+ * revalidation (no FTC guard since 1.1.1). Bundler users: `import '@certrev/cert-block/modal'`.
11
11
  */
12
12
  // Side-effect: register the element + (in a browser) kick off the DOM passes.
13
13
  import "./certrev-cert.entry.js";
@@ -17,7 +17,8 @@ import "./certrev-cert.entry.js";
17
17
  export { CERT_ISSUER_KID, CERT_ISSUER_PUBLIC_KEY_PEM, } from "./certrev-cert.entry.js";
18
18
  // Composable entry points for advanced consumers (the IIFE wires these itself).
19
19
  export { CERT_MODAL_TAG } from "./certrev-cert-modal.js";
20
- export { installFtcGuard } from "./ftc-guard.js";
20
+ /** @deprecated Since 1.1.1 a no-op (the runtime FTC guard was removed); deleted in 2.0. */
21
+ export { installFtcGuard } from "./deprecated.js";
21
22
  export { initCertInteractionsOnReady } from "./interactions.js";
22
23
  export { initCertPlacement } from "./placement.js";
23
24
  export { initBadgeRevalidation, revalidateBadge, } from "./revalidate.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/modal/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8EAA8E;AAC9E,OAAO,yBAAyB,CAAC;AAEjC,+FAA+F;AAC/F,iGAAiG;AACjG,0FAA0F;AAC1F,OAAO,EACN,eAAe,EACf,0BAA0B,GAC1B,MAAM,yBAAyB,CAAC;AAEjC,gFAAgF;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EACN,qBAAqB,EAErB,eAAe,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/modal/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8EAA8E;AAC9E,OAAO,yBAAyB,CAAC;AAEjC,+FAA+F;AAC/F,iGAAiG;AACjG,0FAA0F;AAC1F,OAAO,EACN,eAAe,EACf,0BAA0B,GAC1B,MAAM,yBAAyB,CAAC;AAEjC,gFAAgF;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,2FAA2F;AAC3F,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EACN,qBAAqB,EAErB,eAAe,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -8,8 +8,8 @@
8
8
  * (revealing the row's `.acc-bio` in place; the name caret rotates via CSS) and mirrors the
9
9
  * state on `aria-expanded`. A click on a link inside the row (e.g. the "Compensated expert"
10
10
  * cue) is NOT a toggle. Keyboard: Enter/Space on the role=button row.
11
- * • In-page modals: a `[data-certrev-modal-open]` control opens the unified
12
- * `<certrev-cert-modal>` element via its `.open('cert'|'expert')` method. Bare /
11
+ * • In-page modals: a `[data-certrev-modal-open]` control opens its OWN face's
12
+ * `<certrev-cert-modal>` element (`ownCertModal`) via its `.open('cert'|'expert')` method. Bare /
13
13
  * `"cert"` opens the certificate dialog; `"expert"` opens the reviewer dialog. With no JS / no
14
14
  * element / no envelope a link control falls back to its href (the certificate or the CertREV
15
15
  * profile page). The element owns its OWN close plumbing (× / Close / backdrop / Esc) inside its
@@ -21,6 +21,12 @@ export declare const ACC_ATTR = "data-certrev-acc";
21
21
  export declare const MODAL_OPEN_ATTR = "data-certrev-modal-open";
22
22
  export declare const INITIALS_ATTR = "data-certrev-initials";
23
23
  export declare const CARD_SELECTOR = ".certrev-card";
24
+ /** Stamped on a control that could not tell which modal is its own (value: the reason), beside the
25
+ * bubbling {@link MODAL_UNRESOLVED_EVENT}, so the miss is inspectable rather than a silent link. */
26
+ export declare const MODAL_UNRESOLVED_ATTR = "data-certrev-modal-unresolved";
27
+ /** Dispatched on a control whose face has no modal of its own but sits between other faces'
28
+ * (`ownCertModal` → `ambiguous`). `detail: { reason: 'ambiguous', modals }`. */
29
+ export declare const MODAL_UNRESOLVED_EVENT = "certrev:modal-unresolved";
24
30
  /** The dialog a `data-certrev-modal-open` control targets. */
25
31
  export type ModalKind = 'cert' | 'expert';
26
32
  /** Toggle a contributor row's accordion bio open/closed (the `.open` class drives the CSS reveal +
@@ -29,11 +35,21 @@ export declare function toggleAccordion(row: Element): void;
29
35
  /** Resolve a `data-certrev-modal-open` value to the dialog it opens. `"expert"` opens the reviewer
30
36
  * modal; anything else (the bare attribute, or `"cert"`) opens the certificate modal. */
31
37
  export declare function modalTargetKind(value: string | null): ModalKind;
32
- /** Open a cert dialog via the `<certrev-cert-modal>` element. Returns false (so the caller lets an
33
- * href fall back) when the element is absent or declines (no envelope + no fetch source). */
38
+ /** Open a cert dialog via the document's FIRST `<certrev-cert-modal>` element: a page-level helper
39
+ * for a page with one face. Controls go through `openOwnModal`, which finds their own face's element.
40
+ * Returns false (so the caller lets an href fall back) when the element is absent or declines (no
41
+ * envelope + no fetch source). */
34
42
  export declare function openModal(doc: Document, kind: ModalKind): boolean;
35
43
  /** Open the page's certificate modal (backward-compatible wrapper over openModal). */
36
44
  export declare function openCertModal(doc: Document): boolean;
45
+ /**
46
+ * Open a dialog of `control`'s OWN face's `<certrev-cert-modal>` (see `ownCertModal`), which is what
47
+ * the delegated click / key handlers do. Returns false (so a link control follows its own href) when
48
+ * the face has no modal, the element declines, or the control cannot tell which modal is its own; in
49
+ * that last case it is stamped `data-certrev-modal-unresolved="ambiguous"` and receives
50
+ * `certrev:modal-unresolved`, and another face's certificate is never opened in its place.
51
+ */
52
+ export declare function openOwnModal(control: Element, kind: ModalKind): boolean;
37
53
  /**
38
54
  * Replace a broken avatar `<img>` with its initials placeholder — the SAME markup the Liquid emits
39
55
  * when no photo URL is present. The SSR can't know a present URL will 404 (the canary's expert photo
@@ -1 +1 @@
1
- {"version":3,"file":"interactions.d.ts","sourceRoot":"","sources":["../../src/modal/interactions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,eAAO,MAAM,QAAQ,qBAAqB,CAAA;AAC1C,eAAO,MAAM,eAAe,4BAA4B,CAAA;AACxD,eAAO,MAAM,aAAa,0BAA0B,CAAA;AACpD,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,8DAA8D;AAC9D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAczC;6DAC6D;AAC7D,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAGlD;AAED;0FAC0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAE/D;AAED;8FAC8F;AAC9F,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAIjE;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAEpD;AAMD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAU5D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAItD;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAyDxD;AAED,qDAAqD;AACrD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAI/D"}
1
+ {"version":3,"file":"interactions.d.ts","sourceRoot":"","sources":["../../src/modal/interactions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,eAAO,MAAM,QAAQ,qBAAqB,CAAA;AAC1C,eAAO,MAAM,eAAe,4BAA4B,CAAA;AACxD,eAAO,MAAM,aAAa,0BAA0B,CAAA;AACpD,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAC5C;qGACqG;AACrG,eAAO,MAAM,qBAAqB,kCAAkC,CAAA;AACpE;iFACiF;AACjF,eAAO,MAAM,sBAAsB,6BAA6B,CAAA;AAEhE,8DAA8D;AAC9D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAczC;6DAC6D;AAC7D,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAGlD;AAED;0FAC0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAE/D;AAED;;;mCAGmC;AACnC,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAIjE;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAavE;AAMD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAU5D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAItD;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAyDxD;AAED,qDAAqD;AACrD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAI/D"}
@@ -8,8 +8,8 @@
8
8
  * (revealing the row's `.acc-bio` in place; the name caret rotates via CSS) and mirrors the
9
9
  * state on `aria-expanded`. A click on a link inside the row (e.g. the "Compensated expert"
10
10
  * cue) is NOT a toggle. Keyboard: Enter/Space on the role=button row.
11
- * • In-page modals: a `[data-certrev-modal-open]` control opens the unified
12
- * `<certrev-cert-modal>` element via its `.open('cert'|'expert')` method. Bare /
11
+ * • In-page modals: a `[data-certrev-modal-open]` control opens its OWN face's
12
+ * `<certrev-cert-modal>` element (`ownCertModal`) via its `.open('cert'|'expert')` method. Bare /
13
13
  * `"cert"` opens the certificate dialog; `"expert"` opens the reviewer dialog. With no JS / no
14
14
  * element / no envelope a link control falls back to its href (the certificate or the CertREV
15
15
  * profile page). The element owns its OWN close plumbing (× / Close / backdrop / Esc) inside its
@@ -17,11 +17,17 @@
17
17
  *
18
18
  * No crypto, no network — pure DOM. Idempotent install (guarded so a double-load can't double-bind).
19
19
  */
20
- import { CERT_MODAL_TAG } from './certrev-cert-modal.js';
20
+ import { CERT_MODAL_TAG, ownCertModal } from './certrev-cert-modal.js';
21
21
  export const ACC_ATTR = 'data-certrev-acc';
22
22
  export const MODAL_OPEN_ATTR = 'data-certrev-modal-open';
23
23
  export const INITIALS_ATTR = 'data-certrev-initials';
24
24
  export const CARD_SELECTOR = '.certrev-card';
25
+ /** Stamped on a control that could not tell which modal is its own (value: the reason), beside the
26
+ * bubbling {@link MODAL_UNRESOLVED_EVENT}, so the miss is inspectable rather than a silent link. */
27
+ export const MODAL_UNRESOLVED_ATTR = 'data-certrev-modal-unresolved';
28
+ /** Dispatched on a control whose face has no modal of its own but sits between other faces'
29
+ * (`ownCertModal` → `ambiguous`). `detail: { reason: 'ambiguous', modals }`. */
30
+ export const MODAL_UNRESOLVED_EVENT = 'certrev:modal-unresolved';
25
31
  /** Avatar base classes whose `--initials` variant is the SSR no-photo fallback (kept in lockstep
26
32
  * with certrev-badge.liquid / certrev-memo.liquid). A broken <img> is swapped to `{base} {base}--initials`. */
27
33
  const AVATAR_BASE_CLASSES = ['avatar', 'certrev-memo__avatar'];
@@ -38,8 +44,10 @@ export function toggleAccordion(row) {
38
44
  export function modalTargetKind(value) {
39
45
  return value === 'expert' ? 'expert' : 'cert';
40
46
  }
41
- /** Open a cert dialog via the `<certrev-cert-modal>` element. Returns false (so the caller lets an
42
- * href fall back) when the element is absent or declines (no envelope + no fetch source). */
47
+ /** Open a cert dialog via the document's FIRST `<certrev-cert-modal>` element: a page-level helper
48
+ * for a page with one face. Controls go through `openOwnModal`, which finds their own face's element.
49
+ * Returns false (so the caller lets an href fall back) when the element is absent or declines (no
50
+ * envelope + no fetch source). */
43
51
  export function openModal(doc, kind) {
44
52
  const el = doc.querySelector(CERT_MODAL_TAG);
45
53
  if (el && typeof el.open === 'function')
@@ -50,6 +58,25 @@ export function openModal(doc, kind) {
50
58
  export function openCertModal(doc) {
51
59
  return openModal(doc, 'cert');
52
60
  }
61
+ /**
62
+ * Open a dialog of `control`'s OWN face's `<certrev-cert-modal>` (see `ownCertModal`), which is what
63
+ * the delegated click / key handlers do. Returns false (so a link control follows its own href) when
64
+ * the face has no modal, the element declines, or the control cannot tell which modal is its own; in
65
+ * that last case it is stamped `data-certrev-modal-unresolved="ambiguous"` and receives
66
+ * `certrev:modal-unresolved`, and another face's certificate is never opened in its place.
67
+ */
68
+ export function openOwnModal(control, kind) {
69
+ const own = ownCertModal(control);
70
+ if (own.modal) {
71
+ const el = own.modal;
72
+ return typeof el.open === 'function' ? el.open(kind) : false;
73
+ }
74
+ if (own.reason === 'ambiguous') {
75
+ control.setAttribute(MODAL_UNRESOLVED_ATTR, own.reason);
76
+ control.dispatchEvent(new CustomEvent(MODAL_UNRESOLVED_EVENT, { bubbles: true, detail: { reason: own.reason, modals: own.count } }));
77
+ }
78
+ return false;
79
+ }
53
80
  function asElement(target) {
54
81
  return target && target.closest ? target : null;
55
82
  }
@@ -95,7 +122,7 @@ export function initCertInteractions(doc) {
95
122
  // (Close is owned by the <certrev-cert-modal> element inside its shadow root.)
96
123
  const opener = el.closest(`[${MODAL_OPEN_ATTR}]`);
97
124
  if (opener) {
98
- if (openModal(doc, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR))))
125
+ if (openOwnModal(opener, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR))))
99
126
  e.preventDefault();
100
127
  return;
101
128
  }
@@ -124,7 +151,7 @@ export function initCertInteractions(doc) {
124
151
  return;
125
152
  const opener = el.closest(`[${MODAL_OPEN_ATTR}]`);
126
153
  if (opener) {
127
- if (openModal(doc, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR)))) {
154
+ if (openOwnModal(opener, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR)))) {
128
155
  e.preventDefault();
129
156
  return;
130
157
  }
@@ -1 +1 @@
1
- {"version":3,"file":"interactions.js","sourceRoot":"","sources":["../../src/modal/interactions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"interactions.js","sourceRoot":"","sources":["../../src/modal/interactions.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certrev/cert-block",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Headless render edge for the CertREV CertDeliveryEnvelope: SSR-safe React cert components, a framework-agnostic <certrev-badge> Web Component, a schema.org JSON-LD projector, and a fail-closed verify layer over the shared VerdictKernel.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -753,12 +753,9 @@ function floatingPill(facts: CertBlockFacts, placed: PlacedTest): string {
753
753
  // `data-certrev-*`-hooked building blocks the locked layouts use, so the custom face
754
754
  // carries the same markers as banner/sidebar and the crawl monitor reads them alike.
755
755
  //
756
- // NOT the ftc-guard, though — and the comment here claimed otherwise for as long as the
757
- // face has existed. `modal/ftc-guard.ts` looks for `data-ftc-line` / `data-ftc-disclosure`
758
- // hooks, which `CertBadge.tsx` and `render-badge-html.ts` emit and this renderer emits at
759
- // NONE of its three scope-line sites. The kill switch therefore cannot see anything
760
- // renderCertBlock produces, on ANY face. Whether it should is a live design question
761
- // (which surfaces the switch covers) — not something to settle by quietly adding hooks.
756
+ // No runtime guard watches any of it: the `modal/ftc-guard.ts` kill switch (which never saw
757
+ // this renderer's output anyway: it keyed on `.certrev-memo`) was removed in 1.1.1. Whether a
758
+ // face shows its scope line is decided by its configuration, not policed in the page.
762
759
  // ─────────────────────────────────────────────────────────────────────────────
763
760
 
764
761
  function renderCustomFace(facts: CertBlockFacts, placed: PlacedTest, responsive = false): string {
@@ -23,6 +23,9 @@
23
23
  * so the document-level delegate can't see the close controls.
24
24
  * - Fonts inject at document level on first open (font faces don't scope into a
25
25
  * shadow root); deferring to first open avoids loading them on every article view.
26
+ * - A page can carry several faces, each with its own element (a WordPress Query Loop, a
27
+ * related-posts block). A trigger opens its OWN face's element (`ownCertModal`), never the
28
+ * document's first one, and a suppressed element hides only its own face's triggers.
26
29
  */
27
30
 
28
31
  import { type CertDeliveryArtifact, isTombstone } from '@certrev/cert-contract'
@@ -33,6 +36,44 @@ import { CERT_MODAL_CSS } from './certrev-cert-modal.styles.js'
33
36
  export const CERT_MODAL_TAG = 'certrev-cert-modal'
34
37
  type ModalKind = 'cert' | 'expert'
35
38
 
39
+ /**
40
+ * One rendered face on a surface that can print several per page: the WordPress plugin wraps each
41
+ * face's card, memo and rail wraps in `<div data-certrev-cert="1">`. A trigger never reaches a
42
+ * `<certrev-cert-modal>` in another face root, and one outside every face root never reaches a
43
+ * modal inside one.
44
+ */
45
+ export const FACE_ROOT_SELECTOR = '[data-certrev-cert]'
46
+
47
+ /** The element a trigger opens, or why it has none. */
48
+ export type OwnCertModal =
49
+ | { readonly modal: Element; readonly reason: null }
50
+ | { readonly modal: null; readonly reason: 'none' | 'ambiguous'; readonly count: number }
51
+
52
+ /**
53
+ * The `<certrev-cert-modal>` that belongs to `trigger`'s face: the one in the SMALLEST subtree that
54
+ * holds both. Walk up from the trigger; the first ancestor holding a modal decides:
55
+ * - exactly one → that is the face's modal. On Shopify the card wrap (badge + modal) and the memo
56
+ * wrap (triggers only) are placed apart in the article, and their nearest common container holds
57
+ * the page's one modal; a Builder page puts one modal beside the CMS content; a WordPress face root
58
+ * holds its own.
59
+ * - more than one → `ambiguous`: the trigger's face has no modal of its own and sits between other
60
+ * faces'. Picking one would open another cert's certificate, so there is no answer.
61
+ * The walk never leaves the trigger's face root, and only counts modals in that same face root (or,
62
+ * for a trigger in none, modals in none), so a WordPress face without a modal resolves to `none`.
63
+ */
64
+ export function ownCertModal(trigger: Element): OwnCertModal {
65
+ const face = trigger.closest(FACE_ROOT_SELECTOR)
66
+ for (let node: Element | null = trigger; node; node = node.parentElement) {
67
+ const found = Array.from(node.querySelectorAll(CERT_MODAL_TAG)).filter(
68
+ (m) => m.closest(FACE_ROOT_SELECTOR) === face,
69
+ )
70
+ if (found.length === 1) return { modal: found[0] as Element, reason: null }
71
+ if (found.length > 1) return { modal: null, reason: 'ambiguous', count: found.length }
72
+ if (node === face) break
73
+ }
74
+ return { modal: null, reason: 'none', count: 0 }
75
+ }
76
+
36
77
  /** Shared constructable stylesheet (one per document lifetime). */
37
78
  let sharedSheet: CSSStyleSheet | null = null
38
79
  function supportsConstructableSheets(): boolean {
@@ -208,11 +249,13 @@ export class CertRevCertModal extends HTMLElement {
208
249
  /**
209
250
  * The modal kill-switch: a fetched envelope is REVOKED/EXPIRED. Don't open;
210
251
  * mark the host suppressed (mirrors the badge revalidation's `data-certrev-suppressed`) and
211
- * hide the light-DOM triggers so a stale click can't re-open a pulled cert. Best-effort DOM.
252
+ * hide THIS face's light-DOM triggers (those whose `ownCertModal` is this element) so a stale
253
+ * click can't re-open a pulled cert. Another face's triggers on the page are not touched.
212
254
  */
213
255
  private markSuppressed(): void {
214
256
  this.setAttribute('data-certrev-suppressed', '')
215
- for (const t of Array.from(document.querySelectorAll('[data-certrev-modal-open]'))) {
257
+ for (const t of Array.from(this.ownerDocument.querySelectorAll('[data-certrev-modal-open]'))) {
258
+ if (ownCertModal(t).modal !== this) continue
216
259
  t.setAttribute('hidden', '')
217
260
  t.setAttribute('aria-hidden', 'true')
218
261
  }
@@ -10,7 +10,6 @@
10
10
 
11
11
  import type { ResolvePublicKeyByKid } from '@certrev/cert-contract'
12
12
  import './certrev-cert-modal.js' // registers the unified <certrev-cert-modal> element
13
- import { installFtcGuard } from './ftc-guard.js'
14
13
  import { initCertInteractionsOnReady } from './interactions.js'
15
14
  import { initCertPlacement } from './placement.js'
16
15
  import { initBadgeRevalidation } from './revalidate.js'
@@ -37,10 +36,9 @@ if (typeof document !== 'undefined') {
37
36
  // in-article position — so the badge re-verify pass below finds `<certrev-badge>` already in
38
37
  // place and never re-runs against a detached/re-attached node.
39
38
  initCertPlacement(document)
39
+ // No disclosure guard since 1.1.1: nothing here hides, replaces or re-checks a face at load or on
40
+ // mutation, so a face that loads inside a closed <details>, tab or accordion renders when opened.
40
41
  // Tap-to-expand contributor bios + the in-page certificate modal (progressive enhancement).
41
42
  initCertInteractionsOnReady(document)
42
- // FTC §3 anti-stripping: neutralize a memo whose verbatim disclosure was stripped/hidden/altered
43
- // (restores the iframe kill switch in the native, in-DOM render). No-op when there's no memo.
44
- installFtcGuard(document)
45
43
  initBadgeRevalidation(document, { verify: createDeliveryVerifier(resolveKid) })
46
44
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Public names kept only so a patch release never breaks a compiling integration (the semver
3
+ * contract at the head of CHANGELOG.md). Each one does nothing it used to do; removal is for 2.0.
4
+ */
5
+
6
+ /** Dispatched on the document, once per document, on the first call of a removed API, so a caller
7
+ * that still relies on it can see that it does nothing. `detail: { api, removedIn }`. */
8
+ export const DEPRECATED_API_EVENT = 'certrev:deprecated-api'
9
+
10
+ /** Documents that have already been told, so repeated calls dispatch once. */
11
+ const announcedDocs = new WeakSet<Document>()
12
+
13
+ /**
14
+ * @deprecated Removed in 1.1.1: the runtime FTC disclosure guard is gone. This installs nothing,
15
+ * checks nothing and never hides or replaces a face; whether a face shows its disclosure is settled
16
+ * where the face is configured. The first call per document dispatches {@link DEPRECATED_API_EVENT}.
17
+ * Will be deleted in 2.0.
18
+ */
19
+ export function installFtcGuard(doc: Document): void {
20
+ if (announcedDocs.has(doc)) return
21
+ announcedDocs.add(doc)
22
+ doc.dispatchEvent(
23
+ new CustomEvent(DEPRECATED_API_EVENT, { detail: { api: 'installFtcGuard', removedIn: '1.1.1' } }),
24
+ )
25
+ }
@@ -4,9 +4,13 @@
4
4
  * module. Legally load-bearing: the curly apostrophe (U+2019) in "article’s"
5
5
  * is byte-exact; a reword is a contract divergence.
6
6
  *
7
- * Kept as its OWN module (imported by both ftc-guard + display-strings, exactly as in portal) so
8
- * the browser bundle emits the same `var` structure as the deployed certrev-cert.js — do not merge
9
- * it into display-strings.ts (that consolidation drops one `var` and breaks byte-parity).
7
+ * Kept as its OWN module (imported by display-strings, exactly as in portal) so the browser bundle
8
+ * emits the same `var` structure as the deployed certrev-cert.js — do not merge it into
9
+ * display-strings.ts (that consolidation drops one `var` and breaks byte-parity).
10
+ *
11
+ * It is the text the faces RENDER, nothing more. Since 1.1.1 nothing in this package checks at
12
+ * runtime that it is present or visible (the `ftc-guard` kill switch is gone): whether a face shows
13
+ * its disclosure is settled where the face is configured, not by hiding or replacing it in the page.
10
14
  */
11
15
 
12
16
  export const FTC_DISCLOSURE_LINE =
@@ -6,8 +6,8 @@
6
6
  * storefront (`<script src=".../dist/modal/certrev-cert.js">`) instead of vendoring a copy.
7
7
  *
8
8
  * Importing this module as ESM has the SAME side effect as loading the IIFE: it registers the
9
- * `<certrev-cert-modal>` element and (in a browser) runs placement + interactions + FTC guard +
10
- * badge revalidation. Bundler users: `import '@certrev/cert-block/modal'`.
9
+ * `<certrev-cert-modal>` element and (in a browser) runs placement + interactions + badge
10
+ * revalidation (no FTC guard since 1.1.1). Bundler users: `import '@certrev/cert-block/modal'`.
11
11
  */
12
12
 
13
13
  // Side-effect: register the element + (in a browser) kick off the DOM passes.
@@ -23,7 +23,8 @@ export {
23
23
 
24
24
  // Composable entry points for advanced consumers (the IIFE wires these itself).
25
25
  export { CERT_MODAL_TAG } from "./certrev-cert-modal.js";
26
- export { installFtcGuard } from "./ftc-guard.js";
26
+ /** @deprecated Since 1.1.1 a no-op (the runtime FTC guard was removed); deleted in 2.0. */
27
+ export { installFtcGuard } from "./deprecated.js";
27
28
  export { initCertInteractionsOnReady } from "./interactions.js";
28
29
  export { initCertPlacement } from "./placement.js";
29
30
  export {
@@ -8,8 +8,8 @@
8
8
  * (revealing the row's `.acc-bio` in place; the name caret rotates via CSS) and mirrors the
9
9
  * state on `aria-expanded`. A click on a link inside the row (e.g. the "Compensated expert"
10
10
  * cue) is NOT a toggle. Keyboard: Enter/Space on the role=button row.
11
- * • In-page modals: a `[data-certrev-modal-open]` control opens the unified
12
- * `<certrev-cert-modal>` element via its `.open('cert'|'expert')` method. Bare /
11
+ * • In-page modals: a `[data-certrev-modal-open]` control opens its OWN face's
12
+ * `<certrev-cert-modal>` element (`ownCertModal`) via its `.open('cert'|'expert')` method. Bare /
13
13
  * `"cert"` opens the certificate dialog; `"expert"` opens the reviewer dialog. With no JS / no
14
14
  * element / no envelope a link control falls back to its href (the certificate or the CertREV
15
15
  * profile page). The element owns its OWN close plumbing (× / Close / backdrop / Esc) inside its
@@ -18,12 +18,18 @@
18
18
  * No crypto, no network — pure DOM. Idempotent install (guarded so a double-load can't double-bind).
19
19
  */
20
20
 
21
- import { CERT_MODAL_TAG } from './certrev-cert-modal.js'
21
+ import { CERT_MODAL_TAG, ownCertModal } from './certrev-cert-modal.js'
22
22
 
23
23
  export const ACC_ATTR = 'data-certrev-acc'
24
24
  export const MODAL_OPEN_ATTR = 'data-certrev-modal-open'
25
25
  export const INITIALS_ATTR = 'data-certrev-initials'
26
26
  export const CARD_SELECTOR = '.certrev-card'
27
+ /** Stamped on a control that could not tell which modal is its own (value: the reason), beside the
28
+ * bubbling {@link MODAL_UNRESOLVED_EVENT}, so the miss is inspectable rather than a silent link. */
29
+ export const MODAL_UNRESOLVED_ATTR = 'data-certrev-modal-unresolved'
30
+ /** Dispatched on a control whose face has no modal of its own but sits between other faces'
31
+ * (`ownCertModal` → `ambiguous`). `detail: { reason: 'ambiguous', modals }`. */
32
+ export const MODAL_UNRESOLVED_EVENT = 'certrev:modal-unresolved'
27
33
 
28
34
  /** The dialog a `data-certrev-modal-open` control targets. */
29
35
  export type ModalKind = 'cert' | 'expert'
@@ -53,8 +59,10 @@ export function modalTargetKind(value: string | null): ModalKind {
53
59
  return value === 'expert' ? 'expert' : 'cert'
54
60
  }
55
61
 
56
- /** Open a cert dialog via the `<certrev-cert-modal>` element. Returns false (so the caller lets an
57
- * href fall back) when the element is absent or declines (no envelope + no fetch source). */
62
+ /** Open a cert dialog via the document's FIRST `<certrev-cert-modal>` element: a page-level helper
63
+ * for a page with one face. Controls go through `openOwnModal`, which finds their own face's element.
64
+ * Returns false (so the caller lets an href fall back) when the element is absent or declines (no
65
+ * envelope + no fetch source). */
58
66
  export function openModal(doc: Document, kind: ModalKind): boolean {
59
67
  const el = doc.querySelector(CERT_MODAL_TAG) as CertModalElement | null
60
68
  if (el && typeof el.open === 'function') return el.open(kind)
@@ -66,6 +74,28 @@ export function openCertModal(doc: Document): boolean {
66
74
  return openModal(doc, 'cert')
67
75
  }
68
76
 
77
+ /**
78
+ * Open a dialog of `control`'s OWN face's `<certrev-cert-modal>` (see `ownCertModal`), which is what
79
+ * the delegated click / key handlers do. Returns false (so a link control follows its own href) when
80
+ * the face has no modal, the element declines, or the control cannot tell which modal is its own; in
81
+ * that last case it is stamped `data-certrev-modal-unresolved="ambiguous"` and receives
82
+ * `certrev:modal-unresolved`, and another face's certificate is never opened in its place.
83
+ */
84
+ export function openOwnModal(control: Element, kind: ModalKind): boolean {
85
+ const own = ownCertModal(control)
86
+ if (own.modal) {
87
+ const el = own.modal as CertModalElement
88
+ return typeof el.open === 'function' ? el.open(kind) : false
89
+ }
90
+ if (own.reason === 'ambiguous') {
91
+ control.setAttribute(MODAL_UNRESOLVED_ATTR, own.reason)
92
+ control.dispatchEvent(
93
+ new CustomEvent(MODAL_UNRESOLVED_EVENT, { bubbles: true, detail: { reason: own.reason, modals: own.count } }),
94
+ )
95
+ }
96
+ return false
97
+ }
98
+
69
99
  function asElement(target: EventTarget | null): Element | null {
70
100
  return target && (target as Element).closest ? (target as Element) : null
71
101
  }
@@ -111,7 +141,7 @@ export function initCertInteractions(doc: Document): void {
111
141
  // (Close is owned by the <certrev-cert-modal> element inside its shadow root.)
112
142
  const opener = el.closest(`[${MODAL_OPEN_ATTR}]`)
113
143
  if (opener) {
114
- if (openModal(doc, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR)))) e.preventDefault()
144
+ if (openOwnModal(opener, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR)))) e.preventDefault()
115
145
  return
116
146
  }
117
147
 
@@ -144,7 +174,7 @@ export function initCertInteractions(doc: Document): void {
144
174
 
145
175
  const opener = el.closest(`[${MODAL_OPEN_ATTR}]`)
146
176
  if (opener) {
147
- if (openModal(doc, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR)))) {
177
+ if (openOwnModal(opener, modalTargetKind(opener.getAttribute(MODAL_OPEN_ATTR)))) {
148
178
  e.preventDefault()
149
179
  return
150
180
  }