@certrev/cert-block 0.1.2 → 0.2.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.
Files changed (47) hide show
  1. package/dist/builder/index.d.ts +54 -52
  2. package/dist/builder/index.d.ts.map +1 -1
  3. package/dist/builder/index.js +64 -47
  4. package/dist/builder/index.js.map +1 -1
  5. package/dist/components/CertBadge.d.ts +18 -3
  6. package/dist/components/CertBadge.d.ts.map +1 -1
  7. package/dist/components/CertBadge.js +8 -2
  8. package/dist/components/CertBadge.js.map +1 -1
  9. package/dist/components/CertReview.d.ts +7 -0
  10. package/dist/components/CertReview.d.ts.map +1 -1
  11. package/dist/components/CertReview.js +1 -1
  12. package/dist/components/CertReview.js.map +1 -1
  13. package/dist/components/format.d.ts +14 -2
  14. package/dist/components/format.d.ts.map +1 -1
  15. package/dist/components/format.js +18 -5
  16. package/dist/components/format.js.map +1 -1
  17. package/dist/components/render-cert-block.d.ts +89 -0
  18. package/dist/components/render-cert-block.d.ts.map +1 -0
  19. package/dist/components/render-cert-block.js +282 -0
  20. package/dist/components/render-cert-block.js.map +1 -0
  21. package/dist/components/render-def.d.ts +178 -0
  22. package/dist/components/render-def.d.ts.map +1 -0
  23. package/dist/components/render-def.js +204 -0
  24. package/dist/components/render-def.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/webcomponent/render-badge-html.d.ts +8 -0
  30. package/dist/webcomponent/render-badge-html.d.ts.map +1 -1
  31. package/dist/webcomponent/render-badge-html.js +31 -4
  32. package/dist/webcomponent/render-badge-html.js.map +1 -1
  33. package/package.json +3 -2
  34. package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +13 -0
  35. package/src/__tests__/components.test.tsx +16 -1
  36. package/src/__tests__/render-cert-block.test.ts +259 -0
  37. package/src/__tests__/render-def.test.tsx +286 -0
  38. package/src/__tests__/webcomponent.test.tsx +10 -0
  39. package/src/builder/__tests__/builder.test.tsx +38 -51
  40. package/src/builder/index.tsx +86 -64
  41. package/src/components/CertBadge.tsx +56 -4
  42. package/src/components/CertReview.tsx +8 -0
  43. package/src/components/format.ts +22 -5
  44. package/src/components/render-cert-block.ts +403 -0
  45. package/src/components/render-def.ts +286 -0
  46. package/src/index.ts +35 -0
  47. package/src/webcomponent/render-badge-html.ts +46 -4
