@certrev/cert-block 0.2.0 → 0.5.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/README.md +48 -1
- package/dist/builder/cert-chrome-data.d.ts +129 -0
- package/dist/builder/cert-chrome-data.d.ts.map +1 -0
- package/dist/builder/cert-chrome-data.js +59 -0
- package/dist/builder/cert-chrome-data.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +68 -0
- package/dist/builder/cert-review-card.d.ts.map +1 -0
- package/dist/builder/cert-review-card.js +129 -0
- package/dist/builder/cert-review-card.js.map +1 -0
- package/dist/builder/index.d.ts +2 -0
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +3 -0
- package/dist/builder/index.js.map +1 -1
- package/dist/components/preview-block.d.ts +43 -0
- package/dist/components/preview-block.d.ts.map +1 -0
- package/dist/components/preview-block.js +53 -0
- package/dist/components/preview-block.js.map +1 -0
- package/dist/components/render-cert-block.d.ts +50 -5
- package/dist/components/render-cert-block.d.ts.map +1 -1
- package/dist/components/render-cert-block.js +341 -93
- package/dist/components/render-cert-block.js.map +1 -1
- package/dist/components/render-def.d.ts +27 -0
- package/dist/components/render-def.d.ts.map +1 -1
- package/dist/components/render-def.js +38 -0
- package/dist/components/render-def.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/modal/avatar-resize.d.ts +60 -0
- package/dist/modal/avatar-resize.d.ts.map +1 -0
- package/dist/modal/avatar-resize.js +91 -0
- package/dist/modal/avatar-resize.js.map +1 -0
- package/dist/modal/cert-modal-view.d.ts +74 -0
- package/dist/modal/cert-modal-view.d.ts.map +1 -0
- package/dist/modal/cert-modal-view.js +160 -0
- package/dist/modal/cert-modal-view.js.map +1 -0
- package/dist/modal/certrev-cert-modal.d.ts +72 -0
- package/dist/modal/certrev-cert-modal.d.ts.map +1 -0
- package/dist/modal/certrev-cert-modal.js +270 -0
- package/dist/modal/certrev-cert-modal.js.map +1 -0
- package/dist/modal/certrev-cert-modal.styles.d.ts +35 -0
- package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -0
- package/dist/modal/certrev-cert-modal.styles.js +488 -0
- package/dist/modal/certrev-cert-modal.styles.js.map +1 -0
- package/dist/modal/certrev-cert.entry.d.ts +19 -0
- package/dist/modal/certrev-cert.entry.d.ts.map +1 -0
- package/dist/modal/certrev-cert.entry.js +39 -0
- package/dist/modal/certrev-cert.entry.js.map +1 -0
- package/dist/modal/certrev-cert.js +557 -0
- package/dist/modal/display-strings.d.ts +22 -0
- package/dist/modal/display-strings.d.ts.map +1 -0
- package/dist/modal/display-strings.js +35 -0
- package/dist/modal/display-strings.js.map +1 -0
- package/dist/modal/escape.d.ts +13 -0
- package/dist/modal/escape.d.ts.map +1 -0
- package/dist/modal/escape.js +20 -0
- package/dist/modal/escape.js.map +1 -0
- package/dist/modal/font-face.d.ts +4 -0
- package/dist/modal/font-face.d.ts.map +1 -0
- package/dist/modal/font-face.js +137 -0
- package/dist/modal/font-face.js.map +1 -0
- package/dist/modal/ftc-disclosure.d.ts +12 -0
- package/dist/modal/ftc-disclosure.d.ts.map +1 -0
- package/dist/modal/ftc-disclosure.js +12 -0
- package/dist/modal/ftc-disclosure.js.map +1 -0
- package/dist/modal/ftc-guard.d.ts +50 -0
- package/dist/modal/ftc-guard.d.ts.map +1 -0
- package/dist/modal/ftc-guard.js +117 -0
- package/dist/modal/ftc-guard.js.map +1 -0
- package/dist/modal/index.d.ts +20 -0
- package/dist/modal/index.d.ts.map +1 -0
- package/dist/modal/index.js +25 -0
- package/dist/modal/index.js.map +1 -0
- package/dist/modal/interactions.d.ts +53 -0
- package/dist/modal/interactions.d.ts.map +1 -0
- package/dist/modal/interactions.js +147 -0
- package/dist/modal/interactions.js.map +1 -0
- package/dist/modal/logo-paths.d.ts +15 -0
- package/dist/modal/logo-paths.d.ts.map +1 -0
- package/dist/modal/logo-paths.js +24 -0
- package/dist/modal/logo-paths.js.map +1 -0
- package/dist/modal/placement.d.ts +95 -0
- package/dist/modal/placement.d.ts.map +1 -0
- package/dist/modal/placement.js +302 -0
- package/dist/modal/placement.js.map +1 -0
- package/dist/modal/revalidate.d.ts +48 -0
- package/dist/modal/revalidate.d.ts.map +1 -0
- package/dist/modal/revalidate.js +65 -0
- package/dist/modal/revalidate.js.map +1 -0
- package/dist/modal/verify-client.d.ts +24 -0
- package/dist/modal/verify-client.d.ts.map +1 -0
- package/dist/modal/verify-client.js +38 -0
- package/dist/modal/verify-client.js.map +1 -0
- package/package.json +10 -4
- package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +10 -6
- package/src/__tests__/preview-block.test.ts +84 -0
- package/src/__tests__/render-cert-block.test.ts +404 -6
- package/src/builder/__tests__/builder.test.tsx +71 -0
- package/src/builder/cert-chrome-data.ts +159 -0
- package/src/builder/cert-review-card.tsx +177 -0
- package/src/builder/index.tsx +18 -0
- package/src/components/preview-block.ts +74 -0
- package/src/components/render-cert-block.ts +427 -121
- package/src/components/render-def.ts +55 -0
- package/src/index.ts +9 -0
- package/src/modal/avatar-resize.ts +99 -0
- package/src/modal/cert-modal-view.test.ts +175 -0
- package/src/modal/cert-modal-view.ts +210 -0
- package/src/modal/certrev-cert-modal.styles.ts +494 -0
- package/src/modal/certrev-cert-modal.test.ts +125 -0
- package/src/modal/certrev-cert-modal.ts +273 -0
- package/src/modal/certrev-cert.entry.ts +44 -0
- package/src/modal/display-strings.ts +38 -0
- package/src/modal/escape.ts +20 -0
- package/src/modal/font-face.ts +138 -0
- package/src/modal/ftc-disclosure.ts +13 -0
- package/src/modal/ftc-guard.test.ts +153 -0
- package/src/modal/ftc-guard.ts +119 -0
- package/src/modal/index.ts +34 -0
- package/src/modal/interactions.test.ts +250 -0
- package/src/modal/interactions.ts +166 -0
- package/src/modal/logo-paths.ts +23 -0
- package/src/modal/placement.test.ts +398 -0
- package/src/modal/placement.ts +331 -0
- package/src/modal/revalidate.test.ts +111 -0
- package/src/modal/revalidate.ts +81 -0
- package/src/modal/verify-client.test.ts +164 -0
- package/src/modal/verify-client.ts +42 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure envelope → modal-HTML mapping for `<certrev-cert-modal>` (POR-10102).
|
|
3
|
+
*
|
|
4
|
+
* DOM-free and side-effect-free so it unit-tests without a browser: given the
|
|
5
|
+
* signed envelope's `payload.content`, it returns the inner HTML of the certificate
|
|
6
|
+
* (3a) and reviewer (4b) `<dialog>`s. The element (`certrev-cert-modal.ts`) owns the
|
|
7
|
+
* shadow root, the `<dialog>` wrappers, and the open/close wiring; this module owns
|
|
8
|
+
* only the content.
|
|
9
|
+
*
|
|
10
|
+
* Invariants:
|
|
11
|
+
* - Every dynamic value is `escapeHtml`'d (the envelope is signed, but a reviewer
|
|
12
|
+
* name / memo is still attacker-adjacent content).
|
|
13
|
+
* - Only `http(s)` hrefs render (mirrors the Liquid `certrev-safe-url` guard).
|
|
14
|
+
* - Verified-only fields (POR-10091): the bio binds the envelope's verified
|
|
15
|
+
* `background`/`bio`; this module never reaches for a raw column.
|
|
16
|
+
* - Graceful degradation: a missing article title drops the "Certifies the article"
|
|
17
|
+
* block; a missing display cert id drops that meta column; no credentials → no
|
|
18
|
+
* chips / no creds line; no bio → no bio paragraph. Nothing renders an empty gap.
|
|
19
|
+
* - WS4a (display eviction) accept-on-read: an ABSENT `display` block resolves to the
|
|
20
|
+
* preset default (show-all) — every read below is `display?.x !== false` / `=== false`,
|
|
21
|
+
* so `undefined` never suppresses a field. The modal is the LOCKED navy trust chrome and
|
|
22
|
+
* is deliberately NOT themeable by a brand render-def; it reads visibility off the signed
|
|
23
|
+
* envelope only, so no render-def wiring belongs here.
|
|
24
|
+
*/
|
|
25
|
+
import { escapeHtml } from './escape.js';
|
|
26
|
+
import { resizeAvatarUrl } from './avatar-resize.js';
|
|
27
|
+
import { CERT_SCOPE_LINE, formatCertDisplayDate } from './display-strings.js';
|
|
28
|
+
import { checkIcon, chevronMark, verifiedStamp } from './certrev-cert-modal.styles.js';
|
|
29
|
+
const HONORIFIC_RE = /^(dr|prof|professor|mr|mrs|ms|mx|rev|sir|dame)\.?\s+/i;
|
|
30
|
+
/** The bare given+family name: strip the leading honorific AND the trailing post-nominal
|
|
31
|
+
* (`, MD`), so "Dr. Erik Schraga, MD" → "Erik Schraga". */
|
|
32
|
+
function coreName(displayName) {
|
|
33
|
+
const withoutHonorific = displayName.replace(HONORIFIC_RE, '').trim();
|
|
34
|
+
const comma = withoutHonorific.indexOf(',');
|
|
35
|
+
return (comma === -1 ? withoutHonorific : withoutHonorific.slice(0, comma)).trim();
|
|
36
|
+
}
|
|
37
|
+
/** Two-letter initials — first + last of the core name (honorific + post-nominal stripped),
|
|
38
|
+
* so "Dr. Erik Schraga, MD" → "ES" (matches the Liquid derivation), not "EM". */
|
|
39
|
+
export function deriveInitials(name) {
|
|
40
|
+
const words = coreName(name).split(/\s+/).filter(Boolean);
|
|
41
|
+
if (words.length === 0)
|
|
42
|
+
return '';
|
|
43
|
+
if (words.length === 1)
|
|
44
|
+
return words[0].slice(0, 2).toUpperCase();
|
|
45
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
46
|
+
}
|
|
47
|
+
/** The handwritten-signature form (the core name). */
|
|
48
|
+
export function signatureName(displayName) {
|
|
49
|
+
return coreName(displayName);
|
|
50
|
+
}
|
|
51
|
+
/** `https://certrev.com/verify/x` → `certrev.com/verify/x` (footer URL line). */
|
|
52
|
+
export function stripProtocol(url) {
|
|
53
|
+
return url.replace(/^https?:\/\//i, '').replace(/\/$/, '');
|
|
54
|
+
}
|
|
55
|
+
/** Return the url only if it is an absolute http(s) URL, else `#` (safe-url guard). */
|
|
56
|
+
export function safeHref(url) {
|
|
57
|
+
if (!url)
|
|
58
|
+
return '#';
|
|
59
|
+
return /^https?:\/\//i.test(url.trim()) ? url.trim() : '#';
|
|
60
|
+
}
|
|
61
|
+
/** A single credential's display label (cardLabel ?? fullName ?? abbreviation). */
|
|
62
|
+
function credLabel(c) {
|
|
63
|
+
return (c.cardLabel || c.fullName || c.abbreviation || '').trim();
|
|
64
|
+
}
|
|
65
|
+
/** The verified credential labels, in order, de-duped + non-empty. */
|
|
66
|
+
function credLabels(expert) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const c of expert?.credentials ?? []) {
|
|
69
|
+
const label = credLabel(c);
|
|
70
|
+
if (label && !out.includes(label))
|
|
71
|
+
out.push(label);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** The reviewer bio — verified background preferred, then verified bio. */
|
|
76
|
+
function bioText(expert, display) {
|
|
77
|
+
if (display?.showBio === false)
|
|
78
|
+
return '';
|
|
79
|
+
return (expert?.background || expert?.bio || '').trim();
|
|
80
|
+
}
|
|
81
|
+
/** An avatar cell — the cert-frozen photo (when shown) or the initials chip. */
|
|
82
|
+
function avatarHtml(expert, name, sizeClass, showPhoto) {
|
|
83
|
+
const photo = (expert?.photoUrl || '').trim();
|
|
84
|
+
if (showPhoto && /^https?:\/\//i.test(photo)) {
|
|
85
|
+
// Display px from the size class; request the @2x derivative for retina crispness (POR-10470).
|
|
86
|
+
// resizeAvatarUrl is host-guarded + enablement-gated: until Cloudflare Image Resizing is live it
|
|
87
|
+
// returns the raw canonical URL, so the modal never 404s an unresized host.
|
|
88
|
+
const displayPx = sizeClass.includes('--64') ? 64 : 56;
|
|
89
|
+
const src = resizeAvatarUrl(photo, displayPx === 64 ? 128 : 112);
|
|
90
|
+
return `<img class="crm-avatar ${sizeClass}" src="${escapeHtml(src)}" alt="" width="${displayPx}" height="${displayPx}" decoding="async">`;
|
|
91
|
+
}
|
|
92
|
+
return `<span class="crm-avatar ${sizeClass}">${escapeHtml(deriveInitials(name))}</span>`;
|
|
93
|
+
}
|
|
94
|
+
const CLOSE_BUTTON = `<button type="button" class="crm-close" data-certrev-modal-close aria-label="Close">×</button>`;
|
|
95
|
+
function headHtml(eyebrow) {
|
|
96
|
+
return `<div class="crm-head">${chevronMark(16, '#ffffff')}<span class="crm-head__eyebrow">${escapeHtml(eyebrow)}</span><span class="crm-head__wordmark">CertREV</span></div>`;
|
|
97
|
+
}
|
|
98
|
+
function certifiedChip(cls) {
|
|
99
|
+
return `<span class="${cls}">${checkIcon(8)}Certified</span>`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The FTC disclosure footer (POR-10496), rendered on both dialogs. The material-connection cue
|
|
103
|
+
* (`expert.compensationCue`, a FINISHED string off the signed envelope) leads ONLY when the reviewer
|
|
104
|
+
* was compensated; a pro-bono reviewer (null/absent cue) drops the label but keeps the scope line —
|
|
105
|
+
* which makes no compensation claim, so it is true for volunteers too and renders unconditionally.
|
|
106
|
+
* The scope line is the verbatim FTC copy (`CERT_SCOPE_LINE`), never re-assembled here.
|
|
107
|
+
*/
|
|
108
|
+
function disclosureFooter(expert) {
|
|
109
|
+
const cue = (expert?.compensationCue || '').trim();
|
|
110
|
+
const cueHtml = cue ? `<span class="crm-disclosure__cue">${escapeHtml(cue)}</span> ` : '';
|
|
111
|
+
return `<div class="crm-disclosure">${cueHtml}${escapeHtml(CERT_SCOPE_LINE)}</div>`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The certificate modal (3a) inner HTML. Returns `null` when there is no reviewer
|
|
115
|
+
* name to display — a certificate card must never render anonymous (fail-closed,
|
|
116
|
+
* mirrors the Liquid `ename != ''` gate).
|
|
117
|
+
*/
|
|
118
|
+
export function renderCertDialogInner(content) {
|
|
119
|
+
const expert = content.expert ?? null;
|
|
120
|
+
const name = (expert?.displayName || '').trim();
|
|
121
|
+
if (!name)
|
|
122
|
+
return null;
|
|
123
|
+
const showPhoto = content.display?.showExpertPhoto !== false;
|
|
124
|
+
const creds = credLabels(expert).join(' · ');
|
|
125
|
+
const issued = formatCertDisplayDate(content.certifiedAt) ?? '';
|
|
126
|
+
const certId = (content.displayCertId || '').trim();
|
|
127
|
+
const title = (content.articleTitle || '').trim();
|
|
128
|
+
const verifyUrl = (content.verifyUrl || '').trim();
|
|
129
|
+
const sig = signatureName(name);
|
|
130
|
+
const certifiesBlock = title
|
|
131
|
+
? `<div class="crm-certifies"><div class="crm-certifies__eyebrow">Certifies the article</div><div class="crm-certifies__title">${escapeHtml(title)}</div></div>`
|
|
132
|
+
: '';
|
|
133
|
+
const certIdCol = certId
|
|
134
|
+
? `<div class="crm-meta__col"><div class="crm-meta__label">Certificate ID</div><div class="crm-meta__value crm-meta__value--mono">${escapeHtml(certId)}</div></div>`
|
|
135
|
+
: '';
|
|
136
|
+
const urlLine = verifyUrl ? `<div class="crm-sig__url">${escapeHtml(stripProtocol(verifyUrl))}</div>` : '';
|
|
137
|
+
return `${CLOSE_BUTTON}${headHtml('Certificate of Expert Review')}<div class="crm-body--cert"><div class="crm-expert-row">${avatarHtml(expert, name, 'crm-avatar--56', showPhoto)}<div class="crm-expert-row__info"><div class="crm-expert-row__eyebrow">Reviewing Expert</div><div class="crm-expert-row__name">${escapeHtml(name)}</div>${creds ? `<div class="crm-expert-row__creds">${escapeHtml(creds)}</div>` : ''}</div></div>${certifiesBlock}<div class="crm-meta"><div class="crm-meta__col"><div class="crm-meta__label">Issued</div><div class="crm-meta__value">${escapeHtml(issued)}</div></div>${certIdCol}<div class="crm-meta__col"><div class="crm-meta__label">Status</div><div class="crm-meta__status">${certifiedChip('crm-chip')}</div></div></div><div class="crm-sig"><div><div class="crm-sig__name">${escapeHtml(sig)}</div><div class="crm-sig__rule"></div>${urlLine}</div>${verifiedStamp('cert')}</div>${disclosureFooter(expert)}<div class="crm-actions"><a class="crm-cta" href="${escapeHtml(safeHref(verifyUrl))}" target="_blank" rel="noopener noreferrer">View full certificate</a><button type="button" class="crm-ghost" data-certrev-modal-close>Close</button></div></div>`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The reviewer/profile modal (4b) inner HTML. Returns `null` when there is no
|
|
141
|
+
* reviewer name (fail-closed). The affiliation subtitle is intentionally omitted
|
|
142
|
+
* (no verified data source — POR-10102 decision).
|
|
143
|
+
*/
|
|
144
|
+
export function renderExpertDialogInner(content) {
|
|
145
|
+
const expert = content.expert ?? null;
|
|
146
|
+
const name = (expert?.displayName || '').trim();
|
|
147
|
+
if (!name)
|
|
148
|
+
return null;
|
|
149
|
+
const showPhoto = content.display?.showExpertPhoto !== false;
|
|
150
|
+
const chips = credLabels(expert);
|
|
151
|
+
const bio = bioText(expert, content.display);
|
|
152
|
+
const date = formatCertDisplayDate(content.certifiedAt);
|
|
153
|
+
const chipsHtml = chips.length
|
|
154
|
+
? `<div class="crm-chips">${chips.map((c) => `<span class="crm-chips__chip">${escapeHtml(c)}</span>`).join('')}</div>`
|
|
155
|
+
: '';
|
|
156
|
+
const bioHtml = bio ? `<p class="crm-bio">${escapeHtml(bio)}</p>` : '';
|
|
157
|
+
const trustText = date ? `Independently reviewed · Certified ${escapeHtml(date)}` : 'Independently reviewed';
|
|
158
|
+
return `${CLOSE_BUTTON}${headHtml('Verified reviewer')}<div class="crm-body--expert"><div class="crm-id">${avatarHtml(expert, name, 'crm-avatar--64', showPhoto)}<div class="crm-id__info"><div class="crm-id__name">${escapeHtml(name)}</div></div></div>${chipsHtml}${bioHtml}<div class="crm-trust">${certifiedChip('crm-trust__chip')}<span class="crm-trust__text">${trustText}</span></div>${disclosureFooter(expert)}<div class="crm-actions"><a class="crm-cta" href="${escapeHtml(safeHref(expert?.profileUrl))}" target="_blank" rel="noopener noreferrer">View full profile on CertREV</a><button type="button" class="crm-ghost" data-certrev-modal-close>Close</button></div></div>`;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=cert-modal-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cert-modal-view.js","sourceRoot":"","sources":["../../src/modal/cert-modal-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<certrev-cert-modal>` — the ONE Shadow-DOM element that renders both in-page cert
|
|
3
|
+
* dialogs (certificate 3a + reviewer 4b) across Shopify / WordPress / Builder
|
|
4
|
+
* (POR-10102). Replaces the three drifting per-platform modal implementations.
|
|
5
|
+
*
|
|
6
|
+
* Design:
|
|
7
|
+
* - One element, one OPEN shadow root, two `<dialog>`s (`#cert`, `#expert`). Public
|
|
8
|
+
* `open('cert'|'expert')` → `dialog.showModal()` (guarded). Data is public SSR
|
|
9
|
+
* content already, so an open shadow adds no exposure and eases debugging/tests.
|
|
10
|
+
* - CSS is adopted via a shared constructable stylesheet (falls back to an inline
|
|
11
|
+
* `<style>` on older engines). Shadow scoping means the brand theme can't bleed in
|
|
12
|
+
* and these rules can't leak out — the literal-hex/theme-armor constraints that
|
|
13
|
+
* bind the `body_html` modal do not apply here.
|
|
14
|
+
* - Envelope acquisition, in priority order: (1) a light-DOM
|
|
15
|
+
* `<script type="application/json" data-certrev-envelope>` child (Shopify + WP SSR,
|
|
16
|
+
* parsed lazily on first open — no network, crawl-safe); (2) a `data-envelope`
|
|
17
|
+
* attribute (stories/tests); (3) a Delivery-API fetch from
|
|
18
|
+
* `{data-delivery-api}/api/cert/v1/delivery/{data-platform}/{data-external-id}`
|
|
19
|
+
* (Builder/Hydrogen). The triggers (`data-certrev-modal-open`) stay in the light
|
|
20
|
+
* DOM (badge/memo) and are wired by `interactions.ts`.
|
|
21
|
+
* - Close plumbing (×, Close, backdrop, Esc) lives HERE, not in `interactions.ts`:
|
|
22
|
+
* a click inside a shadow root is retargeted to the host for a document listener,
|
|
23
|
+
* so the document-level delegate can't see the close controls.
|
|
24
|
+
* - Fonts inject at document level on first open (font faces don't scope into a
|
|
25
|
+
* shadow root); deferring to first open avoids loading them on every article view.
|
|
26
|
+
*/
|
|
27
|
+
import { type CertDeliveryArtifact } from '@certrev/cert-contract';
|
|
28
|
+
export declare const CERT_MODAL_TAG = "certrev-cert-modal";
|
|
29
|
+
type ModalKind = 'cert' | 'expert';
|
|
30
|
+
export declare class CertRevCertModal extends HTMLElement {
|
|
31
|
+
private root;
|
|
32
|
+
private rendered;
|
|
33
|
+
private content;
|
|
34
|
+
private fetchInFlight;
|
|
35
|
+
constructor();
|
|
36
|
+
/**
|
|
37
|
+
* Open the given dialog. Returns `true` when this element will handle the open
|
|
38
|
+
* (so a link-shaped trigger should `preventDefault`), `false` when it cannot
|
|
39
|
+
* (no envelope + no fetch source) so the caller lets the `href` fall back.
|
|
40
|
+
*/
|
|
41
|
+
open(kind: ModalKind): boolean;
|
|
42
|
+
/** Parse the synchronous envelope source (embedded JSON / attr) + render once. */
|
|
43
|
+
private ensureRendered;
|
|
44
|
+
private readSyncContent;
|
|
45
|
+
private hasFetchSource;
|
|
46
|
+
private fetchThenShow;
|
|
47
|
+
private renderDialogs;
|
|
48
|
+
private show;
|
|
49
|
+
/**
|
|
50
|
+
* The modal kill-switch (POR-10101): a fetched envelope is REVOKED/EXPIRED. Don't open;
|
|
51
|
+
* mark the host suppressed (mirrors the badge revalidation's `data-certrev-suppressed`) and
|
|
52
|
+
* hide the light-DOM triggers so a stale click can't re-open a pulled cert. Best-effort DOM.
|
|
53
|
+
*/
|
|
54
|
+
private markSuppressed;
|
|
55
|
+
private onShadowClick;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The modal-side lifecycle gate (POR-10101 / POR-10684): a fetched Delivery-API artifact that is
|
|
59
|
+
* REVOKED or EXPIRED must NOT render "verified" content. Two revocation shapes suppress:
|
|
60
|
+
* • the SLIM `CertTombstone` the control plane serves for a revoked placement (POR-10481) —
|
|
61
|
+
* detected by the `kind: 'tombstone'` discriminator via `isTombstone`. It carries NO payload,
|
|
62
|
+
* so a `payload.lifecycle` read alone would MISS it (and `markSuppressed()` would never fire);
|
|
63
|
+
* • a legacy envelope with `lifecycle.revokedAt` set, or any envelope whose `expiresAt` passed.
|
|
64
|
+
* The Delivery API only serves CertREV-signed artifacts, so this shape read is a safe suppress
|
|
65
|
+
* signal for the display surface (the `<certrev-badge>` revalidation runs the full ed25519
|
|
66
|
+
* verdict via `verifyArtifact` — this is the modal's belt-and-suspenders).
|
|
67
|
+
*/
|
|
68
|
+
export declare function isArtifactSuppressed(artifact: CertDeliveryArtifact | null | undefined): boolean;
|
|
69
|
+
/** Register the element once (idempotent — a double script-load must not throw). */
|
|
70
|
+
export declare function defineCertModal(): void;
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=certrev-cert-modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certrev-cert-modal.d.ts","sourceRoot":"","sources":["../../src/modal/certrev-cert-modal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,KAAK,oBAAoB,EAAe,MAAM,wBAAwB,CAAA;AAK/E,eAAO,MAAM,cAAc,uBAAuB,CAAA;AAClD,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AA+ClC,qBAAa,gBAAiB,SAAQ,WAAW;IAChD,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,aAAa,CAA6B;;IAiBlD;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO;IAgB9B,kFAAkF;IAClF,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,cAAc;YAQR,aAAa;IA4C3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,IAAI;IAKZ;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;CAcrB;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAQ/F;AAED,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<certrev-cert-modal>` — the ONE Shadow-DOM element that renders both in-page cert
|
|
3
|
+
* dialogs (certificate 3a + reviewer 4b) across Shopify / WordPress / Builder
|
|
4
|
+
* (POR-10102). Replaces the three drifting per-platform modal implementations.
|
|
5
|
+
*
|
|
6
|
+
* Design:
|
|
7
|
+
* - One element, one OPEN shadow root, two `<dialog>`s (`#cert`, `#expert`). Public
|
|
8
|
+
* `open('cert'|'expert')` → `dialog.showModal()` (guarded). Data is public SSR
|
|
9
|
+
* content already, so an open shadow adds no exposure and eases debugging/tests.
|
|
10
|
+
* - CSS is adopted via a shared constructable stylesheet (falls back to an inline
|
|
11
|
+
* `<style>` on older engines). Shadow scoping means the brand theme can't bleed in
|
|
12
|
+
* and these rules can't leak out — the literal-hex/theme-armor constraints that
|
|
13
|
+
* bind the `body_html` modal do not apply here.
|
|
14
|
+
* - Envelope acquisition, in priority order: (1) a light-DOM
|
|
15
|
+
* `<script type="application/json" data-certrev-envelope>` child (Shopify + WP SSR,
|
|
16
|
+
* parsed lazily on first open — no network, crawl-safe); (2) a `data-envelope`
|
|
17
|
+
* attribute (stories/tests); (3) a Delivery-API fetch from
|
|
18
|
+
* `{data-delivery-api}/api/cert/v1/delivery/{data-platform}/{data-external-id}`
|
|
19
|
+
* (Builder/Hydrogen). The triggers (`data-certrev-modal-open`) stay in the light
|
|
20
|
+
* DOM (badge/memo) and are wired by `interactions.ts`.
|
|
21
|
+
* - Close plumbing (×, Close, backdrop, Esc) lives HERE, not in `interactions.ts`:
|
|
22
|
+
* a click inside a shadow root is retargeted to the host for a document listener,
|
|
23
|
+
* so the document-level delegate can't see the close controls.
|
|
24
|
+
* - Fonts inject at document level on first open (font faces don't scope into a
|
|
25
|
+
* shadow root); deferring to first open avoids loading them on every article view.
|
|
26
|
+
*/
|
|
27
|
+
import { isTombstone } from '@certrev/cert-contract';
|
|
28
|
+
import { CERT_FONT_FACE_CSS, CERT_FONTS_ORIGIN } from './font-face.js';
|
|
29
|
+
import { renderCertDialogInner, renderExpertDialogInner } from './cert-modal-view.js';
|
|
30
|
+
import { CERT_MODAL_CSS } from './certrev-cert-modal.styles.js';
|
|
31
|
+
export const CERT_MODAL_TAG = 'certrev-cert-modal';
|
|
32
|
+
/** Shared constructable stylesheet (one per document lifetime). */
|
|
33
|
+
let sharedSheet = null;
|
|
34
|
+
function supportsConstructableSheets() {
|
|
35
|
+
return (typeof CSSStyleSheet !== 'undefined' &&
|
|
36
|
+
'replaceSync' in CSSStyleSheet.prototype &&
|
|
37
|
+
'adoptedStyleSheets' in Document.prototype);
|
|
38
|
+
}
|
|
39
|
+
function getSharedSheet() {
|
|
40
|
+
if (!supportsConstructableSheets())
|
|
41
|
+
return null;
|
|
42
|
+
if (!sharedSheet) {
|
|
43
|
+
sharedSheet = new CSSStyleSheet();
|
|
44
|
+
sharedSheet.replaceSync(CERT_MODAL_CSS);
|
|
45
|
+
}
|
|
46
|
+
return sharedSheet;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Inject the modal font families once, at document level, on first open — SELF-HOSTED from R2
|
|
50
|
+
* (POR-10657). Declares the shared CERT_FONT_FACE_CSS @font-face block inline instead of loading a
|
|
51
|
+
* render-blocking `fonts.googleapis.com` stylesheet (which also leaked the visitor's IP to Google).
|
|
52
|
+
* @font-face at document level resolves across the shadow boundary, so the shadow-DOM modal picks
|
|
53
|
+
* these up exactly as it did the old Google <link>. A crossorigin preconnect warms the R2 origin
|
|
54
|
+
* (an @font-face fetch is always a CORS request). Idempotent across instances via the marker attr.
|
|
55
|
+
*/
|
|
56
|
+
let fontsInjected = false;
|
|
57
|
+
function injectFontsOnce(doc) {
|
|
58
|
+
if (fontsInjected)
|
|
59
|
+
return;
|
|
60
|
+
fontsInjected = true;
|
|
61
|
+
if (doc.querySelector('style[data-certrev-modal-fonts]'))
|
|
62
|
+
return;
|
|
63
|
+
if (!doc.querySelector('link[data-certrev-modal-preconnect]')) {
|
|
64
|
+
const pre = doc.createElement('link');
|
|
65
|
+
pre.rel = 'preconnect';
|
|
66
|
+
pre.href = CERT_FONTS_ORIGIN;
|
|
67
|
+
pre.crossOrigin = '';
|
|
68
|
+
pre.setAttribute('data-certrev-modal-preconnect', '');
|
|
69
|
+
doc.head.appendChild(pre);
|
|
70
|
+
}
|
|
71
|
+
const style = doc.createElement('style');
|
|
72
|
+
style.textContent = CERT_FONT_FACE_CSS;
|
|
73
|
+
style.setAttribute('data-certrev-modal-fonts', '');
|
|
74
|
+
doc.head.appendChild(style);
|
|
75
|
+
}
|
|
76
|
+
export class CertRevCertModal extends HTMLElement {
|
|
77
|
+
root;
|
|
78
|
+
rendered = false;
|
|
79
|
+
content = null;
|
|
80
|
+
fetchInFlight = null;
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
this.root = this.attachShadow({ mode: 'open' });
|
|
84
|
+
const sheet = getSharedSheet();
|
|
85
|
+
if (sheet) {
|
|
86
|
+
this.root.adoptedStyleSheets = [sheet];
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const style = document.createElement('style');
|
|
90
|
+
style.textContent = CERT_MODAL_CSS;
|
|
91
|
+
this.root.appendChild(style);
|
|
92
|
+
}
|
|
93
|
+
// One delegated close listener inside the shadow root.
|
|
94
|
+
this.root.addEventListener('click', (e) => this.onShadowClick(e));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Open the given dialog. Returns `true` when this element will handle the open
|
|
98
|
+
* (so a link-shaped trigger should `preventDefault`), `false` when it cannot
|
|
99
|
+
* (no envelope + no fetch source) so the caller lets the `href` fall back.
|
|
100
|
+
*/
|
|
101
|
+
open(kind) {
|
|
102
|
+
// A host page that already provides the five families (e.g. the design guide, which
|
|
103
|
+
// self-hosts them) suppresses the modal's font injection with `data-fonts="host"` — so
|
|
104
|
+
// embedding the component adds no font-src/style-src origins to that page's CSP.
|
|
105
|
+
if (this.getAttribute('data-fonts') !== 'host')
|
|
106
|
+
injectFontsOnce(document);
|
|
107
|
+
if (this.ensureRendered()) {
|
|
108
|
+
this.show(kind);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (this.hasFetchSource()) {
|
|
112
|
+
void this.fetchThenShow(kind);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
/** Parse the synchronous envelope source (embedded JSON / attr) + render once. */
|
|
118
|
+
ensureRendered() {
|
|
119
|
+
if (this.rendered)
|
|
120
|
+
return this.content != null;
|
|
121
|
+
const content = this.readSyncContent();
|
|
122
|
+
if (!content)
|
|
123
|
+
return false;
|
|
124
|
+
this.content = content;
|
|
125
|
+
this.renderDialogs(content);
|
|
126
|
+
this.rendered = true;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
readSyncContent() {
|
|
130
|
+
const attr = this.getAttribute('data-envelope');
|
|
131
|
+
if (attr)
|
|
132
|
+
return safeParseContent(attr);
|
|
133
|
+
const script = this.querySelector('script[data-certrev-envelope]');
|
|
134
|
+
if (script?.textContent)
|
|
135
|
+
return safeParseContent(script.textContent);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
hasFetchSource() {
|
|
139
|
+
return !!(this.getAttribute('data-delivery-api') &&
|
|
140
|
+
this.getAttribute('data-platform') &&
|
|
141
|
+
this.getAttribute('data-external-id'));
|
|
142
|
+
}
|
|
143
|
+
async fetchThenShow(kind) {
|
|
144
|
+
if (!this.fetchInFlight) {
|
|
145
|
+
const base = (this.getAttribute('data-delivery-api') || '').replace(/\/$/, '');
|
|
146
|
+
const platform = this.getAttribute('data-platform') || '';
|
|
147
|
+
const externalId = this.getAttribute('data-external-id') || '';
|
|
148
|
+
this.fetchInFlight = (async () => {
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch(`${base}/api/cert/v1/delivery/${encodeURIComponent(platform)}/${encodeURIComponent(externalId)}`, {
|
|
151
|
+
cache: 'no-store',
|
|
152
|
+
credentials: 'omit',
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
return;
|
|
156
|
+
const artifact = (await res.json());
|
|
157
|
+
// A 200 may be a REVOCATION — either the SLIM `CertTombstone` the control plane now
|
|
158
|
+
// serves for a revoked placement (POR-10481, no payload) or a legacy envelope with
|
|
159
|
+
// `revokedAt` set — plus an EXPIRED envelope. The Delivery API serves those signed so
|
|
160
|
+
// the edge SUPPRESSES rather than fail-opening on a 404 (POR-10101). Never render
|
|
161
|
+
// "verified" content from a revoked/expired artifact.
|
|
162
|
+
if (isArtifactSuppressed(artifact)) {
|
|
163
|
+
this.markSuppressed();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Not suppressed → a live envelope (a tombstone always suppresses above); `isTombstone`
|
|
167
|
+
// narrows the union so the payload read is type-safe (POR-10684).
|
|
168
|
+
const content = isTombstone(artifact)
|
|
169
|
+
? undefined
|
|
170
|
+
: artifact.payload?.content;
|
|
171
|
+
if (content) {
|
|
172
|
+
this.content = content;
|
|
173
|
+
this.renderDialogs(content);
|
|
174
|
+
this.rendered = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// fail-open: no modal, the trigger's href (if any) already prevented — a dead click at worst
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
}
|
|
182
|
+
await this.fetchInFlight;
|
|
183
|
+
if (this.rendered)
|
|
184
|
+
this.show(kind);
|
|
185
|
+
}
|
|
186
|
+
renderDialogs(content) {
|
|
187
|
+
const cert = renderCertDialogInner(content);
|
|
188
|
+
const expert = renderExpertDialogInner(content);
|
|
189
|
+
const wrap = document.createElement('div');
|
|
190
|
+
wrap.innerHTML = `${cert ? `<dialog id="cert" class="crm-dialog crm-dialog--cert" aria-label="CertREV certificate">${cert}</dialog>` : ''}${expert ? `<dialog id="expert" class="crm-dialog crm-dialog--expert" aria-label="Verified reviewer profile">${expert}</dialog>` : ''}`;
|
|
191
|
+
// keep the adopted stylesheet / fallback <style>; append the dialogs
|
|
192
|
+
for (const node of Array.from(wrap.childNodes))
|
|
193
|
+
this.root.appendChild(node);
|
|
194
|
+
}
|
|
195
|
+
show(kind) {
|
|
196
|
+
const dlg = this.root.getElementById(kind);
|
|
197
|
+
if (dlg && typeof dlg.showModal === 'function' && !dlg.open)
|
|
198
|
+
dlg.showModal();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The modal kill-switch (POR-10101): a fetched envelope is REVOKED/EXPIRED. Don't open;
|
|
202
|
+
* mark the host suppressed (mirrors the badge revalidation's `data-certrev-suppressed`) and
|
|
203
|
+
* hide the light-DOM triggers so a stale click can't re-open a pulled cert. Best-effort DOM.
|
|
204
|
+
*/
|
|
205
|
+
markSuppressed() {
|
|
206
|
+
this.setAttribute('data-certrev-suppressed', '');
|
|
207
|
+
for (const t of Array.from(document.querySelectorAll('[data-certrev-modal-open]'))) {
|
|
208
|
+
t.setAttribute('hidden', '');
|
|
209
|
+
t.setAttribute('aria-hidden', 'true');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
onShadowClick(e) {
|
|
213
|
+
const target = e.target;
|
|
214
|
+
if (!target)
|
|
215
|
+
return;
|
|
216
|
+
// backdrop click (the click landed directly on the dialog element)
|
|
217
|
+
if (target instanceof HTMLDialogElement && target.classList.contains('crm-dialog')) {
|
|
218
|
+
target.close();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const closer = target.closest('[data-certrev-modal-close]');
|
|
222
|
+
if (closer) {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
closer.closest('dialog')?.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function safeParseContent(json) {
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(json);
|
|
231
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The modal-side lifecycle gate (POR-10101 / POR-10684): a fetched Delivery-API artifact that is
|
|
239
|
+
* REVOKED or EXPIRED must NOT render "verified" content. Two revocation shapes suppress:
|
|
240
|
+
* • the SLIM `CertTombstone` the control plane serves for a revoked placement (POR-10481) —
|
|
241
|
+
* detected by the `kind: 'tombstone'` discriminator via `isTombstone`. It carries NO payload,
|
|
242
|
+
* so a `payload.lifecycle` read alone would MISS it (and `markSuppressed()` would never fire);
|
|
243
|
+
* • a legacy envelope with `lifecycle.revokedAt` set, or any envelope whose `expiresAt` passed.
|
|
244
|
+
* The Delivery API only serves CertREV-signed artifacts, so this shape read is a safe suppress
|
|
245
|
+
* signal for the display surface (the `<certrev-badge>` revalidation runs the full ed25519
|
|
246
|
+
* verdict via `verifyArtifact` — this is the modal's belt-and-suspenders).
|
|
247
|
+
*/
|
|
248
|
+
export function isArtifactSuppressed(artifact) {
|
|
249
|
+
if (!artifact)
|
|
250
|
+
return false;
|
|
251
|
+
if (isTombstone(artifact))
|
|
252
|
+
return true;
|
|
253
|
+
const lc = artifact.payload?.lifecycle;
|
|
254
|
+
if (!lc)
|
|
255
|
+
return false;
|
|
256
|
+
if (lc.revokedAt != null)
|
|
257
|
+
return true;
|
|
258
|
+
if (lc.expiresAt && Date.now() >= new Date(lc.expiresAt).getTime())
|
|
259
|
+
return true;
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
/** Register the element once (idempotent — a double script-load must not throw). */
|
|
263
|
+
export function defineCertModal() {
|
|
264
|
+
if (typeof customElements === 'undefined')
|
|
265
|
+
return;
|
|
266
|
+
if (!customElements.get(CERT_MODAL_TAG))
|
|
267
|
+
customElements.define(CERT_MODAL_TAG, CertRevCertModal);
|
|
268
|
+
}
|
|
269
|
+
defineCertModal();
|
|
270
|
+
//# sourceMappingURL=certrev-cert-modal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certrev-cert-modal.js","sourceRoot":"","sources":["../../src/modal/certrev-cert-modal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped styles + inline SVGs for the `<certrev-cert-modal>` Shadow-DOM element
|
|
3
|
+
* (POR-10102). The single source of truth for the certificate (3a) + reviewer (4b)
|
|
4
|
+
* modal look, ported byte-faithfully from the Claude Design reference
|
|
5
|
+
* (`docs/claude/design/certificate-modals/`, footer variant 3a + reviewer 4b).
|
|
6
|
+
*
|
|
7
|
+
* Because the element renders inside a shadow root, this CSS is fully scoped: the
|
|
8
|
+
* brand storefront theme cannot bleed in, and these rules never leak out — so the
|
|
9
|
+
* old `certrev-cert.css` "theme-bleed armor" and the literal-hex-only constraint
|
|
10
|
+
* (which the `body_html` WordPress modal still needs) do NOT apply here. Tokens are
|
|
11
|
+
* plain custom properties on `:host`; fonts resolve from document-level `@font-face`
|
|
12
|
+
* that the element injects on first construct (font faces do not scope into a shadow
|
|
13
|
+
* root, so they must live at document level — see `certrev-cert-modal.ts`).
|
|
14
|
+
*/
|
|
15
|
+
/** The four-chevron CertREV mark (viewBox 0 0 375 375), tinted via the passed fill. Path data is
|
|
16
|
+
* single-sourced from src/lib/design-guide/logo.ts (the SOT the codegen mirrors into every twin). */
|
|
17
|
+
export declare function chevronMark(size: number, fill: string): string;
|
|
18
|
+
/** The 8px lime-chip check glyph (viewBox 0 0 24 24). */
|
|
19
|
+
export declare function checkIcon(size?: number): string;
|
|
20
|
+
/**
|
|
21
|
+
* The rotated 74×74 verified stamp: two concentric navy rings + a circular
|
|
22
|
+
* `textPath` reading "CERTREV VERIFIED · CERTIFIED ·" + the chevron mark centered.
|
|
23
|
+
* `idSuffix` uniquifies the internal `<path>` id so multiple stamps on one page
|
|
24
|
+
* (or a story grid) don't collide on the shared `href`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function verifiedStamp(idSuffix?: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The five families the modal uses, self-hosted-or-Google (the element injects
|
|
29
|
+
* ONE document-level `<link>`/`@font-face` for these; font-display:swap). Kept as
|
|
30
|
+
* a constant so the loader + any story reference the identical set.
|
|
31
|
+
*/
|
|
32
|
+
export declare const CERT_MODAL_FONT_FAMILIES: readonly ["DM Sans", "Playfair Display", "JetBrains Mono", "Plus Jakarta Sans", "Allura"];
|
|
33
|
+
/** The full scoped stylesheet, adopted via a constructable `CSSStyleSheet`. */
|
|
34
|
+
export declare const CERT_MODAL_CSS = "\n:host {\n\t--cr-navy: #0a1b3f;\n\t--cr-lime: #d4e157;\n\t--cr-white: #ffffff;\n\t--cr-navy-06: rgba(10, 27, 63, 0.06);\n\t--cr-navy-10: rgba(10, 27, 63, 0.1);\n\t--cr-navy-25: rgba(10, 27, 63, 0.25);\n\t--cr-navy-40: rgba(10, 27, 63, 0.4);\n\t--cr-navy-45: rgba(10, 27, 63, 0.45);\n\t--cr-navy-50: rgba(10, 27, 63, 0.5);\n\t--cr-navy-55: rgba(10, 27, 63, 0.55);\n\t--cr-navy-60: rgba(10, 27, 63, 0.6);\n\t--cr-navy-85: rgba(10, 27, 63, 0.85);\n\t--cr-white-60: rgba(255, 255, 255, 0.6);\n\t--cr-white-85: rgba(255, 255, 255, 0.85);\n\t--cr-cta-hover: #0c2149;\n\t--cr-ghost-hover: rgba(10, 27, 63, 0.12);\n\t--cr-shadow: 0 30px 60px -18px rgba(10, 27, 63, 0.5);\n\t--cr-backdrop: rgba(10, 27, 63, 0.45);\n\t--cr-sans: 'DM Sans', system-ui, -apple-system, sans-serif;\n\t--cr-serif: 'Playfair Display', Georgia, serif;\n\t--cr-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;\n\t--cr-wordmark: 'Plus Jakarta Sans', system-ui, sans-serif;\n\t--cr-script: 'Allura', cursive;\n}\n\n.crm-dialog {\n\tborder: none;\n\tborder-radius: 14px;\n\tpadding: 0;\n\twidth: 92%;\n\tmax-height: 92vh;\n\toverflow: auto;\n\tbox-shadow: var(--cr-shadow);\n\tbackground: var(--cr-white);\n\tcolor: var(--cr-navy);\n\tfont-family: var(--cr-sans);\n\t/* fixed, NOT relative (POR-10473): a :modal dialog (showModal) renders in the TOP LAYER; a\n\t * relative dialog anchors to its scrolled document position, so once the page is scrolled the\n\t * panel renders off-screen while the ::backdrop still covers the viewport (measured top -1972 at\n\t * scrollY 2537). fixed is viewport-anchored (the UA inset:0 + margin:auto centers it) and still\n\t * provides the containing block for the absolutely-positioned .crm-close (x stays in-corner). */\n\tposition: fixed;\n\t/* defensive resets against a hostile brand theme reaching the top-layer dialog */\n\tbox-sizing: border-box;\n\ttext-align: left;\n\tline-height: 1.4;\n}\n.crm-dialog *,\n.crm-dialog *::before,\n.crm-dialog *::after {\n\tbox-sizing: border-box;\n}\n.crm-dialog::backdrop {\n\tbackground: var(--cr-backdrop);\n}\n.crm-dialog--cert {\n\tmax-width: 484px;\n}\n.crm-dialog--expert {\n\tmax-width: 436px;\n}\n\n/* Close (\u00D7) */\n.crm-close {\n\tposition: absolute;\n\ttop: 12px;\n\tright: 15px;\n\tz-index: 2;\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tpadding: 2px;\n\tcolor: var(--cr-white-60);\n\tfont-size: 1.35rem;\n\tline-height: 1;\n\tfont-family: var(--cr-sans);\n\ttransition: color 0.15s;\n}\n.crm-close:hover {\n\tcolor: var(--cr-white);\n}\n\n/* Header bar */\n.crm-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 9px;\n\tpadding: 13px 22px;\n\tbackground: var(--cr-navy);\n\tcolor: var(--cr-white);\n}\n.crm-head__eyebrow {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 600;\n\tletter-spacing: 0.16em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-white-85);\n}\n.crm-head__wordmark {\n\tmargin-left: auto;\n\tmargin-right: 22px;\n\tfont-family: var(--cr-wordmark);\n\tfont-weight: 800;\n\tfont-size: 13px;\n\tletter-spacing: 0.03em;\n\tcolor: var(--cr-white);\n}\n\n/* Body */\n.crm-body--cert {\n\tpadding: 24px 28px 26px;\n}\n.crm-body--expert {\n\tpadding: 22px 26px 24px;\n}\n\n/* Avatar (shared, lime ring) */\n.crm-avatar {\n\tposition: relative;\n\tflex-shrink: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tborder-radius: 50%;\n\tbackground: var(--cr-navy);\n\tcolor: var(--cr-white);\n\tfont-weight: 700;\n\tobject-fit: cover;\n\tbox-shadow: 0 0 0 2px var(--cr-white), 0 0 0 3.5px var(--cr-lime);\n}\n.crm-avatar--56 {\n\twidth: 56px;\n\theight: 56px;\n\tfont-size: 17px;\n}\n.crm-avatar--64 {\n\twidth: 64px;\n\theight: 64px;\n\tfont-size: 1.35rem;\n\tletter-spacing: 0.02em;\n}\n\n/* Certificate \u2014 reviewing-expert row */\n.crm-expert-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 14px;\n}\n.crm-expert-row__info {\n\tflex: 1;\n\tmin-width: 0;\n}\n.crm-expert-row__eyebrow {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 600;\n\tletter-spacing: 0.14em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-45);\n}\n.crm-expert-row__name {\n\tfont-size: 15px;\n\tfont-weight: 700;\n\tcolor: var(--cr-navy);\n\tmargin-top: 2px;\n}\n.crm-expert-row__creds {\n\tfont-size: 12px;\n\tcolor: var(--cr-navy-60);\n\tmargin-top: 1px;\n}\n\n/* Certificate \u2014 \"Certifies the article\" */\n.crm-certifies {\n\tmargin: 18px 0 0;\n\tpadding-top: 16px;\n\tborder-top: 1px solid var(--cr-navy-10);\n}\n.crm-certifies__eyebrow {\n\tfont-family: var(--cr-mono);\n\tfont-size: 9px;\n\tfont-weight: 600;\n\tletter-spacing: 0.16em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-40);\n}\n.crm-certifies__title {\n\tfont-family: var(--cr-serif);\n\tfont-weight: 700;\n\tfont-size: 18px;\n\tline-height: 1.25;\n\tcolor: var(--cr-navy);\n\tmargin-top: 5px;\n\ttext-wrap: balance;\n}\n\n/* Certificate \u2014 meta row */\n.crm-meta {\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin-top: 16px;\n}\n.crm-meta__col {\n\tflex: 1;\n}\n.crm-meta__label {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8px;\n\tletter-spacing: 0.12em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-40);\n}\n.crm-meta__value {\n\tfont-size: 12.5px;\n\tfont-weight: 600;\n\tcolor: var(--cr-navy);\n\tmargin-top: 3px;\n}\n.crm-meta__value--mono {\n\tfont-family: var(--cr-mono);\n\tfont-size: 12px;\n}\n.crm-meta__status {\n\tmargin-top: 3px;\n}\n\n/* Certified chip (meta status) */\n.crm-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 2px 7px;\n\tborder-radius: 4px;\n\tbackground: var(--cr-lime);\n\tcolor: var(--cr-navy);\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n}\n\n/* Certificate \u2014 signature + stamp footer */\n.crm-sig {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tjustify-content: space-between;\n\tmargin-top: 20px;\n\tpadding-top: 16px;\n\tborder-top: 1px solid var(--cr-navy-10);\n\tgap: 12px;\n}\n.crm-sig__name {\n\tfont-family: var(--cr-script);\n\tfont-size: 30px;\n\tline-height: 0.9;\n\tcolor: var(--cr-navy);\n}\n.crm-sig__rule {\n\twidth: 146px;\n\tmax-width: 100%;\n\theight: 1px;\n\tbackground: var(--cr-navy-25);\n\tmargin: 4px 0 6px;\n}\n.crm-sig__url {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8px;\n\tfont-weight: 500;\n\tletter-spacing: 0.12em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-50);\n}\n.crm-stamp {\n\ttransform: rotate(-8deg);\n\tflex-shrink: 0;\n}\n.crm-stamp svg {\n\tdisplay: block;\n}\n\n/* Actions (shared) */\n.crm-actions {\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin-top: 20px;\n}\n.crm-cta {\n\tflex: 1;\n\tdisplay: inline-block;\n\ttext-align: center;\n\tpadding: 11px 16px;\n\tbackground: var(--cr-navy);\n\tcolor: var(--cr-white);\n\ttext-decoration: none;\n\tborder-radius: 8px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tfont-family: var(--cr-sans);\n\tborder: none;\n\tcursor: pointer;\n\ttransition: background 0.15s;\n}\n.crm-cta:hover {\n\tbackground: var(--cr-cta-hover);\n}\n.crm-ghost {\n\tpadding: 11px 18px;\n\tbackground: var(--cr-navy-06);\n\tcolor: var(--cr-navy);\n\tborder: none;\n\tborder-radius: 8px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tfont-family: var(--cr-sans);\n\tcursor: pointer;\n\ttransition: background 0.15s;\n}\n.crm-ghost:hover {\n\tbackground: var(--cr-ghost-hover);\n}\n\n/* Reviewer \u2014 identity row */\n.crm-id {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 14px;\n}\n.crm-id__info {\n\tmin-width: 0;\n}\n.crm-id__name {\n\tfont-size: 16px;\n\tfont-weight: 700;\n\tline-height: 1.25;\n\tcolor: var(--cr-navy);\n}\n.crm-id__subtitle {\n\tfont-size: 12.5px;\n\tline-height: 1.4;\n\tcolor: var(--cr-navy-60);\n\tmargin-top: 2px;\n}\n\n/* Reviewer \u2014 credential chips */\n.crm-chips {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 7px;\n\tmargin-top: 16px;\n}\n.crm-chips__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tpadding: 4px 10px;\n\tborder-radius: 999px;\n\tbackground: var(--cr-navy-06);\n\tcolor: var(--cr-navy);\n\tfont-size: 11px;\n\tfont-weight: 600;\n}\n\n/* Reviewer \u2014 bio */\n.crm-bio {\n\tmargin: 16px 0 0;\n\tfont-size: 0.9rem;\n\tline-height: 1.55;\n\tcolor: var(--cr-navy-85);\n}\n\n/* Reviewer \u2014 trust line (the disclosure footer below now owns the spacing to the actions) */\n.crm-trust {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 9px;\n\tflex-wrap: wrap;\n\tmargin: 16px 0 0;\n\tpadding-top: 16px;\n\tborder-top: 1px solid var(--cr-navy-10);\n}\n.crm-trust__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 3px 8px;\n\tborder-radius: 4px;\n\tbackground: var(--cr-lime);\n\tcolor: var(--cr-navy);\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n}\n.crm-trust__text {\n\tfont-family: var(--cr-mono);\n\tfont-size: 9px;\n\tletter-spacing: 0.05em;\n\tcolor: var(--cr-navy-50);\n}\n\n/* FTC disclosure footer (POR-10496) \u2014 material-connection cue (compensated only) + scope line\n * (always). Rendered on both the certificate + reviewer dialogs, immediately above the actions. */\n.crm-disclosure {\n\tmargin-top: 14px;\n\tpadding-top: 14px;\n\tborder-top: 1px solid var(--cr-navy-10);\n\tfont-family: var(--cr-sans);\n\tfont-size: 11.5px;\n\tline-height: 1.5;\n\tcolor: var(--cr-navy-50);\n}\n.crm-disclosure__cue {\n\tfont-family: var(--cr-mono);\n\tfont-size: 10px;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy);\n}\n\n/* Narrow storefronts / phones */\n@media (max-width: 440px) {\n\t.crm-body--cert {\n\t\tpadding: 20px 18px 22px;\n\t}\n\t.crm-body--expert {\n\t\tpadding: 18px 18px 20px;\n\t}\n}\n";
|
|
35
|
+
//# sourceMappingURL=certrev-cert-modal.styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certrev-cert-modal.styles.d.ts","sourceRoot":"","sources":["../../src/modal/certrev-cert-modal.styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;qGACqG;AACrG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,IAAI,SAAI,GAAG,MAAM,CAE1C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,SAAQ,GAAG,MAAM,CAItD;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,2FAM3B,CAAA;AAOV,+EAA+E;AAC/E,eAAO,MAAM,cAAc,ghTAib1B,CAAA"}
|