@certrev/cert-block 0.5.4 → 1.0.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/CHANGELOG.md +195 -12
- package/LICENSE +21 -0
- package/README.md +422 -50
- package/dist/builder/cert-chrome-data.d.ts +26 -8
- package/dist/builder/cert-chrome-data.d.ts.map +1 -1
- package/dist/builder/cert-chrome-data.js +5 -5
- package/dist/builder/cert-chrome-data.js.map +1 -1
- package/dist/builder/cert-review-card.d.ts +35 -5
- package/dist/builder/cert-review-card.d.ts.map +1 -1
- package/dist/builder/cert-review-card.js +115 -34
- package/dist/builder/cert-review-card.js.map +1 -1
- package/dist/builder/index.d.ts +33 -16
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +33 -16
- package/dist/builder/index.js.map +1 -1
- package/dist/components/CertBadge.d.ts +2 -2
- package/dist/components/CertBadge.d.ts.map +1 -1
- package/dist/components/CertBadge.js +11 -5
- package/dist/components/CertBadge.js.map +1 -1
- package/dist/components/CertJsonLd.d.ts.map +1 -1
- package/dist/components/CertJsonLd.js.map +1 -1
- package/dist/components/CertRevBacklink.d.ts.map +1 -1
- package/dist/components/CertRevBacklink.js +3 -2
- package/dist/components/CertRevBacklink.js.map +1 -1
- package/dist/components/CertReview.d.ts +1 -1
- package/dist/components/ExpertBio.d.ts.map +1 -1
- package/dist/components/ExpertBio.js +12 -4
- package/dist/components/ExpertBio.js.map +1 -1
- package/dist/components/escape.d.ts +28 -1
- package/dist/components/escape.d.ts.map +1 -1
- package/dist/components/escape.js +37 -8
- package/dist/components/escape.js.map +1 -1
- package/dist/components/format.d.ts +51 -2
- package/dist/components/format.d.ts.map +1 -1
- package/dist/components/format.js +103 -6
- package/dist/components/format.js.map +1 -1
- package/dist/components/preview-block.d.ts +1 -1
- package/dist/components/preview-block.js +3 -3
- package/dist/components/preview-block.js.map +1 -1
- package/dist/components/rel.d.ts +24 -0
- package/dist/components/rel.d.ts.map +1 -0
- package/dist/components/rel.js +24 -0
- package/dist/components/rel.js.map +1 -0
- package/dist/components/render-cert-block.d.ts +50 -22
- package/dist/components/render-cert-block.d.ts.map +1 -1
- package/dist/components/render-cert-block.js +146 -80
- package/dist/components/render-cert-block.js.map +1 -1
- package/dist/components/render-def.d.ts +13 -14
- package/dist/components/render-def.d.ts.map +1 -1
- package/dist/components/render-def.js +14 -15
- package/dist/components/render-def.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/jsonld/project.d.ts +46 -13
- package/dist/jsonld/project.d.ts.map +1 -1
- package/dist/jsonld/project.js +139 -56
- package/dist/jsonld/project.js.map +1 -1
- package/dist/modal/avatar-resize.d.ts +10 -10
- package/dist/modal/avatar-resize.js +14 -14
- package/dist/modal/avatar-resize.js.map +1 -1
- package/dist/modal/cert-modal-view.d.ts +27 -7
- package/dist/modal/cert-modal-view.d.ts.map +1 -1
- package/dist/modal/cert-modal-view.js +49 -25
- package/dist/modal/cert-modal-view.js.map +1 -1
- package/dist/modal/certrev-cert-modal.d.ts +5 -5
- package/dist/modal/certrev-cert-modal.js +9 -9
- package/dist/modal/certrev-cert-modal.js.map +1 -1
- package/dist/modal/certrev-cert-modal.styles.d.ts +6 -6
- package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -1
- package/dist/modal/certrev-cert-modal.styles.js +8 -8
- package/dist/modal/certrev-cert-modal.styles.js.map +1 -1
- package/dist/modal/certrev-cert.entry.d.ts +8 -6
- package/dist/modal/certrev-cert.entry.d.ts.map +1 -1
- package/dist/modal/certrev-cert.entry.js +9 -7
- package/dist/modal/certrev-cert.entry.js.map +1 -1
- package/dist/modal/certrev-cert.js +6 -6
- package/dist/modal/display-strings.d.ts +25 -5
- package/dist/modal/display-strings.d.ts.map +1 -1
- package/dist/modal/display-strings.js +27 -5
- package/dist/modal/display-strings.js.map +1 -1
- package/dist/modal/escape.d.ts +26 -3
- package/dist/modal/escape.d.ts.map +1 -1
- package/dist/modal/escape.js +32 -3
- package/dist/modal/escape.js.map +1 -1
- package/dist/modal/font-face.d.ts.map +1 -1
- package/dist/modal/font-face.js +8 -11
- package/dist/modal/font-face.js.map +1 -1
- package/dist/modal/ftc-disclosure.d.ts +2 -2
- package/dist/modal/ftc-disclosure.js +2 -2
- package/dist/modal/ftc-guard.d.ts +44 -9
- package/dist/modal/ftc-guard.d.ts.map +1 -1
- package/dist/modal/ftc-guard.js +50 -17
- package/dist/modal/ftc-guard.js.map +1 -1
- package/dist/modal/index.d.ts +1 -1
- package/dist/modal/index.js +1 -1
- package/dist/modal/interactions.d.ts +2 -2
- package/dist/modal/interactions.js +2 -2
- package/dist/modal/logo-paths.js +2 -2
- package/dist/modal/logo-paths.js.map +1 -1
- package/dist/modal/placement.d.ts +4 -3
- package/dist/modal/placement.d.ts.map +1 -1
- package/dist/modal/placement.js +7 -6
- package/dist/modal/placement.js.map +1 -1
- package/dist/modal/revalidate.d.ts +2 -3
- package/dist/modal/revalidate.d.ts.map +1 -1
- package/dist/modal/revalidate.js +2 -3
- package/dist/modal/revalidate.js.map +1 -1
- package/dist/modal/verify-client.d.ts +2 -2
- package/dist/modal/verify-client.js +2 -2
- package/dist/verify/cache.d.ts +21 -6
- package/dist/verify/cache.d.ts.map +1 -1
- package/dist/verify/cache.js +29 -8
- package/dist/verify/cache.js.map +1 -1
- package/dist/verify/fetch-with-deadline.d.ts +35 -0
- package/dist/verify/fetch-with-deadline.d.ts.map +1 -0
- package/dist/verify/fetch-with-deadline.js +55 -0
- package/dist/verify/fetch-with-deadline.js.map +1 -0
- package/dist/verify/get-verified-envelope.d.ts +23 -9
- package/dist/verify/get-verified-envelope.d.ts.map +1 -1
- package/dist/verify/get-verified-envelope.js +89 -40
- package/dist/verify/get-verified-envelope.js.map +1 -1
- package/dist/verify/resolve-kid.d.ts +3 -0
- package/dist/verify/resolve-kid.d.ts.map +1 -1
- package/dist/verify/resolve-kid.js +17 -8
- package/dist/verify/resolve-kid.js.map +1 -1
- package/dist/webcomponent/certrev-badge.d.ts.map +1 -1
- package/dist/webcomponent/certrev-badge.js +24 -2
- package/dist/webcomponent/certrev-badge.js.map +1 -1
- package/dist/webcomponent/render-badge-html.d.ts +1 -1
- package/dist/webcomponent/render-badge-html.d.ts.map +1 -1
- package/dist/webcomponent/render-badge-html.js +36 -17
- package/dist/webcomponent/render-badge-html.js.map +1 -1
- package/package.json +14 -2
- package/src/builder/cert-chrome-data.ts +26 -8
- package/src/builder/cert-review-card.tsx +147 -38
- package/src/builder/index.tsx +34 -17
- package/src/components/CertBadge.tsx +14 -9
- package/src/components/CertJsonLd.tsx +1 -1
- package/src/components/CertRevBacklink.tsx +10 -3
- package/src/components/CertReview.tsx +1 -1
- package/src/components/ExpertBio.tsx +12 -7
- package/src/components/escape.ts +37 -7
- package/src/components/format.ts +102 -6
- package/src/components/preview-block.ts +3 -3
- package/src/components/rel.ts +23 -0
- package/src/components/render-cert-block.ts +192 -109
- package/src/components/render-def.ts +14 -15
- package/src/index.ts +4 -2
- package/src/jsonld/project.ts +155 -59
- package/src/modal/avatar-resize.ts +14 -14
- package/src/modal/cert-modal-view.ts +52 -27
- package/src/modal/certrev-cert-modal.styles.ts +8 -8
- package/src/modal/certrev-cert-modal.ts +9 -9
- package/src/modal/certrev-cert.entry.ts +9 -7
- package/src/modal/display-strings.ts +26 -5
- package/src/modal/escape.ts +32 -4
- package/src/modal/font-face.ts +8 -11
- package/src/modal/ftc-disclosure.ts +2 -2
- package/src/modal/ftc-guard.ts +73 -18
- package/src/modal/index.ts +1 -1
- package/src/modal/interactions.ts +2 -2
- package/src/modal/logo-paths.ts +2 -2
- package/src/modal/placement.ts +7 -6
- package/src/modal/revalidate.ts +2 -3
- package/src/modal/verify-client.ts +2 -2
- package/src/verify/cache.ts +34 -8
- package/src/verify/fetch-with-deadline.ts +60 -0
- package/src/verify/get-verified-envelope.ts +131 -48
- package/src/verify/resolve-kid.ts +22 -6
- package/src/webcomponent/certrev-badge.ts +24 -2
- package/src/webcomponent/render-badge-html.ts +66 -19
- package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +0 -17
- package/src/__tests__/components.test.tsx +0 -206
- package/src/__tests__/preview-block.test.ts +0 -84
- package/src/__tests__/project.test.ts +0 -128
- package/src/__tests__/render-cert-block.test.ts +0 -657
- package/src/__tests__/render-def.test.tsx +0 -295
- package/src/__tests__/verify.test.ts +0 -239
- package/src/__tests__/webcomponent.test.tsx +0 -116
- package/src/builder/__tests__/builder.test.tsx +0 -158
- package/src/modal/cert-modal-view.test.ts +0 -175
- package/src/modal/certrev-cert-modal.test.ts +0 -125
- package/src/modal/ftc-guard.test.ts +0 -153
- package/src/modal/interactions.test.ts +0 -250
- package/src/modal/placement.test.ts +0 -398
- package/src/modal/revalidate.test.ts +0 -111
- package/src/modal/verify-client.test.ts +0 -164
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/builder/index.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAexD,MAAM,cAAc,GAAG,aAAa,CAAsB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;AAE9F;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAC/B,OAAO,EACP,SAAS,GAAG,KAAK,EACjB,QAAQ,GAKR;IACA,OAAO,KAAC,cAAc,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,YAAG,QAAQ,GAA2B,CAAA;AACpG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB;IACvC,OAAO,CACN,kDACiC,EAAE,EAClC,KAAK,EAAE;YACN,MAAM,EAAE,oBAAoB;YAC5B,YAAY,EAAE,CAAC;YACf,OAAO,EAAE,WAAW;YACpB,IAAI,EAAE,gBAAgB;YACtB,KAAK,EAAE,SAAS;YAChB,UAAU,EAAE,SAAS;SACrB,uCAEgB,8CAA0B,oJAEtC,CACN,CAAA;AACF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC5B,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,UAAU,CAAC,cAAc,CAAC,CAAA;IACzD,IAAI,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACtD,OAAO,KAAC,UAAU,IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,SAAG,CAAA;IACrF,CAAC;IACD,IAAI,SAAS;QAAE,OAAO,KAAC,wBAAwB,KAAG,CAAA;IAClD,OAAO,IAAI,CAAA;AACZ,CAAC;AAaD;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAA+B;IACjE,SAAS,EAAE,aAAa;IACxB,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,EAAE;CACV,CAAA;AAED,gFAAgF;AAChF,OAAO,EACN,wBAAwB,GAIxB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACN,oBAAoB,EAEpB,mBAAmB,EAEnB,cAAc,EAGd,oBAAoB,EACpB,gBAAgB,EAChB,WAAW,GACX,MAAM,uBAAuB,CAAA"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `'full'` shows the photo, certified/updated dates, and the memo (per display flags). BOTH
|
|
16
16
|
* styles carry the compensation cue + FTC scope line (the always-on compliance disclosures).
|
|
17
17
|
*
|
|
18
|
-
* THEMING
|
|
18
|
+
* THEMING: presentation defaults come from `payload.content.display`; an optional
|
|
19
19
|
* `renderDef` prop layers a brand's validated themeable tokens (emitted as `--certrev-*`
|
|
20
20
|
* CSS custom properties) and its subtractive field-visibility on top. No `renderDef` → the
|
|
21
21
|
* CertREV preset default (theme output byte-identical to un-themed).
|
|
@@ -32,7 +32,7 @@ export interface CertBadgeProps {
|
|
|
32
32
|
/** Override the badge style independent of the signed display config. */
|
|
33
33
|
readonly badgeStyle?: 'full' | 'compact';
|
|
34
34
|
/**
|
|
35
|
-
* The brand render-def
|
|
35
|
+
* The brand render-def. ABSENT → the badge renders the CertREV preset
|
|
36
36
|
* default (fail-safe; theme output byte-identical to un-themed). PRESENT → its
|
|
37
37
|
* validated themeable tokens are emitted as `--certrev-*` CSS custom properties
|
|
38
38
|
* and its subtractive hide-set removes brand-toggleable fields. Invalid tokens
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertBadge.d.ts","sourceRoot":"","sources":["../../src/components/CertBadge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"CertBadge.d.ts","sourceRoot":"","sources":["../../src/components/CertBadge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIrE,OAAO,EAGN,KAAK,aAAa,EAGlB,MAAM,iBAAiB,CAAA;AAExB,MAAM,WAAW,cAAc;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,oGAAoG;IACpG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;CAClC;AAwCD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAgI9C"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { safeHttpUrl } from './escape.js';
|
|
3
|
-
import { credentialSuffix, expertNameWithCredentials, formatDate, resolveDisplay } from './format.js';
|
|
3
|
+
import { credentialSuffix, dedupeCredential, expertNameWithCredentials, formatDate, resolveDisplay } from './format.js';
|
|
4
|
+
import { CERTREV_LINK_REL } from './rel.js';
|
|
4
5
|
import { CERT_SCOPE_LINE, CERT_STRINGS_VERSION, resolveRenderTheme, themeCssVarDeclarations, } from './render-def.js';
|
|
5
6
|
const ROOT_CLASS = 'certrev-badge';
|
|
6
7
|
/** Inline-SVG verified checkmark — no external asset, accent-tinted, decorative. */
|
|
@@ -11,7 +12,9 @@ function ExpertPhoto({ content, show }) {
|
|
|
11
12
|
const src = show ? safeHttpUrl(content.expert.photoUrl) : null;
|
|
12
13
|
if (!src)
|
|
13
14
|
return null;
|
|
14
|
-
return (_jsx("img", { className: `${ROOT_CLASS}__photo`, src: src,
|
|
15
|
+
return (_jsx("img", { className: `${ROOT_CLASS}__photo`, src: src,
|
|
16
|
+
// Decorative: the byline renders the same name as text right beside it.
|
|
17
|
+
alt: "", width: 40, height: 40, loading: "lazy", decoding: "async" }));
|
|
15
18
|
}
|
|
16
19
|
export function CertBadge(props) {
|
|
17
20
|
const { payload } = props;
|
|
@@ -23,7 +26,10 @@ export function CertBadge(props) {
|
|
|
23
26
|
const profileUrl = safeHttpUrl(content.expert.profileUrl);
|
|
24
27
|
const certifiedLabel = formatDate(content.certifiedAt);
|
|
25
28
|
const updatedLabel = formatDate(content.contentModifiedAt);
|
|
26
|
-
|
|
29
|
+
// Composed name + suffix, so the suffix drops a credential the stored display name already
|
|
30
|
+
// carries ("Dr. Erik Schraga, MD" + "MD, EM" → one "MD"). `credentialSuffix` stays whole for
|
|
31
|
+
// callers that render the list away from the name.
|
|
32
|
+
const suffix = dedupeCredential(content.expert.displayName, credentialSuffix(content));
|
|
27
33
|
const accent = display.accentColor;
|
|
28
34
|
const rootClass = `${ROOT_CLASS} ${ROOT_CLASS}--${style}${props.className ? ` ${props.className}` : ''}`;
|
|
29
35
|
// Accent is exposed as a custom property so brand themes can read it, and applied
|
|
@@ -31,12 +37,12 @@ export function CertBadge(props) {
|
|
|
31
37
|
// A THEMED brand additionally emits the surface/radius/font vars; with no render-def
|
|
32
38
|
// `themeCssVarDeclarations` is empty, so the un-themed style is byte-identical.
|
|
33
39
|
const rootStyle = {
|
|
34
|
-
|
|
40
|
+
'--certrev-accent': accent,
|
|
35
41
|
borderInlineStartColor: accent,
|
|
36
42
|
};
|
|
37
43
|
for (const [name, value] of themeCssVarDeclarations(theme))
|
|
38
44
|
rootStyle[name] = value;
|
|
39
45
|
const expertName = (_jsxs("span", { className: `${ROOT_CLASS}__expert-name`, children: [content.expert.displayName, suffix ? _jsxs("span", { className: `${ROOT_CLASS}__credentials`, children: [", ", suffix] }) : null] }));
|
|
40
|
-
return (_jsxs("section", { className: rootClass, style: rootStyle, "data-certrev-cert-id": payload.certId, "data-certrev-strings-version": CERT_STRINGS_VERSION, "data-certrev-def-version": theme.version, "aria-label": `Content reviewed by ${expertNameWithCredentials(content)}`, children: [_jsxs("div", { className: `${ROOT_CLASS}__header`, children: [_jsx(VerifiedMark, { accent: accent }), style === 'full' ? _jsx(ExpertPhoto, { content: content, show: display.showExpertPhoto }) : null, _jsxs("div", { className: `${ROOT_CLASS}__heading`, children: [_jsx("span", { className: `${ROOT_CLASS}__eyebrow`, children: "Expert reviewed" }), _jsxs("span", { className: `${ROOT_CLASS}__byline`, children: ["Reviewed by", ' ', profileUrl ? (_jsx("a", { className: `${ROOT_CLASS}__expert-link`, href: profileUrl, rel:
|
|
46
|
+
return (_jsxs("section", { className: rootClass, style: rootStyle, "data-certrev-cert-id": payload.certId, "data-certrev-strings-version": CERT_STRINGS_VERSION, "data-certrev-def-version": theme.version, "aria-label": `Content reviewed by ${expertNameWithCredentials(content)}`, children: [_jsxs("div", { className: `${ROOT_CLASS}__header`, children: [_jsx(VerifiedMark, { accent: accent }), style === 'full' ? _jsx(ExpertPhoto, { content: content, show: display.showExpertPhoto }) : null, _jsxs("div", { className: `${ROOT_CLASS}__heading`, children: [_jsx("span", { className: `${ROOT_CLASS}__eyebrow`, children: "Expert reviewed" }), _jsxs("span", { className: `${ROOT_CLASS}__byline`, children: ["Reviewed by", ' ', profileUrl ? (_jsx("a", { className: `${ROOT_CLASS}__expert-link`, href: profileUrl, rel: CERTREV_LINK_REL, children: expertName })) : (expertName)] })] })] }), style === 'full' ? (_jsxs(_Fragment, { children: [display.showMemo && content.memo ? _jsx("p", { className: `${ROOT_CLASS}__memo`, children: content.memo }) : null, display.showAuthor && content.author.name && content.author.name !== content.expert.displayName ? (_jsxs("p", { className: `${ROOT_CLASS}__author`, children: ["Written by ", content.author.name, content.author.title ? `, ${content.author.title}` : ''] })) : null, _jsxs("dl", { className: `${ROOT_CLASS}__dates`, children: [certifiedLabel ? (_jsxs("div", { className: `${ROOT_CLASS}__date`, children: [_jsx("dt", { children: "Certified" }), _jsx("dd", { children: _jsx("time", { dateTime: content.certifiedAt, children: certifiedLabel }) })] })) : null, updatedLabel ? (_jsxs("div", { className: `${ROOT_CLASS}__date`, children: [_jsx("dt", { children: "Last updated" }), _jsx("dd", { children: _jsx("time", { dateTime: content.contentModifiedAt ?? undefined, children: updatedLabel }) })] })) : null] })] })) : null, _jsxs("div", { className: `${ROOT_CLASS}__disclosure`, children: [_jsxs("span", { className: `${ROOT_CLASS}__compensated-cue`, children: ["Compensated", ' ', profileUrl ? (_jsx("a", { className: `${ROOT_CLASS}__cue-link`, href: profileUrl, rel: CERTREV_LINK_REL, children: "expert" })) : ('expert')] }), _jsx("p", { className: `${ROOT_CLASS}__ftc`, "data-ftc-disclosure": true, children: _jsx("span", { "data-ftc-line": true, children: CERT_SCOPE_LINE }) })] }), verifyUrl ? (_jsx("a", { className: `${ROOT_CLASS}__verify`, href: verifyUrl, rel: CERTREV_LINK_REL, "aria-label": "Verify this certification on CertREV", children: "Verify on CertREV" })) : null] }));
|
|
41
47
|
}
|
|
42
48
|
//# sourceMappingURL=CertBadge.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertBadge.js","sourceRoot":"","sources":["../../src/components/CertBadge.tsx"],"names":[],"mappings":";AAwBA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"CertBadge.js","sourceRoot":"","sources":["../../src/components/CertBadge.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertJsonLd.d.ts","sourceRoot":"","sources":["../../src/components/CertJsonLd.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"CertJsonLd.d.ts","sourceRoot":"","sources":["../../src/components/CertJsonLd.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,KAAK,oBAAoB,EAA+C,MAAM,sBAAsB,CAAA;AAE7G,MAAM,WAAW,eAAgB,SAAQ,oBAAoB;IAC5D,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,+BAWhD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertJsonLd.js","sourceRoot":"","sources":["../../src/components/CertJsonLd.tsx"],"names":[],"mappings":";AAgBA,OAAO,
|
|
1
|
+
{"version":3,"file":"CertJsonLd.js","sourceRoot":"","sources":["../../src/components/CertJsonLd.tsx"],"names":[],"mappings":";AAgBA,OAAO,EAA6B,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAQ7G,MAAM,UAAU,UAAU,CAAC,KAAsB;IAChD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAA;IAC5C,MAAM,IAAI,GAAG,wBAAwB,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAA;IACvE,OAAO,CACN,iBACC,IAAI,EAAC,qBAAqB,EAC1B,EAAE,EAAE,QAAQ;QACZ,8MAA8M;QAC9M,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GACxC,CACF,CAAA;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertRevBacklink.d.ts","sourceRoot":"","sources":["../../src/components/CertRevBacklink.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"CertRevBacklink.d.ts","sourceRoot":"","sources":["../../src/components/CertRevBacklink.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAKxD,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACvB;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,sCA6C1D"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { safeHttpUrl } from './escape.js';
|
|
3
3
|
import { resolveDisplay } from './format.js';
|
|
4
|
+
import { CERTREV_LINK_REL } from './rel.js';
|
|
4
5
|
const ROOT_CLASS = 'certrev-backlink';
|
|
5
6
|
export function CertRevBacklink(props) {
|
|
6
7
|
const verifyUrl = safeHttpUrl(props.payload.content.verifyUrl);
|
|
@@ -10,7 +11,7 @@ export function CertRevBacklink(props) {
|
|
|
10
11
|
const rootClass = `${ROOT_CLASS}${props.className ? ` ${props.className}` : ''}`;
|
|
11
12
|
// Typed as a string record (not inline) so the `--certrev-accent` CSS custom property
|
|
12
13
|
// is accepted — React's CSSProperties rejects arbitrary custom props on an inline literal.
|
|
13
|
-
const rootStyle = {
|
|
14
|
-
return (_jsxs("a", { className: rootClass, style: rootStyle, href: verifyUrl, rel:
|
|
14
|
+
const rootStyle = { '--certrev-accent': display.accentColor, color: display.accentColor };
|
|
15
|
+
return (_jsxs("a", { className: rootClass, style: rootStyle, href: verifyUrl, rel: CERTREV_LINK_REL, "aria-label": "Verify this certification on CertREV", children: [_jsxs("svg", { className: `${ROOT_CLASS}__icon`, width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: [_jsx("title", { children: "Verified" }), _jsx("path", { d: "M12 2l7 3v6c0 4.4-3 8.3-7 9-4-0.7-7-4.6-7-9V5l7-3z", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinejoin: "round" }), _jsx("path", { d: "M8.5 12l2.3 2.3L15.5 9", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { className: `${ROOT_CLASS}__label`, children: props.label ?? 'Verify on CertREV' })] }));
|
|
15
16
|
}
|
|
16
17
|
//# sourceMappingURL=CertRevBacklink.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertRevBacklink.js","sourceRoot":"","sources":["../../src/components/CertRevBacklink.tsx"],"names":[],"mappings":";AAUA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"CertRevBacklink.js","sourceRoot":"","sources":["../../src/components/CertRevBacklink.tsx"],"names":[],"mappings":";AAUA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAU3C,MAAM,UAAU,GAAG,kBAAkB,CAAA;AAErC,MAAM,UAAU,eAAe,CAAC,KAA2B;IAC1D,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC9D,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAA;IAC3B,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAChF,MAAM,SAAS,GAAG,GAAG,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;IAChF,sFAAsF;IACtF,2FAA2F;IAC3F,MAAM,SAAS,GAA2B,EAAE,kBAAkB,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAA;IAEjH,OAAO,CACN,aACC,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,gBAAgB,gBACV,sCAAsC,aAEjD,eACC,SAAS,EAAE,GAAG,UAAU,QAAQ,EAChC,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,iBACC,MAAM,EAClB,SAAS,EAAC,OAAO,aAEjB,uCAAuB,EACvB,eACC,CAAC,EAAC,oDAAoD,EACtD,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,KAAK,EACjB,cAAc,EAAC,OAAO,GACrB,EACF,eACC,CAAC,EAAC,wBAAwB,EAC1B,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,KAAK,EACjB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,GACrB,IACG,EACN,eAAM,SAAS,EAAE,GAAG,UAAU,SAAS,YAAG,KAAK,CAAC,KAAK,IAAI,mBAAmB,GAAQ,IACjF,CACJ,CAAA;AACF,CAAC"}
|
|
@@ -18,7 +18,7 @@ export interface CertReviewProps {
|
|
|
18
18
|
readonly className?: string;
|
|
19
19
|
readonly badgeStyle?: 'full' | 'compact';
|
|
20
20
|
/**
|
|
21
|
-
* The brand render-def
|
|
21
|
+
* The brand render-def. Forwarded to `<CertBadge>` for themeable-token
|
|
22
22
|
* emission + subtractive visibility; absent → the CertREV preset default. The
|
|
23
23
|
* JSON-LD is presentation-blind, so the def never touches the structured data.
|
|
24
24
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpertBio.d.ts","sourceRoot":"","sources":["../../src/components/ExpertBio.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"ExpertBio.d.ts","sourceRoot":"","sources":["../../src/components/ExpertBio.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAKxD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,0FAA0F;IAC1F,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;CAC1C;AAID,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BA0D9C"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { safeHttpUrl } from './escape.js';
|
|
3
|
-
import { resolveDisplay } from './format.js';
|
|
3
|
+
import { credentialSuffix, dedupeCredential, resolveDisplay } from './format.js';
|
|
4
|
+
import { CERTREV_LINK_REL } from './rel.js';
|
|
4
5
|
const ROOT_CLASS = 'certrev-expert-bio';
|
|
5
6
|
export function ExpertBio(props) {
|
|
6
7
|
const { payload } = props;
|
|
@@ -10,8 +11,15 @@ export function ExpertBio(props) {
|
|
|
10
11
|
const photoUrl = display.showExpertPhoto ? safeHttpUrl(expert.photoUrl) : null;
|
|
11
12
|
const Heading = props.headingLevel ?? 'h3';
|
|
12
13
|
const rootClass = `${ROOT_CLASS}${props.className ? ` ${props.className}` : ''}`;
|
|
13
|
-
const rootStyle = {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const rootStyle = { '--certrev-accent': display.accentColor };
|
|
15
|
+
// Same composition rule as <CertBadge> / renderBadgeHtml: drop a credential the stored
|
|
16
|
+
// display name already carries, so one payload never reads "Dr. Erik Schraga, MD, MD, EM".
|
|
17
|
+
// The full list still renders below, away from the name, where every credential belongs.
|
|
18
|
+
const nameSuffix = dedupeCredential(expert.displayName, credentialSuffix(payload.content));
|
|
19
|
+
const nameNode = (_jsxs(Heading, { className: `${ROOT_CLASS}__name`, children: [expert.displayName, nameSuffix ? _jsxs("span", { className: `${ROOT_CLASS}__credentials`, children: [", ", nameSuffix] }) : null] }));
|
|
20
|
+
return (_jsxs("aside", { className: rootClass, style: rootStyle, "aria-label": `About the reviewing expert, ${expert.displayName}`, children: [photoUrl ? (_jsx("img", { className: `${ROOT_CLASS}__photo`, src: photoUrl,
|
|
21
|
+
// Decorative: the name is announced by this aside's aria-label AND rendered as text
|
|
22
|
+
// immediately below, so alt text repeats it a third time. Matches the modal's avatar.
|
|
23
|
+
alt: "", width: 64, height: 64, loading: "lazy", decoding: "async" })) : null, _jsxs("div", { className: `${ROOT_CLASS}__body`, children: [profileUrl ? (_jsx("a", { className: `${ROOT_CLASS}__name-link`, href: profileUrl, rel: CERTREV_LINK_REL, children: nameNode })) : (nameNode), expert.credentials.length > 0 ? (_jsx("ul", { className: `${ROOT_CLASS}__credential-list`, children: expert.credentials.map((c) => (_jsxs("li", { className: `${ROOT_CLASS}__credential`, children: [_jsx("abbr", { title: c.fullName, children: c.abbreviation }), _jsxs("span", { className: `${ROOT_CLASS}__credential-full`, children: [" \u2014 ", c.fullName] })] }, `${c.abbreviation}:${c.fullName}`))) })) : null, _jsx("p", { className: `${ROOT_CLASS}__role`, children: "Verified expert reviewer" })] })] }));
|
|
16
24
|
}
|
|
17
25
|
//# sourceMappingURL=ExpertBio.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpertBio.js","sourceRoot":"","sources":["../../src/components/ExpertBio.tsx"],"names":[],"mappings":";AASA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"ExpertBio.js","sourceRoot":"","sources":["../../src/components/ExpertBio.tsx"],"names":[],"mappings":"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"}
|
|
@@ -12,9 +12,36 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Everything here is pure and runtime-agnostic (no DOM, no Node APIs) so it runs in a
|
|
14
14
|
* server component, an edge runtime, and the Web Component identically.
|
|
15
|
+
*
|
|
16
|
+
* Every helper here is TYPE-HOSTILE-SAFE, and that is load-bearing rather than defensive
|
|
17
|
+
* habit: these run downstream of `get-verified-envelope.ts`'s bare
|
|
18
|
+
* `JSON.parse(...) as CertDeliveryEnvelope` cast, and `verifyEnvelope` checks the SIGNATURE,
|
|
19
|
+
* never the SHAPE — so a number / object / array arrives where the contract promised a
|
|
20
|
+
* string. A `TypeError` here is not a missing badge: under `renderToReadableStream` it aborts
|
|
21
|
+
* the whole article route with zero HTML, and the Builder SDK ships no error boundary on its
|
|
22
|
+
* node/edge builds. So the family's contract is NEVER THROW — see `asText`.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Coerce an untyped value to renderable text. The rule is deliberately narrow:
|
|
26
|
+
* • a string passes through untouched — every helper below preserves its exact string
|
|
27
|
+
* semantics, because `escapeHtml` is the ONLY XSS boundary four hand-built-HTML
|
|
28
|
+
* renderers have and any change to its string output is a security regression;
|
|
29
|
+
* • a finite number renders as its digits (a stray count is at least legible copy);
|
|
30
|
+
* • EVERYTHING ELSE — null, undefined, booleans, objects, arrays, functions, symbols,
|
|
31
|
+
* NaN/Infinity — renders as empty.
|
|
32
|
+
*
|
|
33
|
+
* The last clause is the important one, and it is why this is not `String(input)`. Blind
|
|
34
|
+
* coercion would splice `[object Object]` (or a hostile `toString`'s payload) into the
|
|
35
|
+
* article body, and on a null-prototype object it throws the very TypeError this exists to
|
|
36
|
+
* prevent. A value that is not text is not copy; the correct render of it is nothing.
|
|
37
|
+
*
|
|
38
|
+
* Exported for `format.ts` only — so a helper there can't drift back to a bare dereference —
|
|
39
|
+
* and deliberately NOT re-exported from the package entry: an internal invariant, not a
|
|
40
|
+
* contract surface consumers should build on.
|
|
15
41
|
*/
|
|
42
|
+
export declare function asText(input: unknown): string;
|
|
16
43
|
/** Escape text for safe inclusion in HTML element content or double-quoted attributes. */
|
|
17
|
-
export declare function escapeHtml(input: string): string;
|
|
44
|
+
export declare function escapeHtml(input: string | null | undefined): string;
|
|
18
45
|
/** Alias kept explicit at call sites where the value lands in an attribute. */
|
|
19
46
|
export declare const escapeAttribute: typeof escapeHtml;
|
|
20
47
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/components/escape.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/components/escape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAI7C;AAUD,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAEnE;AAED,+EAA+E;AAC/E,eAAO,MAAM,eAAe,mBAAa,CAAA;AAEzC;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAW3E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAO5E;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAEjE"}
|
|
@@ -12,7 +12,40 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Everything here is pure and runtime-agnostic (no DOM, no Node APIs) so it runs in a
|
|
14
14
|
* server component, an edge runtime, and the Web Component identically.
|
|
15
|
+
*
|
|
16
|
+
* Every helper here is TYPE-HOSTILE-SAFE, and that is load-bearing rather than defensive
|
|
17
|
+
* habit: these run downstream of `get-verified-envelope.ts`'s bare
|
|
18
|
+
* `JSON.parse(...) as CertDeliveryEnvelope` cast, and `verifyEnvelope` checks the SIGNATURE,
|
|
19
|
+
* never the SHAPE — so a number / object / array arrives where the contract promised a
|
|
20
|
+
* string. A `TypeError` here is not a missing badge: under `renderToReadableStream` it aborts
|
|
21
|
+
* the whole article route with zero HTML, and the Builder SDK ships no error boundary on its
|
|
22
|
+
* node/edge builds. So the family's contract is NEVER THROW — see `asText`.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Coerce an untyped value to renderable text. The rule is deliberately narrow:
|
|
26
|
+
* • a string passes through untouched — every helper below preserves its exact string
|
|
27
|
+
* semantics, because `escapeHtml` is the ONLY XSS boundary four hand-built-HTML
|
|
28
|
+
* renderers have and any change to its string output is a security regression;
|
|
29
|
+
* • a finite number renders as its digits (a stray count is at least legible copy);
|
|
30
|
+
* • EVERYTHING ELSE — null, undefined, booleans, objects, arrays, functions, symbols,
|
|
31
|
+
* NaN/Infinity — renders as empty.
|
|
32
|
+
*
|
|
33
|
+
* The last clause is the important one, and it is why this is not `String(input)`. Blind
|
|
34
|
+
* coercion would splice `[object Object]` (or a hostile `toString`'s payload) into the
|
|
35
|
+
* article body, and on a null-prototype object it throws the very TypeError this exists to
|
|
36
|
+
* prevent. A value that is not text is not copy; the correct render of it is nothing.
|
|
37
|
+
*
|
|
38
|
+
* Exported for `format.ts` only — so a helper there can't drift back to a bare dereference —
|
|
39
|
+
* and deliberately NOT re-exported from the package entry: an internal invariant, not a
|
|
40
|
+
* contract surface consumers should build on.
|
|
15
41
|
*/
|
|
42
|
+
export function asText(input) {
|
|
43
|
+
if (typeof input === 'string')
|
|
44
|
+
return input;
|
|
45
|
+
if (typeof input === 'number')
|
|
46
|
+
return Number.isFinite(input) ? String(input) : '';
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
16
49
|
const HTML_ESCAPES = {
|
|
17
50
|
'&': '&',
|
|
18
51
|
'<': '<',
|
|
@@ -22,7 +55,7 @@ const HTML_ESCAPES = {
|
|
|
22
55
|
};
|
|
23
56
|
/** Escape text for safe inclusion in HTML element content or double-quoted attributes. */
|
|
24
57
|
export function escapeHtml(input) {
|
|
25
|
-
return input.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
58
|
+
return asText(input).replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
26
59
|
}
|
|
27
60
|
/** Alias kept explicit at call sites where the value lands in an attribute. */
|
|
28
61
|
export const escapeAttribute = escapeHtml;
|
|
@@ -33,9 +66,7 @@ export const escapeAttribute = escapeHtml;
|
|
|
33
66
|
* Callers treat null as "omit the link".
|
|
34
67
|
*/
|
|
35
68
|
export function safeHttpUrl(input) {
|
|
36
|
-
|
|
37
|
-
return null;
|
|
38
|
-
const trimmed = input.trim();
|
|
69
|
+
const trimmed = asText(input).trim();
|
|
39
70
|
if (trimmed === '')
|
|
40
71
|
return null;
|
|
41
72
|
// Site-relative or protocol-relative URLs are safe (no scheme to abuse).
|
|
@@ -56,9 +87,7 @@ export function safeHttpUrl(input) {
|
|
|
56
87
|
* `url(...)`, or break out of the style attribute. Returns null on anything suspicious.
|
|
57
88
|
*/
|
|
58
89
|
export function safeCssColor(input) {
|
|
59
|
-
|
|
60
|
-
return null;
|
|
61
|
-
const v = input.trim();
|
|
90
|
+
const v = asText(input).trim();
|
|
62
91
|
if (v === '')
|
|
63
92
|
return null;
|
|
64
93
|
if (/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(v))
|
|
@@ -71,6 +100,6 @@ export function safeCssColor(input) {
|
|
|
71
100
|
}
|
|
72
101
|
/** Collapse interior whitespace + trim. Used to normalize display text. */
|
|
73
102
|
export function tidyText(input) {
|
|
74
|
-
return (input
|
|
103
|
+
return asText(input).replace(/\s+/g, ' ').trim();
|
|
75
104
|
}
|
|
76
105
|
//# sourceMappingURL=escape.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../../src/components/escape.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../../src/components/escape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,MAAM,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACjF,OAAO,EAAE,CAAA;AACV,CAAC;AAED,MAAM,YAAY,GAA2B;IAC5C,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,OAAO;CACZ,CAAA;AAED,0FAA0F;AAC1F,MAAM,UAAU,UAAU,CAAC,KAAgC;IAC1D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;AACtE,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAA;AAEzC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgC;IAC3D,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAA;IACpC,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IAC/B,yEAAyE;IACzE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAA;IACjG,IAAI,CAAC;QACJ,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;QAC1B,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAC/E,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAA;IACZ,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAgC;IAC5D,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAA;IAC9B,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACzB,IAAI,uDAAuD,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IAC7E,IAAI,4CAA4C,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IAClE,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IACxC,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,KAAgC;IACxD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AACjD,CAAC"}
|
|
@@ -27,8 +27,57 @@ export interface ResolvedDisplay {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function resolveDisplay(display: CertDisplayConfig | undefined, accentOverride?: string, renderDef?: CertRenderDef): ResolvedDisplay;
|
|
29
29
|
export declare function formatDate(iso: string | null | undefined): string | null;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* "PhD, RD" — the expert's credential abbreviations, comma-joined.
|
|
32
|
+
*
|
|
33
|
+
* Shape-guarded because the envelope reaching here was only SIGNATURE-checked: a signed
|
|
34
|
+
* payload whose `credentials` is absent or is not an array used to throw
|
|
35
|
+
* `TypeError: ...map is not a function` and take the host route down with it. An entry with
|
|
36
|
+
* no usable abbreviation is DROPPED rather than joined as empty — the old code rendered
|
|
37
|
+
* "Dr. Jane Doe, , RD" off one missing field. A well-formed envelope is byte-identical.
|
|
38
|
+
*/
|
|
31
39
|
export declare function credentialSuffix(content: CertContent): string;
|
|
32
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Drop from `credential` any token the display NAME already prints, so a name that arrives
|
|
42
|
+
* carrying its own post-nominal doesn't get it appended a second time — the defect that put
|
|
43
|
+
* "Dr. Erik Schraga, MD, MD, EM" on a live customer page (P2-6).
|
|
44
|
+
*
|
|
45
|
+
* THE ROOT CAUSE IS UPSTREAM DATA, NOT THIS FUNCTION. Portal cleans an expert display name
|
|
46
|
+
* with `cleanExpertDisplayName` and has DB CHECK constraints for exactly this shape
|
|
47
|
+
* (migration 20260506151204), but `resolveStampReviewerName` skips the cleaner and
|
|
48
|
+
* `package_drafts.expert_display_name` carries no constraint — so the duplicate is STORED,
|
|
49
|
+
* and a consumer reading the Delivery API directly still receives it. This is the render
|
|
50
|
+
* boundary refusing to print the same claim twice, nothing more.
|
|
51
|
+
*
|
|
52
|
+
* THE RULE, and why it is drawn this narrowly — a false positive here would delete a REAL
|
|
53
|
+
* credential from a compliance-facing card, which is worse than the duplicate it fixes:
|
|
54
|
+
* • a credential token is dropped only when it equals a WHOLE comma-separated segment of
|
|
55
|
+
* the name. Never a substring: "MD" must survive against "Dr. MDonald", "Jane Amdahl"
|
|
56
|
+
* and "Md. Rahman", all of which a naive `name.includes(credential)` would gut;
|
|
57
|
+
* • comparison is `credentialKey`'d, so "M.D." in the name cancels "MD" in the credential;
|
|
58
|
+
* • the credential may be a LIST ("MD, FAAD"): each token is judged on its own, and the
|
|
59
|
+
* survivors keep their original spelling and order. A name carrying only "MD" therefore
|
|
60
|
+
* still renders ", FAAD" — no verified credential is ever lost to a partial overlap;
|
|
61
|
+
* • nothing duplicated ⇒ the credential string is returned VERBATIM (not re-joined), so
|
|
62
|
+
* the overwhelmingly common case is byte-identical to the pre-guard render.
|
|
63
|
+
*
|
|
64
|
+
* What it deliberately does NOT do: it never edits the NAME. "Dr. Erik Schraga, MD" keeps its
|
|
65
|
+
* inline "MD" — un-embedding a post-nominal is `cleanExpertDisplayName`'s job on the write
|
|
66
|
+
* path, and guessing at it here would be the same false-positive class in the other direction.
|
|
67
|
+
* It also cannot catch a duplicate spelled differently in the two fields ("Doctor of Medicine"
|
|
68
|
+
* in the name against "MD" in the credential) or one embedded mid-name rather than as its own
|
|
69
|
+
* segment ("Erik Schraga MD" with no comma) — both stay for the upstream fix.
|
|
70
|
+
*/
|
|
71
|
+
export declare function dedupeCredential(name: unknown, credential: unknown): string;
|
|
72
|
+
/**
|
|
73
|
+
* "Dr. Jane Doe, PhD, RD" — display name with credential suffix appended.
|
|
74
|
+
*
|
|
75
|
+
* With no name there is nobody to attribute the review to, so the whole label is empty:
|
|
76
|
+
* credentials alone would read as "Content reviewed by PhD, RD" in `CertBadge`'s aria-label.
|
|
77
|
+
*
|
|
78
|
+
* The suffix passes `dedupeCredential` because this composes name AND credential into one
|
|
79
|
+
* string; `credentialSuffix` above stays the full verified list, so a caller rendering the
|
|
80
|
+
* credentials AWAY from the name (a chip row, a standalone line) still gets every one of them.
|
|
81
|
+
*/
|
|
33
82
|
export declare function expertNameWithCredentials(content: CertContent): string;
|
|
34
83
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/components/format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/components/format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAE3E,OAAO,EAAE,KAAK,aAAa,EAAqC,MAAM,iBAAiB,CAAA;AAEvF,iEAAiE;AACjE,eAAO,MAAM,cAAc,YAAY,CAAA;AAEvC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;CACvC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,iBAAiB,GAAG,SAAS,EACtC,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,aAAa,GACvB,eAAe,CASjB;AAiBD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAMxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAO7D;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAY3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAKtE"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Presentation helpers shared by the React components + Web Component. Pure, no DOM,
|
|
3
3
|
* no React — so the same formatting drives JSX and the hand-built Web Component string.
|
|
4
4
|
*/
|
|
5
|
+
import { asText } from './escape.js';
|
|
5
6
|
import { isFieldHidden, resolveRenderTheme } from './render-def.js';
|
|
6
7
|
/** Default accent if the brand didn't pin one (CertREV teal). */
|
|
7
8
|
export const DEFAULT_ACCENT = '#0f766e';
|
|
@@ -32,10 +33,17 @@ export function resolveDisplay(display, accentOverride, renderDef) {
|
|
|
32
33
|
* We deliberately DON'T use `toLocaleDateString` — its output varies by runtime ICU data,
|
|
33
34
|
* which would make SSR output non-deterministic + cause hydration mismatches. Fixed
|
|
34
35
|
* English month abbreviations keep server + client byte-identical.
|
|
36
|
+
*
|
|
37
|
+
* ISO-8601 STRINGS ONLY — a non-string yields null. `Date.parse` coerces its argument, so an
|
|
38
|
+
* unguarded number sailed straight through as a YEAR: `formatDate(42)` returned "Jan 1, 2042"
|
|
39
|
+
* off a malformed envelope, while the number a caller would actually MEAN (epoch ms,
|
|
40
|
+
* 1750000000000) returned null. A date helper that invents a confident wrong certification
|
|
41
|
+
* date is worse than one that renders nothing, and the wire contract (`certifiedAt`,
|
|
42
|
+
* `contentModifiedAt`) is ISO-8601 strings — there is no second input format to honour.
|
|
35
43
|
*/
|
|
36
44
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
37
45
|
export function formatDate(iso) {
|
|
38
|
-
if (
|
|
46
|
+
if (typeof iso !== 'string' || iso === '')
|
|
39
47
|
return null;
|
|
40
48
|
const ms = Date.parse(iso);
|
|
41
49
|
if (Number.isNaN(ms))
|
|
@@ -43,13 +51,102 @@ export function formatDate(iso) {
|
|
|
43
51
|
const d = new Date(ms);
|
|
44
52
|
return `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
|
|
45
53
|
}
|
|
46
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* "PhD, RD" — the expert's credential abbreviations, comma-joined.
|
|
56
|
+
*
|
|
57
|
+
* Shape-guarded because the envelope reaching here was only SIGNATURE-checked: a signed
|
|
58
|
+
* payload whose `credentials` is absent or is not an array used to throw
|
|
59
|
+
* `TypeError: ...map is not a function` and take the host route down with it. An entry with
|
|
60
|
+
* no usable abbreviation is DROPPED rather than joined as empty — the old code rendered
|
|
61
|
+
* "Dr. Jane Doe, , RD" off one missing field. A well-formed envelope is byte-identical.
|
|
62
|
+
*/
|
|
47
63
|
export function credentialSuffix(content) {
|
|
48
|
-
|
|
64
|
+
const credentials = content?.expert?.credentials;
|
|
65
|
+
if (!Array.isArray(credentials))
|
|
66
|
+
return '';
|
|
67
|
+
return credentials
|
|
68
|
+
.map((c) => asText(c?.abbreviation))
|
|
69
|
+
.filter((abbreviation) => abbreviation !== '')
|
|
70
|
+
.join(', ');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Split a comma-separated label into its trimmed, non-empty parts.
|
|
74
|
+
* "Dr. Erik Schraga, MD" → `['Dr. Erik Schraga', 'MD']`.
|
|
75
|
+
*/
|
|
76
|
+
function commaTokens(value) {
|
|
77
|
+
return value
|
|
78
|
+
.split(',')
|
|
79
|
+
.map((token) => token.trim())
|
|
80
|
+
.filter((token) => token !== '');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Fold one token to the form two spellings of the SAME credential share: case-insensitive,
|
|
84
|
+
* periods dropped, interior whitespace collapsed. "M.D." / "MD." / "md" → `md`.
|
|
85
|
+
*/
|
|
86
|
+
function credentialKey(token) {
|
|
87
|
+
return token.toLowerCase().replace(/\./g, '').replace(/\s+/g, ' ').trim();
|
|
49
88
|
}
|
|
50
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Drop from `credential` any token the display NAME already prints, so a name that arrives
|
|
91
|
+
* carrying its own post-nominal doesn't get it appended a second time — the defect that put
|
|
92
|
+
* "Dr. Erik Schraga, MD, MD, EM" on a live customer page (P2-6).
|
|
93
|
+
*
|
|
94
|
+
* THE ROOT CAUSE IS UPSTREAM DATA, NOT THIS FUNCTION. Portal cleans an expert display name
|
|
95
|
+
* with `cleanExpertDisplayName` and has DB CHECK constraints for exactly this shape
|
|
96
|
+
* (migration 20260506151204), but `resolveStampReviewerName` skips the cleaner and
|
|
97
|
+
* `package_drafts.expert_display_name` carries no constraint — so the duplicate is STORED,
|
|
98
|
+
* and a consumer reading the Delivery API directly still receives it. This is the render
|
|
99
|
+
* boundary refusing to print the same claim twice, nothing more.
|
|
100
|
+
*
|
|
101
|
+
* THE RULE, and why it is drawn this narrowly — a false positive here would delete a REAL
|
|
102
|
+
* credential from a compliance-facing card, which is worse than the duplicate it fixes:
|
|
103
|
+
* • a credential token is dropped only when it equals a WHOLE comma-separated segment of
|
|
104
|
+
* the name. Never a substring: "MD" must survive against "Dr. MDonald", "Jane Amdahl"
|
|
105
|
+
* and "Md. Rahman", all of which a naive `name.includes(credential)` would gut;
|
|
106
|
+
* • comparison is `credentialKey`'d, so "M.D." in the name cancels "MD" in the credential;
|
|
107
|
+
* • the credential may be a LIST ("MD, FAAD"): each token is judged on its own, and the
|
|
108
|
+
* survivors keep their original spelling and order. A name carrying only "MD" therefore
|
|
109
|
+
* still renders ", FAAD" — no verified credential is ever lost to a partial overlap;
|
|
110
|
+
* • nothing duplicated ⇒ the credential string is returned VERBATIM (not re-joined), so
|
|
111
|
+
* the overwhelmingly common case is byte-identical to the pre-guard render.
|
|
112
|
+
*
|
|
113
|
+
* What it deliberately does NOT do: it never edits the NAME. "Dr. Erik Schraga, MD" keeps its
|
|
114
|
+
* inline "MD" — un-embedding a post-nominal is `cleanExpertDisplayName`'s job on the write
|
|
115
|
+
* path, and guessing at it here would be the same false-positive class in the other direction.
|
|
116
|
+
* It also cannot catch a duplicate spelled differently in the two fields ("Doctor of Medicine"
|
|
117
|
+
* in the name against "MD" in the credential) or one embedded mid-name rather than as its own
|
|
118
|
+
* segment ("Erik Schraga MD" with no comma) — both stay for the upstream fix.
|
|
119
|
+
*/
|
|
120
|
+
export function dedupeCredential(name, credential) {
|
|
121
|
+
const suffix = asText(credential);
|
|
122
|
+
const tokens = commaTokens(suffix);
|
|
123
|
+
if (tokens.length === 0)
|
|
124
|
+
return suffix;
|
|
125
|
+
// Every segment of the name, not just the trailing ones: for a well-formed name the first
|
|
126
|
+
// segment is a human name and can never equal a credential token, and the one case where it
|
|
127
|
+
// does — a stored name that is NOTHING but a credential ("MD") — is exactly the case where
|
|
128
|
+
// dropping the suffix is right, since the token still renders once as the name.
|
|
129
|
+
const carried = new Set(commaTokens(asText(name)).map(credentialKey));
|
|
130
|
+
const kept = tokens.filter((token) => !carried.has(credentialKey(token)));
|
|
131
|
+
if (kept.length === tokens.length)
|
|
132
|
+
return suffix;
|
|
133
|
+
return kept.join(', ');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* "Dr. Jane Doe, PhD, RD" — display name with credential suffix appended.
|
|
137
|
+
*
|
|
138
|
+
* With no name there is nobody to attribute the review to, so the whole label is empty:
|
|
139
|
+
* credentials alone would read as "Content reviewed by PhD, RD" in `CertBadge`'s aria-label.
|
|
140
|
+
*
|
|
141
|
+
* The suffix passes `dedupeCredential` because this composes name AND credential into one
|
|
142
|
+
* string; `credentialSuffix` above stays the full verified list, so a caller rendering the
|
|
143
|
+
* credentials AWAY from the name (a chip row, a standalone line) still gets every one of them.
|
|
144
|
+
*/
|
|
51
145
|
export function expertNameWithCredentials(content) {
|
|
52
|
-
const
|
|
53
|
-
|
|
146
|
+
const name = asText(content?.expert?.displayName);
|
|
147
|
+
if (name === '')
|
|
148
|
+
return '';
|
|
149
|
+
const suffix = dedupeCredential(name, credentialSuffix(content));
|
|
150
|
+
return suffix ? `${name}, ${suffix}` : name;
|
|
54
151
|
}
|
|
55
152
|
//# sourceMappingURL=format.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/components/format.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAsB,aAAa,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEvF,iEAAiE;AACjE,MAAM,CAAC,MAAM,cAAc,GAAG,SAAS,CAAA;AAUvC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAC7B,OAAsC,EACtC,cAAuB,EACvB,SAAyB;IAEzB,MAAM,KAAK,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAA;IAC3C,OAAO;QACN,WAAW,EAAE,cAAc,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,EAAE,WAAW,IAAI,cAAc;QACrF,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,eAAe,CAAC;QACjG,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,IAAI;QACvC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;QAC1E,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,MAAM;KACzC,CAAA;AACF,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/components/format.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `renderPreviewBlock` — a STATIC, NON-DELIVERY preview of the cert block for pre-sales
|
|
3
|
-
* look-demos (
|
|
3
|
+
* look-demos (lifted from the Builder demo).
|
|
4
4
|
*
|
|
5
5
|
* The visual is BYTE-FOR-BYTE `renderCertBlock` (pixel-parity — "see it on your own site
|
|
6
6
|
* today"), wrapped in a `[data-certrev-preview]` container carrying a REQUIRED expiry.
|