@@ -0,0 +1,89 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * renderCertBlock — the LOCKED CertREV cert design, as an SSR-safe HTML string
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * Paints the owner-approved cert render (decision 2026-07-17) in one of three
7
+ * placement modes — `banner`, `sidebar`, `floating` — from RESOLVED theme tokens +
8
+ * the cert facts. It returns a self-contained HTML string that renders with ZERO
9
+ * JavaScript (the CertREV modal + the reviewer-bio accordion are progressive
10
+ * enhancement, wired by the shared embed script via the `data-certrev-*` hooks).
11
+ *
12
+ * WHY A STRING (not a React component): the block is emitted into storefront HTML
13
+ * across every render surface (Shopify Liquid, WordPress PHP, the JS engine, and
14
+ * this headless twin). A pure string renderer — like `renderBadgeHtml` — runs on
15
+ * every runtime the main entry targets (edge / Workers / server) with no React, no
16
+ * DOM, no Node builtins.
17
+ *
18
+ * THEMING: the reference is variable-driven. This emits the guide's CSS-var theming
19
+ * hooks on the ROOT element so the block is self-contained on a storefront that does
20
+ * NOT define the guide's `:root` — the accent (`--ba`), its auto-contrast ink
21
+ * (`--ba-fg`), the corner radius (`--br`), the body font (`--bf`) and surface
22
+ * (`--surface`) come from the resolved theme; the navy trust tints + the fixed
23
+ * label/serif/wordmark fonts are LOCKED literals (guide "the lock is legal, not
24
+ * aesthetic"). A brand themes accent / font / corners / surface; the navy chrome ink
25
+ * stays fixed.
26
+ *
27
+ * COMPLIANCE (LOCKED, non-negotiable): the three material disclosures always render
28
+ * on a CLAIM face (banner + sidebar) — the compensation cue, the byte-exact FTC
29
+ * scope line, and the dated credential verification, which stays GROUPED with the
30
+ * credential (`<name>, <credential>` → "Credential verified by CertREV" → "on
31
+ * <date>"). The `floating` pill makes no claim (it only labels + links to the
32
+ * modal), so it carries no disclosures — matching the locked reference.
33
+ *
34
+ * SECURITY: this is storefront HTML built by hand (no JSX auto-escaping), so EVERY
35
+ * interpolated value is escaped — `escapeHtml` for content, `escapeAttribute` for
36
+ * attribute values, `safeHttpUrl` for hrefs. Theme tokens are re-validated
37
+ * (`resolveBlockTheme`) and only ever emitted as strict hex / length / enum-stack.
38
+ */
39
+ import { type ResolvedBlockTheme } from './render-def.js';
40
+ /** The three LOCKED placement faces. */
41
+ export type CertBlockMode = 'banner' | 'sidebar' | 'floating';
42
+ /**
43
+ * The cert facts + LOCKED display strings a face renders. Facts are escaped; the
44
+ * three locked strings (`compensationCue`, `scopeLine`, `credentialLine`) are
45
+ * byte-verbatim per the CertREV Display Guide and default to the mirrored constants
46
+ * when a caller omits them — so the compliance baseline is exact even if unsupplied.
47
+ */
48
+ export interface CertBlockFacts {
49
+ /** The content author (the "Written by" column). */
50
+ readonly authorName: string;
51
+ /** The author's subtitle (e.g. "Editorial"). Omitted when absent. */
52
+ readonly authorTitle?: string;
53
+ /** The reviewing expert's display name, WITHOUT the credential (e.g. "Jane Doe"). */
54
+ readonly reviewerName: string;
55
+ /** The reviewer's credential abbreviation (e.g. "MD"). Empty → name-only. */
56
+ readonly credential: string;
57
+ /** ISO instant CertREV verified the CREDENTIAL (the dated verification date). */
58
+ readonly credentialVerifiedAt: string;
59
+ /** ISO instant the article was certified (the "Certified <date>" footer). */
60
+ readonly certifiedAt: string;
61
+ /** The expert's memo (rendered as the serif blockquote). */
62
+ readonly memo: string;
63
+ /** The expert's bio (banner accordion / sidebar paragraph). Omitted when absent. */
64
+ readonly bio?: string;
65
+ /** The expert profile URL (the "Profile" link + modal `expert` hook). */
66
+ readonly profileUrl: string;
67
+ /** The certificate URL (the "Certificate" link + modal hook). */
68
+ readonly certificateUrl: string;
69
+ /** LOCKED — the compensation cue. Default: `COMPENSATED_EXPERT_CUE`. */
70
+ readonly compensationCue?: string;
71
+ /** LOCKED — the byte-exact FTC scope line. Default: `CERT_SCOPE_LINE`. */
72
+ readonly scopeLine?: string;
73
+ /** LOCKED — the verification attribution. Default: `CREDENTIAL_VERIFIED_ATTRIBUTION`. */
74
+ readonly credentialLine?: string;
75
+ }
76
+ /** The full input to `renderCertBlock`. */
77
+ export interface RenderCertBlockInput {
78
+ readonly mode: CertBlockMode;
79
+ /** RESOLVED theme tokens (the portal bakes these). Absent/invalid tokens fall back to navy. */
80
+ readonly theme?: Partial<ResolvedBlockTheme>;
81
+ readonly facts: CertBlockFacts;
82
+ }
83
+ /**
84
+ * Render the LOCKED CertREV cert design as an SSR-safe HTML string for one of the
85
+ * three placement modes. Returns a self-contained `<div class="certrev-cert …">`
86
+ * that needs no JS to render (the modal + bio accordion progressively enhance).
87
+ */
88
+ export declare function renderCertBlock(input: RenderCertBlockInput): string;
89
+ //# sourceMappingURL=render-cert-block.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-cert-block.d.ts","sourceRoot":"","sources":["../../src/components/render-cert-block.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,OAAO,EAMN,KAAK,kBAAkB,EAEvB,MAAM,iBAAiB,CAAA;AAExB,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;AAE7D;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,qFAAqF;IACrF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oFAAoF;IACpF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;IAC5B,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAC9B;AAmSD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAWnE"}
@@ -0,0 +1,282 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * renderCertBlock — the LOCKED CertREV cert design, as an SSR-safe HTML string
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * Paints the owner-approved cert render (decision 2026-07-17) in one of three
7
+ * placement modes — `banner`, `sidebar`, `floating` — from RESOLVED theme tokens +
8
+ * the cert facts. It returns a self-contained HTML string that renders with ZERO
9
+ * JavaScript (the CertREV modal + the reviewer-bio accordion are progressive
10
+ * enhancement, wired by the shared embed script via the `data-certrev-*` hooks).
11
+ *
12
+ * WHY A STRING (not a React component): the block is emitted into storefront HTML
13
+ * across every render surface (Shopify Liquid, WordPress PHP, the JS engine, and
14
+ * this headless twin). A pure string renderer — like `renderBadgeHtml` — runs on
15
+ * every runtime the main entry targets (edge / Workers / server) with no React, no
16
+ * DOM, no Node builtins.
17
+ *
18
+ * THEMING: the reference is variable-driven. This emits the guide's CSS-var theming
19
+ * hooks on the ROOT element so the block is self-contained on a storefront that does
20
+ * NOT define the guide's `:root` — the accent (`--ba`), its auto-contrast ink
21
+ * (`--ba-fg`), the corner radius (`--br`), the body font (`--bf`) and surface
22
+ * (`--surface`) come from the resolved theme; the navy trust tints + the fixed
23
+ * label/serif/wordmark fonts are LOCKED literals (guide "the lock is legal, not
24
+ * aesthetic"). A brand themes accent / font / corners / surface; the navy chrome ink
25
+ * stays fixed.
26
+ *
27
+ * COMPLIANCE (LOCKED, non-negotiable): the three material disclosures always render
28
+ * on a CLAIM face (banner + sidebar) — the compensation cue, the byte-exact FTC
29
+ * scope line, and the dated credential verification, which stays GROUPED with the
30
+ * credential (`<name>, <credential>` → "Credential verified by CertREV" → "on
31
+ * <date>"). The `floating` pill makes no claim (it only labels + links to the
32
+ * modal), so it carries no disclosures — matching the locked reference.
33
+ *
34
+ * SECURITY: this is storefront HTML built by hand (no JSX auto-escaping), so EVERY
35
+ * interpolated value is escaped — `escapeHtml` for content, `escapeAttribute` for
36
+ * attribute values, `safeHttpUrl` for hrefs. Theme tokens are re-validated
37
+ * (`resolveBlockTheme`) and only ever emitted as strict hex / length / enum-stack.
38
+ */
39
+ import { escapeAttribute, escapeHtml, safeHttpUrl } from './escape.js';
40
+ import { formatDate } from './format.js';
41
+ import { CERT_SCOPE_LINE, CERT_STRINGS_VERSION, COMPENSATED_EXPERT_CUE, CREDENTIAL_VERIFIED_ATTRIBUTION, RENDER_BLOCK_FONT_STACKS, resolveBlockTheme, } from './render-def.js';
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ // The CertREV logo — the fixed 4-chevron mark (owner decision 2026-07-17: the logo
44
+ // IS permitted on the article face). Path data is byte-exact to the locked reference.
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ const LOGO_PATHS = [
47
+ 'M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z',
48
+ 'M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z',
49
+ 'M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z',
50
+ 'M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z',
51
+ ];
52
+ /**
53
+ * The 4-chevron CertREV mark at `size`px, filled `fill` (a controlled literal). The
54
+ * navy/pill HEADERS pass `currentColor` so the mark tracks the header text's accent
55
+ * foreground ink (`--ba-fg`) — white on the navy default, and the dark auto-contrast
56
+ * ink on a light themed accent (so it never goes white-on-light). The memo credit
57
+ * passes the fixed navy `#0a1b3f` (it sits on the white surface).
58
+ */
59
+ function logo(size, fill) {
60
+ return (`<svg viewBox="0 0 375 375" width="${size}" height="${size}" style="flex-shrink:0" aria-label="CertREV">` +
61
+ LOGO_PATHS.map((d) => `<path fill="${fill}" d="${d}"></path>`).join('') +
62
+ `</svg>`);
63
+ }
64
+ /** The bio-accordion caret (progressive-enhancement affordance). */
65
+ const CARET = `<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;">` +
66
+ `<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span>`;
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ // Small pure helpers
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+ /** "Jane Doe" → "JD"; "The Editorial Team" → "TE" (first letter of the first two words). */
71
+ function initials(name) {
72
+ const parts = name.trim().split(/\s+/).filter(Boolean);
73
+ const chars = parts
74
+ .slice(0, 2)
75
+ .map((p) => p[0] ?? '')
76
+ .join('');
77
+ return (chars || '?').toUpperCase();
78
+ }
79
+ /** The first name token (for "View Jane's profile"). */
80
+ function firstNameOf(name) {
81
+ const first = name.trim().split(/\s+/)[0];
82
+ return first || name.trim();
83
+ }
84
+ /**
85
+ * A LOCKED display string: use the caller's value ONLY when it is a non-blank
86
+ * string, else the byte-verbatim constant. The three compliance strings
87
+ * (compensation cue, scope line, credential attribution) are undroppable — an
88
+ * omitted OR empty/whitespace override must never blank the disclosure.
89
+ */
90
+ function locked(supplied, constant) {
91
+ return supplied?.trim() ? supplied : constant;
92
+ }
93
+ /** A round avatar tile with the given initials. */
94
+ function avatar(text, size, fontSize) {
95
+ return (`<div style="width:${size}px;height:${size}px;border-radius:50%;background:var(--stone);color:var(--navy-55);` +
96
+ `display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:${fontSize}px;flex-shrink:0;">` +
97
+ `${escapeHtml(text)}</div>`);
98
+ }
99
+ /** "Jane Doe, MD" (or just the name when there is no credential) — escaped. */
100
+ function nameWithCredential(name, credential) {
101
+ const n = escapeHtml(name);
102
+ return credential ? `${n}, ${escapeHtml(credential)}` : n;
103
+ }
104
+ /** The reviewer profile link (carries the modal `expert` hook + a real href fallback). */
105
+ function profileLink(url, inner, style) {
106
+ const href = safeHttpUrl(url) ?? '#';
107
+ return `<a href="${escapeAttribute(href)}" data-certrev-modal-open="expert" rel="noopener" style="${style}">${inner}</a>`;
108
+ }
109
+ /** The certificate link (carries the modal hook + a real href fallback). */
110
+ function certificateLink(url, inner, style) {
111
+ const href = safeHttpUrl(url) ?? '#';
112
+ return `<a href="${escapeAttribute(href)}" data-certrev-modal-open rel="noopener" style="${style}">${inner}</a>`;
113
+ }
114
+ /**
115
+ * The GROUPED dated credential verification (LOCKED requirement): the credentialed
116
+ * name, then the attribution line, then "on <date>" — one block, order preserved.
117
+ * `withAccordion` adds the bio caret + hidden bio (banner) as progressive enhancement.
118
+ */
119
+ function verificationBlock(facts, withAccordion) {
120
+ const credentialLine = locked(facts.credentialLine, CREDENTIAL_VERIFIED_ATTRIBUTION);
121
+ const verified = formatDate(facts.credentialVerifiedAt) ?? escapeHtml(facts.credentialVerifiedAt);
122
+ const caret = withAccordion && facts.bio ? CARET : '';
123
+ const nameSize = withAccordion ? '15px' : '16px';
124
+ const muteSize = withAccordion ? '13px' : '12.5px';
125
+ const bio = withAccordion && facts.bio
126
+ ? `<div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--navy-75);">${escapeHtml(facts.bio)}</div>`
127
+ : '';
128
+ return (`<div>` +
129
+ `<div style="font-size:${nameSize};font-weight:700;">${nameWithCredential(facts.reviewerName, facts.credential)}${caret}</div>` +
130
+ `<div style="font-size:${muteSize};color:var(--navy-55);line-height:1.45;margin-top:3px;">${escapeHtml(credentialLine)}</div>` +
131
+ `<div style="font-size:${muteSize};color:var(--navy-55);line-height:1.35;">on ${verified}</div>` +
132
+ bio +
133
+ `</div>`);
134
+ }
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+ // The root wrapper — sets the guide's CSS vars so the block is self-contained
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+ function rootStyle(theme) {
139
+ // Data-bearing theme tokens are already strict-validated (hex / length / literal
140
+ // ink); the font is a controlled enum → stack literal. The navy tints + fixed
141
+ // label/serif/wordmark fonts are LOCKED literals.
142
+ return [
143
+ `--ba:${theme.accentColor}`,
144
+ `--ba-fg:${theme.accentFg}`,
145
+ `--br:${theme.cornerRadius}`,
146
+ `--bf:${RENDER_BLOCK_FONT_STACKS[theme.fontSlot]}`,
147
+ `--surface:${theme.surface}`,
148
+ `--navy:#0a1b3f`,
149
+ `--navy-06:rgba(10,27,63,.06)`,
150
+ `--navy-10:rgba(10,27,63,.10)`,
151
+ `--navy-35:rgba(10,27,63,.35)`,
152
+ `--navy-55:rgba(10,27,63,.55)`,
153
+ `--navy-75:rgba(10,27,63,.75)`,
154
+ `--stone:#ece9e1`,
155
+ `--font-mono:${RENDER_BLOCK_FONT_STACKS.mono}`,
156
+ `--font-serif:${RENDER_BLOCK_FONT_STACKS.serif}`,
157
+ `--font-wordmark:${RENDER_BLOCK_FONT_STACKS.grotesk}`,
158
+ `font-family:var(--bf)`,
159
+ `color:var(--navy)`,
160
+ ].join(';');
161
+ }
162
+ function root(mode, theme, inner) {
163
+ return (`<div class="certrev-cert certrev-cert--${mode}" data-certrev-mode="${mode}" ` +
164
+ `data-certrev-strings-version="${CERT_STRINGS_VERSION}" style="${rootStyle(theme)}">${inner}</div>`);
165
+ }
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+ // Mode 1 — BANNER: a certification header card + a separate expert-memo card
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ function bannerHeaderCard(facts) {
170
+ const compensationCue = locked(facts.compensationCue, COMPENSATED_EXPERT_CUE);
171
+ const certified = formatDate(facts.certifiedAt) ?? escapeHtml(facts.certifiedAt);
172
+ const subtitle = facts.authorTitle
173
+ ? `<div style="font-size:13px;color:var(--navy-55);">${escapeHtml(facts.authorTitle)}</div>`
174
+ : '';
175
+ return (`<div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--bf);">` +
176
+ // navy header
177
+ `<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--ba,#0a1b3f);color:var(--ba-fg,#fff);">` +
178
+ `<span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;">${logo(20, 'currentColor')} Expert reviewed</span>` +
179
+ `</div>` +
180
+ `<div style="padding:22px 24px 18px;">` +
181
+ `<div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;">` +
182
+ // written by
183
+ `<div>` +
184
+ `<div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div>` +
185
+ `<div style="display:flex;align-items:center;gap:12px;">${avatar(initials(facts.authorName), 42, 12)}` +
186
+ `<div><div style="font-size:15px;font-weight:700;">${escapeHtml(facts.authorName)}</div>${subtitle}</div></div>` +
187
+ `</div>` +
188
+ // reviewed by
189
+ `<div>` +
190
+ `<div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Reviewed by</div>` +
191
+ `<div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about ${escapeAttribute(facts.reviewerName)}" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;">` +
192
+ `${avatar(initials(facts.reviewerName), 42, 12)}${verificationBlock(facts, true)}` +
193
+ `</div>` +
194
+ `</div>` +
195
+ `</div>` +
196
+ // hairline + mono footer
197
+ `<div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div>` +
198
+ `<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;font-family:var(--font-mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--navy-55);">` +
199
+ `<span>Certified <span style="color:var(--navy);margin-left:6px;">${certified}</span></span>` +
200
+ `<span>${escapeHtml(compensationCue)}</span>` +
201
+ `</div>` +
202
+ `</div>` +
203
+ `</div>`);
204
+ }
205
+ function bannerMemoCard(facts) {
206
+ const scopeLine = locked(facts.scopeLine, CERT_SCOPE_LINE);
207
+ const credTail = facts.credential
208
+ ? `<span style="color:var(--navy-55);">, ${escapeHtml(facts.credential)}</span>`
209
+ : '';
210
+ const first = firstNameOf(facts.reviewerName);
211
+ return (`<div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--bf);">` +
212
+ `<div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div>` +
213
+ `<div style="display:flex;gap:15px;">${avatar(initials(facts.reviewerName), 46, 13)}` +
214
+ `<div style="flex:1;min-width:0;">` +
215
+ `<div style="font-size:16px;margin-bottom:10px;"><span style="font-weight:700;">${escapeHtml(facts.reviewerName)}</span>${credTail}</div>` +
216
+ `<blockquote style="margin:0;padding-left:18px;border-left:3px solid var(--ba,#0a1b3f);font-family:var(--font-serif);font-style:italic;font-size:16px;line-height:1.55;color:var(--navy-75);">${escapeHtml(facts.memo)}</blockquote>` +
217
+ `<div style="display:flex;gap:22px;margin-top:16px;">` +
218
+ profileLink(facts.profileUrl, `View ${escapeHtml(first)}'s profile`, 'font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;') +
219
+ certificateLink(facts.certificateUrl, 'View certificate', 'font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;') +
220
+ `</div>` +
221
+ `</div>` +
222
+ `</div>` +
223
+ `<div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div>` +
224
+ `<div style="display:inline-flex;align-items:center;gap:9px;font-size:13px;color:var(--navy-75);">${logo(18, '#0a1b3f')} Verified by <span style="font-family:var(--font-wordmark);font-weight:800;">CertREV</span></div>` +
225
+ `<div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">${escapeHtml(scopeLine)}</div>` +
226
+ `</div>`);
227
+ }
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+ // Mode 2 — SIDEBAR: a reviewer card pinned beside the article body
230
+ // ─────────────────────────────────────────────────────────────────────────────
231
+ function sidebarCard(facts) {
232
+ const compensationCue = locked(facts.compensationCue, COMPENSATED_EXPERT_CUE);
233
+ const scopeLine = locked(facts.scopeLine, CERT_SCOPE_LINE);
234
+ const bioPara = facts.bio
235
+ ? `<p style="font-size:14px;line-height:1.55;color:var(--navy-75);margin:16px 0 0;">${escapeHtml(facts.bio)}</p>`
236
+ : '';
237
+ return (`<div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--bf);">` +
238
+ `<div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--ba,#0a1b3f);color:var(--ba-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;">${logo(18, 'currentColor')} Expert reviewed</div>` +
239
+ `<div style="padding:20px;">` +
240
+ `<div style="display:flex;align-items:flex-start;gap:13px;">${avatar(initials(facts.reviewerName), 46, 13)}${verificationBlock(facts, false)}</div>` +
241
+ bioPara +
242
+ `<blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-family:var(--font-serif);font-style:italic;font-size:15px;line-height:1.5;color:var(--navy-75);">${escapeHtml(facts.memo)}</blockquote>` +
243
+ `<div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">${escapeHtml(compensationCue)}</span> · ${escapeHtml(scopeLine)}</div>` +
244
+ `</div>` +
245
+ `<div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);">` +
246
+ profileLink(facts.profileUrl, 'View Profile', 'text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;') +
247
+ certificateLink(facts.certificateUrl, 'View Certificate', 'text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;') +
248
+ `</div>` +
249
+ `</div>`);
250
+ }
251
+ // ─────────────────────────────────────────────────────────────────────────────
252
+ // Mode 3 — FLOATING: a compact pinned pill (a modal pointer, NOT a claim face)
253
+ // ─────────────────────────────────────────────────────────────────────────────
254
+ function floatingPill(facts) {
255
+ return (`<div class="certrev-cert__pill" style="display:inline-flex;align-items:center;gap:14px;padding:12px 22px;border-radius:999px;background:var(--ba,#0a1b3f);color:var(--ba-fg,#fff);box-shadow:0 10px 26px var(--navy-10);">` +
256
+ `<span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span>` +
257
+ profileLink(facts.profileUrl, 'Profile', 'font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;') +
258
+ `<span style="opacity:.5;">·</span>` +
259
+ certificateLink(facts.certificateUrl, 'Certificate', 'font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;') +
260
+ `</div>`);
261
+ }
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+ // Entry point
264
+ // ─────────────────────────────────────────────────────────────────────────────
265
+ /**
266
+ * Render the LOCKED CertREV cert design as an SSR-safe HTML string for one of the
267
+ * three placement modes. Returns a self-contained `<div class="certrev-cert …">`
268
+ * that needs no JS to render (the modal + bio accordion progressively enhance).
269
+ */
270
+ export function renderCertBlock(input) {
271
+ const theme = resolveBlockTheme(input.theme);
272
+ const { facts, mode } = input;
273
+ switch (mode) {
274
+ case 'banner':
275
+ return root(mode, theme, bannerHeaderCard(facts) + bannerMemoCard(facts));
276
+ case 'sidebar':
277
+ return root(mode, theme, sidebarCard(facts));
278
+ case 'floating':
279
+ return root(mode, theme, floatingPill(facts));
280
+ }
281
+ }
282
+ //# sourceMappingURL=render-cert-block.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-cert-block.js","sourceRoot":"","sources":["../../src/components/render-cert-block.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EACN,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,+BAA+B,EAC/B,wBAAwB,EAExB,iBAAiB,GACjB,MAAM,iBAAiB,CAAA;AAgDxB,gFAAgF;AAChF,mFAAmF;AACnF,sFAAsF;AACtF,gFAAgF;AAEhF,MAAM,UAAU,GAAG;IAClB,gOAAgO;IAChO,6NAA6N;IAC7N,yOAAyO;IACzO,6OAA6O;CAC7O,CAAA;AAED;;;;;;GAMG;AACH,SAAS,IAAI,CAAC,IAAY,EAAE,IAAgC;IAC3D,OAAO,CACN,qCAAqC,IAAI,aAAa,IAAI,+CAA+C;QACzG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACvE,QAAQ,CACR,CAAA;AACF,CAAC;AAED,oEAAoE;AACpE,MAAM,KAAK,GACV,+JAA+J;IAC/J,oNAAoN,CAAA;AAErN,gFAAgF;AAChF,qBAAqB;AACrB,gFAAgF;AAEhF,4FAA4F;AAC5F,SAAS,QAAQ,CAAC,IAAY;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACtD,MAAM,KAAK,GAAG,KAAK;SACjB,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACtB,IAAI,CAAC,EAAE,CAAC,CAAA;IACV,OAAO,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;AACpC,CAAC;AAED,wDAAwD;AACxD,SAAS,WAAW,CAAC,IAAY;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,OAAO,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,CAAA;AAC5B,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM,CAAC,QAA4B,EAAE,QAAgB;IAC7D,OAAO,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC9C,CAAC;AAED,mDAAmD;AACnD,SAAS,MAAM,CAAC,IAAY,EAAE,IAAY,EAAE,QAAgB;IAC3D,OAAO,CACN,qBAAqB,IAAI,aAAa,IAAI,oEAAoE;QAC9G,iHAAiH,QAAQ,qBAAqB;QAC9I,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAC3B,CAAA;AACF,CAAC;AAED,+EAA+E;AAC/E,SAAS,kBAAkB,CAAC,IAAY,EAAE,UAAkB;IAC3D,MAAM,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAC1B,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED,0FAA0F;AAC1F,SAAS,WAAW,CAAC,GAAW,EAAE,KAAa,EAAE,KAAa;IAC7D,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,GAAG,CAAA;IACpC,OAAO,YAAY,eAAe,CAAC,IAAI,CAAC,4DAA4D,KAAK,KAAK,KAAK,MAAM,CAAA;AAC1H,CAAC;AAED,4EAA4E;AAC5E,SAAS,eAAe,CAAC,GAAW,EAAE,KAAa,EAAE,KAAa;IACjE,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,GAAG,CAAA;IACpC,OAAO,YAAY,eAAe,CAAC,IAAI,CAAC,mDAAmD,KAAK,KAAK,KAAK,MAAM,CAAA;AACjH,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,KAAqB,EAAE,aAAsB;IACvE,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,+BAA+B,CAAC,CAAA;IACpF,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAA;IACjG,MAAM,KAAK,GAAG,aAAa,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;IACrD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAChD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;IAClD,MAAM,GAAG,GACR,aAAa,IAAI,KAAK,CAAC,GAAG;QACzB,CAAC,CAAC,oIAAoI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ;QACnK,CAAC,CAAC,EAAE,CAAA;IACN,OAAO,CACN,OAAO;QACP,yBAAyB,QAAQ,sBAAsB,kBAAkB,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,KAAK,QAAQ;QAC/H,yBAAyB,QAAQ,2DAA2D,UAAU,CAAC,cAAc,CAAC,QAAQ;QAC9H,yBAAyB,QAAQ,+CAA+C,QAAQ,QAAQ;QAChG,GAAG;QACH,QAAQ,CACR,CAAA;AACF,CAAC;AAED,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAEhF,SAAS,SAAS,CAAC,KAAyB;IAC3C,iFAAiF;IACjF,8EAA8E;IAC9E,kDAAkD;IAClD,OAAO;QACN,QAAQ,KAAK,CAAC,WAAW,EAAE;QAC3B,WAAW,KAAK,CAAC,QAAQ,EAAE;QAC3B,QAAQ,KAAK,CAAC,YAAY,EAAE;QAC5B,QAAQ,wBAAwB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE;QAClD,aAAa,KAAK,CAAC,OAAO,EAAE;QAC5B,gBAAgB;QAChB,8BAA8B;QAC9B,8BAA8B;QAC9B,8BAA8B;QAC9B,8BAA8B;QAC9B,8BAA8B;QAC9B,iBAAiB;QACjB,eAAe,wBAAwB,CAAC,IAAI,EAAE;QAC9C,gBAAgB,wBAAwB,CAAC,KAAK,EAAE;QAChD,mBAAmB,wBAAwB,CAAC,OAAO,EAAE;QACrD,uBAAuB;QACvB,mBAAmB;KACnB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACZ,CAAC;AAED,SAAS,IAAI,CAAC,IAAmB,EAAE,KAAyB,EAAE,KAAa;IAC1E,OAAO,CACN,0CAA0C,IAAI,wBAAwB,IAAI,IAAI;QAC9E,iCAAiC,oBAAoB,YAAY,SAAS,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ,CACnG,CAAA;AACF,CAAC;AAED,gFAAgF;AAChF,6EAA6E;AAC7E,gFAAgF;AAEhF,SAAS,gBAAgB,CAAC,KAAqB;IAC9C,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAA;IAC7E,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAChF,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW;QACjC,CAAC,CAAC,qDAAqD,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ;QAC5F,CAAC,CAAC,EAAE,CAAA;IACL,OAAO,CACN,+HAA+H;QAC/H,cAAc;QACd,8JAA8J;QAC9J,4KAA4K,IAAI,CAAC,EAAE,EAAE,cAAc,CAAC,yBAAyB;QAC7N,QAAQ;QACR,uCAAuC;QACvC,oEAAoE;QACpE,aAAa;QACb,OAAO;QACP,kKAAkK;QAClK,0DAA0D,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;QACtG,qDAAqD,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,QAAQ,cAAc;QAChH,QAAQ;QACR,cAAc;QACd,OAAO;QACP,mKAAmK;QACnK,uIAAuI,eAAe,CAAC,KAAK,CAAC,YAAY,CAAC,mHAAmH;QAC7R,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE;QAClF,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,yBAAyB;QACzB,8EAA8E;QAC9E,wMAAwM;QACxM,oEAAoE,SAAS,gBAAgB;QAC7F,SAAS,UAAU,CAAC,eAAe,CAAC,SAAS;QAC7C,QAAQ;QACR,QAAQ;QACR,QAAQ,CACR,CAAA;AACF,CAAC;AAED,SAAS,cAAc,CAAC,KAAqB;IAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,eAAe,CAAC,CAAA;IAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU;QAChC,CAAC,CAAC,yCAAyC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS;QAChF,CAAC,CAAC,EAAE,CAAA;IACL,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;IAC7C,OAAO,CACN,4HAA4H;QAC5H,mKAAmK;QACnK,uCAAuC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;QACrF,mCAAmC;QACnC,kFAAkF,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,QAAQ,QAAQ;QAC1I,gMAAgM,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe;QACrO,sDAAsD;QACtD,WAAW,CACV,KAAK,CAAC,UAAU,EAChB,QAAQ,UAAU,CAAC,KAAK,CAAC,YAAY,EACrC,uFAAuF,CACvF;QACD,eAAe,CACd,KAAK,CAAC,cAAc,EACpB,kBAAkB,EAClB,uFAAuF,CACvF;QACD,QAAQ;QACR,QAAQ;QACR,QAAQ;QACR,8EAA8E;QAC9E,oGAAoG,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,mGAAmG;QAC1N,oFAAoF,UAAU,CAAC,SAAS,CAAC,QAAQ;QACjH,QAAQ,CACR,CAAA;AACF,CAAC;AAED,gFAAgF;AAChF,mEAAmE;AACnE,gFAAgF;AAEhF,SAAS,WAAW,CAAC,KAAqB;IACzC,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAA;IAC7E,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,eAAe,CAAC,CAAA;IAC1D,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG;QACxB,CAAC,CAAC,oFAAoF,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM;QACjH,CAAC,CAAC,EAAE,CAAA;IACL,OAAO,CACN,kLAAkL;QAClL,6PAA6P,IAAI,CAAC,EAAE,EAAE,cAAc,CAAC,wBAAwB;QAC7S,6BAA6B;QAC7B,8DAA8D,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ;QACpJ,OAAO;QACP,sMAAsM,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe;QAC3O,+LAA+L,UAAU,CAAC,eAAe,CAAC,aAAa,UAAU,CAAC,SAAS,CAAC,QAAQ;QACpQ,QAAQ;QACR,+FAA+F;QAC/F,WAAW,CACV,KAAK,CAAC,UAAU,EAChB,cAAc,EACd,4JAA4J,CAC5J;QACD,eAAe,CACd,KAAK,CAAC,cAAc,EACpB,kBAAkB,EAClB,sHAAsH,CACtH;QACD,QAAQ;QACR,QAAQ,CACR,CAAA;AACF,CAAC;AAED,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAEhF,SAAS,YAAY,CAAC,KAAqB;IAC1C,OAAO,CACN,4NAA4N;QAC5N,8JAA8J;QAC9J,WAAW,CACV,KAAK,CAAC,UAAU,EAChB,SAAS,EACT,kHAAkH,CAClH;QACD,oCAAoC;QACpC,eAAe,CACd,KAAK,CAAC,cAAc,EACpB,aAAa,EACb,kHAAkH,CAClH;QACD,QAAQ,CACR,CAAA;AACF,CAAC;AAED,gFAAgF;AAChF,cAAc;AACd,gFAAgF;AAEhF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAA2B;IAC1D,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC5C,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;IAC7B,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,QAAQ;YACZ,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;QAC1E,KAAK,SAAS;YACb,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;QAC7C,KAAK,UAAU;YACd,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAA;IAC/C,CAAC;AACF,CAAC"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ─────────────────────────────────────────────────────────────────────────────
3
+ * Design-guide RENDER-DEF support (the themeable-token + subtractive-visibility
4
+ * layer) + the two always-on COMPLIANCE strings.
5
+ * ─────────────────────────────────────────────────────────────────────────────
6
+ *
7
+ * WS6 of the unified cert-render / design-guide build
8
+ * (~/.claude-account-m/plans/smooth-meandering-graham.md). This is the Builder /
9
+ * headless projection twin's half of the "law-as-code" contract: cert-block is one
10
+ * of the four render surfaces (Shopify Liquid, WordPress PHP, JS engine, and this)
11
+ * that must paint the SAME compliant, optionally-themed badge from the SAME facts.
12
+ *
13
+ * cert-block is a self-contained npm package — it does NOT import the portal's
14
+ * `@/lib/design-guide` enforcement core. Instead it mirrors, at the RENDER edge,
15
+ * the small subset that shape the projection carries:
16
+ *
17
+ * 1. The four allowlisted THEMEABLE tokens `{accentColor, surface, cornerRadius,
18
+ * fontSlot}`, emitted as the exact `--certrev-*` CSS custom properties the
19
+ * Shopify/WordPress engines emit and the WS11 crawl monitor asserts
20
+ * (`--certrev-accent` / `--certrev-surface` / `--certrev-radius` /
21
+ * `--certrev-font`). Each is RE-VALIDATED here at render time (defense in
22
+ * depth over the write-path fail-closed gate) against the SAME strict
23
+ * validators the portal's `tokens.ts` uses, and an invalid value is DROPPED
24
+ * (falls back to the preset default) — never emitted. `fontSlot` resolves to
25
+ * a FIXED font STACK (never a free-form family — the CSS-injection surface).
26
+ * 2. SUBTRACTIVE field-visibility: a brand may only HIDE preset-placed,
27
+ * brand-toggleable fields (`reviewerPhoto`, `memo`) — never add a field the
28
+ * preset doesn't place. Locked fields (cue, scope, links) can never be hidden.
29
+ * 3. The two always-on COMPLIANCE strings a claim-bearing face carries on EVERY
30
+ * rung (design guide §3): the compensation cue and the FTC scope line, both
31
+ * byte-verbatim (the scope line's curly apostrophe U+2019 is load-bearing).
32
+ *
33
+ * FAIL-SAFE: when NO render-def is supplied the THEMING layer contributes nothing
34
+ * — no extra CSS vars, no `data-certrev-def-version`, no visibility subtraction —
35
+ * so the badge's theme output is byte-identical to the un-themed default (the
36
+ * program's no-op / merge-safety device). The compensation cue + scope line + the
37
+ * `data-certrev-strings-version` stamp are the compliance BASELINE and land on
38
+ * every claim face regardless of a def (matching the Shopify/WP engine fix).
39
+ *
40
+ * PURITY: no DOM, no Node builtins, no React — safe on every runtime the main
41
+ * entry targets (edge / Workers / server / client).
42
+ */
43
+ /** The font ENUM. Resolves to a FIXED slot stack — never a free-form family. */
44
+ export declare const FONT_SLOTS: readonly ["sans", "serif", "system"];
45
+ export type FontSlot = (typeof FONT_SLOTS)[number];
46
+ /**
47
+ * The `fontSlot` → fixed font-stack literal. BYTE-IDENTICAL to the Shopify Liquid
48
+ * engine's stacks (`shopify/extensions/certrev-schema/snippets/certrev-badge.liquid`
49
+ * — the only font families the twins use), so a themed brand's Builder projection
50
+ * paints the same typography as its Shopify face. A raw family string in a
51
+ * `font-family` CSS var is a CSS-injection surface AND breaks the "CertREV hosts no
52
+ * fonts" invariant, so the enum → stack indirection is the security boundary.
53
+ */
54
+ export declare const FONT_STACKS: Readonly<Record<FontSlot, string>>;
55
+ /** A fully-resolved themeable token set (a brand overrides a subset of these). */
56
+ export interface CertThemeTokens {
57
+ readonly accentColor: string;
58
+ readonly surface: string;
59
+ readonly cornerRadius: string;
60
+ readonly fontSlot: FontSlot;
61
+ }
62
+ /** The badge fields a render-def may SUBTRACTIVELY hide (brand-toggleable, free). */
63
+ export declare const HIDEABLE_FIELDS: readonly ["reviewerPhoto", "memo"];
64
+ export type BadgeVisibilityFieldId = (typeof HIDEABLE_FIELDS)[number];
65
+ /**
66
+ * A brand render-def as it reaches the render edge — the RESOLVED projection the
67
+ * portal's `getActiveRenderDef` → cert-chrome/builder-export path carries (mirrors
68
+ * the Shopify `certrev.render_def` metafield `{v, preset, rung, tokens, hidden}`;
69
+ * cert-block needs only the theming inputs). Every field is OPTIONAL: an absent
70
+ * `renderDef`, an absent token, or an invalid token all fall back to the preset
71
+ * default (fail-safe).
72
+ */
73
+ export interface CertRenderDef {
74
+ /** Partial themeable-token overrides. Absent/invalid keys use the preset default. */
75
+ readonly tokens?: Partial<CertThemeTokens>;
76
+ /** SUBTRACTIVE hide-set — the ONLY visibility a brand can express. */
77
+ readonly hidden?: readonly BadgeVisibilityFieldId[];
78
+ /** Monotonic per-brand def version, stamped as `data-certrev-def-version`. */
79
+ readonly version?: number;
80
+ }
81
+ /** `accentColor` / `surface` — an opaque hex (surface opacity is FORCED for contrast soundness). */
82
+ export declare function isOpaqueHexColor(value: string): boolean;
83
+ /** `cornerRadius` — a single unit-bearing CSS length (or bare `0`). */
84
+ export declare function isCornerRadius(value: string): boolean;
85
+ /** Is `value` a known font slot? */
86
+ export declare function isFontSlot(value: string): value is FontSlot;
87
+ /**
88
+ * The compensation cue (design guide "line A"). Rendered beside the reviewer on
89
+ * EVERY claim-bearing face. Byte-exact; the tag-stripped visible text of the cue
90
+ * markup must equal this constant. Mirrors portal `COMPENSATED_EXPERT_CUE`.
91
+ */
92
+ export declare const COMPENSATED_EXPERT_CUE = "Compensated expert";
93
+ /**
94
+ * The FTC scope disclaimer (design guide "line B"). Byte-verbatim; the curly
95
+ * apostrophe (U+2019) in "article’s" is LOAD-BEARING (the MSA §3 references this
96
+ * copy in the present tense). Mirrors portal `FTC_DISCLOSURE_LINE` / `CERT_SCOPE_LINE`.
97
+ */
98
+ export declare const CERT_SCOPE_LINE = "Independent editorial review of this article\u2019s text and sources. Not a product endorsement, nor medical advice.";
99
+ /**
100
+ * The delivered display-string EDITION, stamped as `data-certrev-strings-version`
101
+ * so the crawl monitor can assert which verbatim strings a live face should carry.
102
+ * MUST equal the portal's `CERT_DISPLAY_STRINGS_VERSION` (display-strings.ts).
103
+ * Bump in lockstep whenever the cue or scope line changes.
104
+ */
105
+ export declare const CERT_STRINGS_VERSION = "2026-07";
106
+ /**
107
+ * The validated theme a render-def resolves to. Every field is present ONLY when
108
+ * the def supplied a VALID value for it (an absent/invalid token is dropped so the
109
+ * preset default shows through). `accent` is surfaced for the accent-precedence
110
+ * chain in `resolveDisplay`; `surface`/`cornerRadius`/`fontStack` have no legacy
111
+ * display equivalent and are emitted ONLY on a themed face.
112
+ */
113
+ export interface ResolvedRenderTheme {
114
+ readonly accent?: string;
115
+ readonly surface?: string;
116
+ readonly cornerRadius?: string;
117
+ readonly fontStack?: string;
118
+ readonly version?: number;
119
+ }
120
+ /** Resolve + re-validate a render-def's tokens. Invalid values are DROPPED (fail-safe). */
121
+ export declare function resolveRenderTheme(def?: CertRenderDef): ResolvedRenderTheme;
122
+ /** Is a brand-toggleable field hidden by this render-def's subtractive hide-set? */
123
+ export declare function isFieldHidden(def: CertRenderDef | undefined, id: BadgeVisibilityFieldId): boolean;
124
+ /**
125
+ * The full portal font-slot enum (mirrors portal `src/lib/design-guide/tokens.ts`
126
+ * `FONT_SLOTS`). `grotesk` ⇐ the guide's wordmark slot; `mono` ⇐ the label slot.
127
+ */
128
+ export declare const RENDER_BLOCK_FONT_SLOTS: readonly ["sans", "serif", "system", "grotesk", "mono"];
129
+ export type RenderBlockFontSlot = (typeof RENDER_BLOCK_FONT_SLOTS)[number];
130
+ /**
131
+ * The `RenderBlockFontSlot` → fixed family stack. BYTE-IDENTICAL to the portal's
132
+ * `FONT_SLOT_STACKS` (the guide's stacks — the cert-render parity anchor across the
133
+ * twins). Resolved family strings, never `var(...)` indirections. A raw family in a
134
+ * `font-family` CSS var is a CSS-injection surface, so the enum → stack indirection
135
+ * stays the security boundary (a caller can only pass an enum member).
136
+ */
137
+ export declare const RENDER_BLOCK_FONT_STACKS: Readonly<Record<RenderBlockFontSlot, string>>;
138
+ /** Is `value` a known render-block font slot? */
139
+ export declare function isRenderBlockFontSlot(value: string): value is RenderBlockFontSlot;
140
+ /**
141
+ * The resolved themeable-token set `renderCertBlock` paints — the render-edge mirror
142
+ * of the portal `ResolvedRenderDef` theme half (`tokens` ⊕ the precomputed
143
+ * `accentFg`). `accentFg` is the ink painted over the accent fill — the portal's
144
+ * guide-faithful Rec.601 auto-contrast pick (`#141414` or `#ffffff`), carried
145
+ * pre-computed so this dumb twin never recomputes it.
146
+ */
147
+ export interface ResolvedBlockTheme {
148
+ readonly accentColor: string;
149
+ readonly surface: string;
150
+ readonly cornerRadius: string;
151
+ readonly fontSlot: RenderBlockFontSlot;
152
+ readonly accentFg: '#141414' | '#ffffff';
153
+ }
154
+ /** The CertREV navy default theme (the un-themed face — matches the locked reference `:root`). */
155
+ export declare const DEFAULT_BLOCK_THEME: ResolvedBlockTheme;
156
+ /**
157
+ * Re-validate a (possibly partial / untrusted) block theme at the render edge —
158
+ * defense in depth over the portal's fail-closed bake. Any invalid token is DROPPED
159
+ * to the navy default (never emitted into storefront CSS). Mirrors the
160
+ * `resolveRenderTheme` fail-safe posture.
161
+ */
162
+ export declare function resolveBlockTheme(theme?: Partial<ResolvedBlockTheme>): ResolvedBlockTheme;
163
+ /**
164
+ * The locked verification ATTRIBUTION phrase, rendered grouped with the dated
165
+ * verification (`<name>, <credential>` → this → `on <date>`). Byte-exact; mirrors the
166
+ * fixed clause inside portal `buildBylineCredentialed` ("Credential verified by
167
+ * CertREV on …"). Never re-word without contract + legal re-review.
168
+ */
169
+ export declare const CREDENTIAL_VERIFIED_ATTRIBUTION = "Credential verified by CertREV";
170
+ /**
171
+ * The extra `--certrev-*` custom-property declarations a THEMED face emits, in the
172
+ * engine's order (surface → radius → font; accent is emitted separately so the
173
+ * no-def output stays byte-identical). Returns `[]` when no theme is present, so
174
+ * the un-themed badge emits ZERO extra vars. The data-bearing values are already
175
+ * strict-validated; the font is a controlled literal stack.
176
+ */
177
+ export declare function themeCssVarDeclarations(theme: ResolvedRenderTheme): ReadonlyArray<readonly [string, string]>;
178
+ //# sourceMappingURL=render-def.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-def.d.ts","sourceRoot":"","sources":["../../src/components/render-def.ts"],"names":[],"mappings":"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"}