@certrev/cert-block 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -7
- package/dist/components/render-cert-block.d.ts.map +1 -1
- package/dist/components/render-cert-block.js +304 -80
- 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 +14 -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 +380 -5
- 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 +387 -102
- 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
|
@@ -19,12 +19,23 @@
|
|
|
19
19
|
* hooks on the ROOT element so the block is self-contained on a storefront that does
|
|
20
20
|
* NOT define the guide's `:root` — the accent (`--ba`), its auto-contrast ink
|
|
21
21
|
* (`--ba-fg`), the corner radius (`--br`), the body font (`--bf`) and surface
|
|
22
|
-
* (`--surface`) come from the resolved theme; the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* (`--surface`) come from the resolved theme; the fixed mono label font (`--font-mono`)
|
|
23
|
+
* is a LOCKED literal (guide "the lock is legal, not aesthetic"). Names, the memo quote,
|
|
24
|
+
* disclosures, links, and the "CertREV" wordmark ride the body font; the serif/grotesk
|
|
25
|
+
* slots remain reachable only as a brand's themeable body-font choice.
|
|
26
|
+
*
|
|
27
|
+
* V2 BRAND-INK (Phase 1, parity with the portal Liquid/CSS engines): a brand may lift TWO
|
|
28
|
+
* more chrome surfaces off their hardcoded navy literals — the header/seal bar (`barInk`,
|
|
29
|
+
* with its auto-contrast `barInkFg`) and the FREE-content trust ink (`inkColor`: reviewer/
|
|
30
|
+
* author names, title, credential, bio, initials + the derived muted scale). These reach
|
|
31
|
+
* the root as the exact `--certrev-bar-bg` / `--certrev-bar-fg` / `--certrev-ink` wire vars
|
|
32
|
+
* the Shopify/WordPress engines emit — but ONLY when the def sets them, so a no-def face is
|
|
33
|
+
* byte-identical. The bar consumes `var(--certrev-bar-bg, #0a1b3f)` / `var(--certrev-bar-fg,
|
|
34
|
+
* #fff)`; the free content consumes the `--cr-ink` family (`--cr-ink` = `var(--certrev-ink,
|
|
35
|
+
* #0a1b3f)`, with `-sub`/`-soft` muted derivations). The LOCKED trust text — the compensation
|
|
36
|
+
* cue, the FTC scope line, "Credential verified by CertREV", the WRITTEN/REVIEWED BY labels,
|
|
37
|
+
* and the certified date — NEVER follows `inkColor`; it stays on the navy `--navy*` scale so a
|
|
38
|
+
* brand ink can never drag legally load-bearing text below its floor.
|
|
28
39
|
*
|
|
29
40
|
* COMPLIANCE (LOCKED, non-negotiable): the three material disclosures always render
|
|
30
41
|
* on a CLAIM face (banner + sidebar) — the compensation cue, the byte-exact FTC
|
|
@@ -37,10 +48,27 @@
|
|
|
37
48
|
* interpolated value is escaped — `escapeHtml` for content, `escapeAttribute` for
|
|
38
49
|
* attribute values, `safeHttpUrl` for hrefs. Theme tokens are re-validated
|
|
39
50
|
* (`resolveBlockTheme`) and only ever emitted as strict hex / length / enum-stack.
|
|
51
|
+
*
|
|
52
|
+
* PLACED-FIELDS STRICTNESS (WS-B): Visibility is decided by the engine's
|
|
53
|
+
* `placedFields` alone; `rung` is a stamp, never a branch; an absent face
|
|
54
|
+
* grandfathers the 0.3.0 full-facts render byte-for-byte.
|
|
40
55
|
*/
|
|
41
56
|
import { type ResolvedBlockTheme } from './render-def.js';
|
|
42
57
|
/** The three LOCKED placement faces. */
|
|
43
58
|
export type CertBlockMode = 'banner' | 'sidebar' | 'floating';
|
|
59
|
+
/** All layouts renderCertBlock can paint: the three locked modes + the engine's free-composition face. */
|
|
60
|
+
export type CertBlockLayout = CertBlockMode | 'custom';
|
|
61
|
+
/**
|
|
62
|
+
* The portal engine's resolved face (assertCompliantRender output). `rung` is
|
|
63
|
+
* INFORMATIONAL ONLY — stamped as data, NEVER branched on for visibility.
|
|
64
|
+
* `placedFields` is the single source of what renders. Plain strings (not a closed
|
|
65
|
+
* union) so a future engine field id degrades to "not painted here" instead of a
|
|
66
|
+
* type break.
|
|
67
|
+
*/
|
|
68
|
+
export interface CertBlockFace {
|
|
69
|
+
readonly rung: string;
|
|
70
|
+
readonly placedFields: readonly string[];
|
|
71
|
+
}
|
|
44
72
|
/**
|
|
45
73
|
* The cert facts + LOCKED display strings a face renders. Facts are escaped; the
|
|
46
74
|
* three locked strings (`compensationCue`, `scopeLine`, `credentialLine`) are
|
|
@@ -77,10 +105,25 @@ export interface CertBlockFacts {
|
|
|
77
105
|
}
|
|
78
106
|
/** The full input to `renderCertBlock`. */
|
|
79
107
|
export interface RenderCertBlockInput {
|
|
80
|
-
readonly mode:
|
|
108
|
+
readonly mode: CertBlockLayout;
|
|
81
109
|
/** RESOLVED theme tokens (the portal bakes these). Absent/invalid tokens fall back to navy. */
|
|
82
110
|
readonly theme?: Partial<ResolvedBlockTheme>;
|
|
83
111
|
readonly facts: CertBlockFacts;
|
|
112
|
+
/**
|
|
113
|
+
* OPTIONAL engine face. ABSENT ⇒ grandfathered full-facts render, byte-identical
|
|
114
|
+
* to 0.3.0 (published callers unaffected). PRESENT ⇒ strict placedFields render.
|
|
115
|
+
*/
|
|
116
|
+
readonly face?: CertBlockFace;
|
|
117
|
+
/**
|
|
118
|
+
* BANNER memo placement (POR-10741 — first-class split, retires the CertReviewCard
|
|
119
|
+
* string-hack). Default `'full'` = header card + memo card in ONE root, BYTE-IDENTICAL
|
|
120
|
+
* to prior behavior. `'header'` / `'memo'` render ONLY that card in its own themed root,
|
|
121
|
+
* so a headless exporter can place the memo as its OWN block under the article body
|
|
122
|
+
* (the "banner → body → memo" layout) WITHOUT string-splitting the rendered HTML — and
|
|
123
|
+
* without duplicating the options payload (each block carries only what its card needs).
|
|
124
|
+
* Ignored outside banner mode.
|
|
125
|
+
*/
|
|
126
|
+
readonly part?: 'full' | 'header' | 'memo';
|
|
84
127
|
}
|
|
85
128
|
/**
|
|
86
129
|
* Render the LOCKED CertREV cert design as an SSR-safe HTML string for one of the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-cert-block.d.ts","sourceRoot":"","sources":["../../src/components/render-cert-block.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"render-cert-block.d.ts","sourceRoot":"","sources":["../../src/components/render-cert-block.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAIH,OAAO,EAMN,KAAK,kBAAkB,EAEvB,MAAM,iBAAiB,CAAA;AAExB,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;AAE7D,0GAA0G;AAC1G,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,QAAQ,CAAA;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CACxC;AAED;;;;;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,eAAe,CAAA;IAC9B,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;CAC1C;AAkhBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CA+BnE"}
|
|
@@ -19,12 +19,23 @@
|
|
|
19
19
|
* hooks on the ROOT element so the block is self-contained on a storefront that does
|
|
20
20
|
* NOT define the guide's `:root` — the accent (`--ba`), its auto-contrast ink
|
|
21
21
|
* (`--ba-fg`), the corner radius (`--br`), the body font (`--bf`) and surface
|
|
22
|
-
* (`--surface`) come from the resolved theme; the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* (`--surface`) come from the resolved theme; the fixed mono label font (`--font-mono`)
|
|
23
|
+
* is a LOCKED literal (guide "the lock is legal, not aesthetic"). Names, the memo quote,
|
|
24
|
+
* disclosures, links, and the "CertREV" wordmark ride the body font; the serif/grotesk
|
|
25
|
+
* slots remain reachable only as a brand's themeable body-font choice.
|
|
26
|
+
*
|
|
27
|
+
* V2 BRAND-INK (Phase 1, parity with the portal Liquid/CSS engines): a brand may lift TWO
|
|
28
|
+
* more chrome surfaces off their hardcoded navy literals — the header/seal bar (`barInk`,
|
|
29
|
+
* with its auto-contrast `barInkFg`) and the FREE-content trust ink (`inkColor`: reviewer/
|
|
30
|
+
* author names, title, credential, bio, initials + the derived muted scale). These reach
|
|
31
|
+
* the root as the exact `--certrev-bar-bg` / `--certrev-bar-fg` / `--certrev-ink` wire vars
|
|
32
|
+
* the Shopify/WordPress engines emit — but ONLY when the def sets them, so a no-def face is
|
|
33
|
+
* byte-identical. The bar consumes `var(--certrev-bar-bg, #0a1b3f)` / `var(--certrev-bar-fg,
|
|
34
|
+
* #fff)`; the free content consumes the `--cr-ink` family (`--cr-ink` = `var(--certrev-ink,
|
|
35
|
+
* #0a1b3f)`, with `-sub`/`-soft` muted derivations). The LOCKED trust text — the compensation
|
|
36
|
+
* cue, the FTC scope line, "Credential verified by CertREV", the WRITTEN/REVIEWED BY labels,
|
|
37
|
+
* and the certified date — NEVER follows `inkColor`; it stays on the navy `--navy*` scale so a
|
|
38
|
+
* brand ink can never drag legally load-bearing text below its floor.
|
|
28
39
|
*
|
|
29
40
|
* COMPLIANCE (LOCKED, non-negotiable): the three material disclosures always render
|
|
30
41
|
* on a CLAIM face (banner + sidebar) — the compensation cue, the byte-exact FTC
|
|
@@ -37,6 +48,10 @@
|
|
|
37
48
|
* interpolated value is escaped — `escapeHtml` for content, `escapeAttribute` for
|
|
38
49
|
* attribute values, `safeHttpUrl` for hrefs. Theme tokens are re-validated
|
|
39
50
|
* (`resolveBlockTheme`) and only ever emitted as strict hex / length / enum-stack.
|
|
51
|
+
*
|
|
52
|
+
* PLACED-FIELDS STRICTNESS (WS-B): Visibility is decided by the engine's
|
|
53
|
+
* `placedFields` alone; `rung` is a stamp, never a branch; an absent face
|
|
54
|
+
* grandfathers the 0.3.0 full-facts render byte-for-byte.
|
|
40
55
|
*/
|
|
41
56
|
import { escapeAttribute, escapeHtml, safeHttpUrl } from './escape.js';
|
|
42
57
|
import { formatDate } from './format.js';
|
|
@@ -101,9 +116,24 @@ function firstNameOf(name) {
|
|
|
101
116
|
function locked(supplied, constant) {
|
|
102
117
|
return supplied?.trim() ? supplied : constant;
|
|
103
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Absent face ⇒ constant-true (the grandfather path: every conditional collapses to
|
|
121
|
+
* today's markup, guaranteeing byte-identity with 0.3.0). Present ⇒ Set membership.
|
|
122
|
+
*/
|
|
123
|
+
function placedTester(face) {
|
|
124
|
+
if (!face)
|
|
125
|
+
return () => true;
|
|
126
|
+
const set = new Set(face.placedFields);
|
|
127
|
+
return (id) => set.has(id);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The memo/custom action-link stamp style — the JetBrains-mono uppercase lockup that
|
|
131
|
+
* matches the badge footer (Owen 2026-07-17); navy so it still reads as an action.
|
|
132
|
+
*/
|
|
133
|
+
const STAMP_LINK_STYLE = 'font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;';
|
|
104
134
|
/** A round avatar tile with the given initials. */
|
|
105
135
|
function avatar(text, size, fontSize) {
|
|
106
|
-
return (`<div style="width:${size}px;height:${size}px;border-radius:50%;background:var(--stone);color:var(--
|
|
136
|
+
return (`<div style="width:${size}px;height:${size}px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);` +
|
|
107
137
|
`display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:${fontSize}px;flex-shrink:0;">` +
|
|
108
138
|
`${escapeHtml(text)}</div>`);
|
|
109
139
|
}
|
|
@@ -127,19 +157,33 @@ function certificateLink(url, inner, style) {
|
|
|
127
157
|
* name, then the attribution line, then "on <date>" — one block, order preserved.
|
|
128
158
|
* `withAccordion` adds the bio caret + hidden bio (banner) as progressive enhancement.
|
|
129
159
|
*/
|
|
130
|
-
function verificationBlock(facts, withAccordion) {
|
|
160
|
+
function verificationBlock(facts, withAccordion, placed) {
|
|
161
|
+
const showCredentialed = placed('bylineCredentialed');
|
|
162
|
+
const showName = placed('bylinePlain') || showCredentialed;
|
|
163
|
+
// Nothing placed ⇒ empty (the caller drops the wrapping toggle div).
|
|
164
|
+
if (!showName)
|
|
165
|
+
return '';
|
|
131
166
|
const credentialLine = locked(facts.credentialLine, CREDENTIAL_VERIFIED_ATTRIBUTION);
|
|
132
167
|
const verified = formatDate(facts.credentialVerifiedAt) ?? escapeHtml(facts.credentialVerifiedAt);
|
|
133
|
-
const
|
|
168
|
+
const showBio = withAccordion && placed('bio') && !!facts.bio;
|
|
169
|
+
const caret = showBio ? CARET : '';
|
|
134
170
|
const nameSize = withAccordion ? '15px' : '16px';
|
|
135
171
|
const muteSize = withAccordion ? '13px' : '12.5px';
|
|
136
|
-
const bio =
|
|
137
|
-
? `<div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--
|
|
172
|
+
const bio = showBio
|
|
173
|
+
? `<div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">${escapeHtml(facts.bio ?? '')}</div>`
|
|
174
|
+
: '';
|
|
175
|
+
// The credential suffix + the GROUPED attribution/date lines are all-or-nothing on
|
|
176
|
+
// `bylineCredentialed`; a plain byline shows the name only.
|
|
177
|
+
const nameInner = showCredentialed
|
|
178
|
+
? nameWithCredential(facts.reviewerName, facts.credential)
|
|
179
|
+
: escapeHtml(facts.reviewerName);
|
|
180
|
+
const attribution = showCredentialed
|
|
181
|
+
? `<div style="font-size:${muteSize};color:var(--navy-55);line-height:1.45;margin-top:3px;">${escapeHtml(credentialLine)}</div>` +
|
|
182
|
+
`<div style="font-size:${muteSize};color:var(--navy-55);line-height:1.35;">on ${verified}</div>`
|
|
138
183
|
: '';
|
|
139
184
|
return (`<div>` +
|
|
140
|
-
`<div style="font-size:${nameSize};font-weight:700;">${
|
|
141
|
-
|
|
142
|
-
`<div style="font-size:${muteSize};color:var(--navy-55);line-height:1.35;">on ${verified}</div>` +
|
|
185
|
+
`<div style="font-size:${nameSize};font-weight:700;color:var(--cr-ink);">${nameInner}${caret}</div>` +
|
|
186
|
+
attribution +
|
|
143
187
|
bio +
|
|
144
188
|
`</div>`);
|
|
145
189
|
}
|
|
@@ -150,12 +194,24 @@ function rootStyle(theme) {
|
|
|
150
194
|
// Data-bearing theme tokens are already strict-validated (hex / length / literal
|
|
151
195
|
// ink); the font is a controlled enum → stack literal. The navy tints + fixed
|
|
152
196
|
// label/serif/wordmark fonts are LOCKED literals.
|
|
197
|
+
const ink = theme.inkColor;
|
|
153
198
|
return [
|
|
154
199
|
`--ba:${theme.accentColor}`,
|
|
155
200
|
`--ba-fg:${theme.accentFg}`,
|
|
156
201
|
`--br:${theme.cornerRadius}`,
|
|
157
|
-
`--bf
|
|
202
|
+
// The body font is a host-overridable HOOK (POR-10742): `--cr-bf` resolves a host-page
|
|
203
|
+
// `--bf` (set on any ancestor) with the fontSlot stack as the FALLBACK — so a storefront
|
|
204
|
+
// can theme the card font via plain CSS, no `!important` (the old inline `--bf:<stack>`
|
|
205
|
+
// declaration shadowed a host `--bf`). Unset ⇒ resolves to the stack ⇒ byte-identical render.
|
|
206
|
+
`--cr-bf:var(--bf,${RENDER_BLOCK_FONT_STACKS[theme.fontSlot]})`,
|
|
158
207
|
`--surface:${theme.surface}`,
|
|
208
|
+
// v2 chrome-ink (Phase 1): the header/seal bar FILL + its auto-contrast FOREGROUND — the exact
|
|
209
|
+
// `--certrev-bar-*` wire vars the Shopify/WP engines emit, present ONLY on a themed bar. ABSENT ⇒
|
|
210
|
+
// the consumers' `var(--certrev-bar-bg, #0a1b3f)` / `var(--certrev-bar-fg, #fff)` navy/white
|
|
211
|
+
// fallbacks resolve — byte-identical to the un-themed bar.
|
|
212
|
+
...(theme.barInk && theme.barInkFg
|
|
213
|
+
? [`--certrev-bar-bg:${theme.barInk}`, `--certrev-bar-fg:${theme.barInkFg}`]
|
|
214
|
+
: []),
|
|
159
215
|
`--navy:#0a1b3f`,
|
|
160
216
|
`--navy-06:rgba(10,27,63,.06)`,
|
|
161
217
|
`--navy-10:rgba(10,27,63,.10)`,
|
|
@@ -163,23 +219,36 @@ function rootStyle(theme) {
|
|
|
163
219
|
`--navy-55:rgba(10,27,63,.55)`,
|
|
164
220
|
`--navy-75:rgba(10,27,63,.75)`,
|
|
165
221
|
`--stone:#ece9e1`,
|
|
222
|
+
// v2 chrome-ink (Phase 1): the FREE-content trust-ink family (names/title/credential/bio/
|
|
223
|
+
// initials). `--certrev-ink` is the wire var, emitted ONLY when the def sets `inkColor`; `--cr-ink`
|
|
224
|
+
// consumes it with a navy fallback, so an un-inkColor'd face resolves to navy. The `-sub`/`-soft`
|
|
225
|
+
// muted scale is the EXACT navy literal when unthemed (byte-identical, zero color-mix dependency)
|
|
226
|
+
// and a color-mix derivation off the brand ink when themed. The LOCKED trust text keeps `--navy*`
|
|
227
|
+
// and never this family, so a brand ink can never touch a legal disclosure.
|
|
228
|
+
...(ink ? [`--certrev-ink:${ink}`] : []),
|
|
229
|
+
`--cr-ink:var(--certrev-ink,#0a1b3f)`,
|
|
230
|
+
`--cr-ink-sub:${ink ? 'color-mix(in srgb,var(--cr-ink) 55%,transparent)' : 'rgba(10,27,63,.55)'}`,
|
|
231
|
+
`--cr-ink-soft:${ink ? 'color-mix(in srgb,var(--cr-ink) 75%,transparent)' : 'rgba(10,27,63,.75)'}`,
|
|
166
232
|
`--font-mono:${RENDER_BLOCK_FONT_STACKS.mono}`,
|
|
167
|
-
`font-family:var(--bf)`,
|
|
233
|
+
`font-family:var(--cr-bf)`,
|
|
168
234
|
`color:var(--navy)`,
|
|
169
235
|
].join(';');
|
|
170
236
|
}
|
|
171
|
-
function root(mode, theme, inner) {
|
|
237
|
+
function root(mode, theme, inner, face) {
|
|
238
|
+
// The rung is an OBSERVABILITY stamp for the crawl monitor — only emitted when a
|
|
239
|
+
// face is present, so the grandfathered (face-absent) output stays byte-identical.
|
|
240
|
+
const rungAttr = face ? ` data-certrev-rung="${escapeAttribute(face.rung)}"` : '';
|
|
172
241
|
return (`<div class="certrev-cert certrev-cert--${mode}" data-certrev-mode="${mode}" ` +
|
|
173
|
-
`data-certrev-strings-version="${CERT_STRINGS_VERSION}" style="${rootStyle(theme)}">${inner}</div>`);
|
|
242
|
+
`data-certrev-strings-version="${CERT_STRINGS_VERSION}"${rungAttr} style="${rootStyle(theme)}">${inner}</div>`);
|
|
174
243
|
}
|
|
175
244
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
176
245
|
// Mode 1 — BANNER: a certification header card + a separate expert-memo card
|
|
177
246
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
-
function bannerHeaderCard(facts) {
|
|
247
|
+
function bannerHeaderCard(facts, placed) {
|
|
179
248
|
const compensationCue = locked(facts.compensationCue, COMPENSATED_EXPERT_CUE);
|
|
180
249
|
const certified = formatDate(facts.certifiedAt) ?? escapeHtml(facts.certifiedAt);
|
|
181
|
-
const subtitle = facts.authorTitle
|
|
182
|
-
? `<div style="font-size:13px;color:var(--
|
|
250
|
+
const subtitle = placed('authorTitle') && facts.authorTitle
|
|
251
|
+
? `<div style="font-size:13px;color:var(--cr-ink-sub);">${escapeHtml(facts.authorTitle)}</div>`
|
|
183
252
|
: '';
|
|
184
253
|
// A subtle right-aligned arrow opens the certificate: the whole navy bar is the mouse
|
|
185
254
|
// target (data-certrev-modal-open); this focusable <a> is the keyboard / SR / no-JS control.
|
|
@@ -187,100 +256,237 @@ function bannerHeaderCard(facts) {
|
|
|
187
256
|
const headerArrow = `<a href="${escapeAttribute(certHref)}" data-certrev-modal-open rel="noopener" aria-label="View certificate" ` +
|
|
188
257
|
`style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;">` +
|
|
189
258
|
`<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a>`;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`<div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
259
|
+
// navy header — the trust bar stays fixed navy (accent theming is Phase B); mark + eyebrow + cert arrow
|
|
260
|
+
const header = placed('label')
|
|
261
|
+
? `<div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;">` +
|
|
262
|
+
`<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)} Expert reviewed</span>` +
|
|
263
|
+
headerArrow +
|
|
264
|
+
`</div>`
|
|
265
|
+
: '';
|
|
266
|
+
// written by — muted label (navy-35); the whole author column is gated on `authorName`
|
|
267
|
+
const showAuthor = placed('authorName');
|
|
268
|
+
const authorColumn = showAuthor
|
|
269
|
+
? `<div>` +
|
|
270
|
+
`<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>` +
|
|
271
|
+
`<div style="display:flex;align-items:center;gap:12px;">${avatar(initials(facts.authorName), 42, 12)}` +
|
|
272
|
+
`<div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">${escapeHtml(facts.authorName)}</div>${subtitle}</div></div>` +
|
|
273
|
+
`</div>`
|
|
274
|
+
: '';
|
|
275
|
+
// grid collapses to a single reviewer column when the author is absent
|
|
276
|
+
const gridCols = showAuthor ? '1fr 1fr' : '1fr';
|
|
277
|
+
// reviewed by — emphasized label (navy, 700): the certification's authority voice
|
|
278
|
+
const reviewerAvatar = placed('reviewerPhoto') ? avatar(initials(facts.reviewerName), 42, 12) : '';
|
|
279
|
+
const reviewerBlock = verificationBlock(facts, true, placed);
|
|
280
|
+
const revToggle = reviewerBlock
|
|
281
|
+
? `<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;">` +
|
|
282
|
+
`${reviewerAvatar}${reviewerBlock}` +
|
|
283
|
+
`</div>`
|
|
284
|
+
: '';
|
|
285
|
+
// "Certified <date>" is card chrome (renders with the card); the cue is engine-placed
|
|
286
|
+
const cueSpan = placed('compensationCue') ? `<span>${escapeHtml(compensationCue)}</span>` : '';
|
|
287
|
+
return (`<div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);">` +
|
|
288
|
+
header +
|
|
196
289
|
`<div style="padding:22px 24px 18px;">` +
|
|
197
|
-
`<div style="display:grid;grid-template-columns
|
|
198
|
-
|
|
199
|
-
`<div>` +
|
|
200
|
-
`<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>` +
|
|
201
|
-
`<div style="display:flex;align-items:center;gap:12px;">${avatar(initials(facts.authorName), 42, 12)}` +
|
|
202
|
-
`<div><div style="font-size:15px;font-weight:700;">${escapeHtml(facts.authorName)}</div>${subtitle}</div></div>` +
|
|
203
|
-
`</div>` +
|
|
204
|
-
// reviewed by — emphasized label (navy, 700): the certification's authority voice
|
|
290
|
+
`<div style="display:grid;grid-template-columns:${gridCols};gap:22px;">` +
|
|
291
|
+
authorColumn +
|
|
205
292
|
`<div>` +
|
|
206
293
|
`<div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div>` +
|
|
207
|
-
|
|
208
|
-
`${avatar(initials(facts.reviewerName), 42, 12)}${verificationBlock(facts, true)}` +
|
|
209
|
-
`</div>` +
|
|
294
|
+
revToggle +
|
|
210
295
|
`</div>` +
|
|
211
296
|
`</div>` +
|
|
212
297
|
// hairline + mono footer — "Certified <date>" + "Compensated expert", UNIFORM navy-55
|
|
213
298
|
`<div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div>` +
|
|
214
299
|
`<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);">` +
|
|
215
300
|
`<span>Certified <span style="margin-left:8px;">${certified}</span></span>` +
|
|
216
|
-
|
|
301
|
+
cueSpan +
|
|
217
302
|
`</div>` +
|
|
218
303
|
`</div>` +
|
|
219
304
|
`</div>`);
|
|
220
305
|
}
|
|
221
|
-
function bannerMemoCard(facts) {
|
|
306
|
+
function bannerMemoCard(facts, placed) {
|
|
222
307
|
const scopeLine = locked(facts.scopeLine, CERT_SCOPE_LINE);
|
|
223
|
-
const credTail = facts.credential
|
|
224
|
-
? `<span style="color:var(--
|
|
308
|
+
const credTail = placed('bylineCredentialed') && facts.credential
|
|
309
|
+
? `<span style="color:var(--cr-ink-sub);">, ${escapeHtml(facts.credential)}</span>`
|
|
225
310
|
: '';
|
|
226
311
|
const first = firstNameOf(facts.reviewerName);
|
|
312
|
+
// The eyebrow + reviewer name row + memo quote are grouped under `memo`; the avatar
|
|
313
|
+
// inside it is gated on `reviewerPhoto`.
|
|
314
|
+
const memoAvatar = placed('reviewerPhoto') ? avatar(initials(facts.reviewerName), 46, 13) : '';
|
|
315
|
+
const memoSection = placed('memo')
|
|
316
|
+
? `<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>` +
|
|
317
|
+
`<div style="display:flex;gap:15px;">${memoAvatar}` +
|
|
318
|
+
`<div style="flex:1;min-width:0;">` +
|
|
319
|
+
`<div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">${escapeHtml(facts.reviewerName)}</span>${credTail}</div>` +
|
|
320
|
+
// Readable, UPRIGHT DM Sans quote (Owen 2026-07-17: the display-serif italic hurt legibility);
|
|
321
|
+
// pre-line preserves the reviewer's paragraph breaks. No left accent bar (matches the guide).
|
|
322
|
+
`<p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">${escapeHtml(facts.memo)}</p>` +
|
|
323
|
+
`</div>` +
|
|
324
|
+
`</div>`
|
|
325
|
+
: '';
|
|
227
326
|
// The memo footer's profile/certificate actions read as the JetBrains-mono uppercase stamp
|
|
228
327
|
// that matches the badge footer lockup (Owen 2026-07-17) — navy, so they still read as actions.
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
328
|
+
const profile = placed('profileLink')
|
|
329
|
+
? profileLink(facts.profileUrl, `${escapeHtml(first)}'s Profile`, STAMP_LINK_STYLE)
|
|
330
|
+
: '';
|
|
331
|
+
const certificate = placed('certificateLink')
|
|
332
|
+
? certificateLink(facts.certificateUrl, 'Certificate', STAMP_LINK_STYLE)
|
|
333
|
+
: '';
|
|
334
|
+
const scope = placed('scopeLine')
|
|
335
|
+
? `<div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">${escapeHtml(scopeLine)}</div>`
|
|
336
|
+
: '';
|
|
337
|
+
return (`<div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);">` +
|
|
338
|
+
memoSection +
|
|
240
339
|
// footer hairline + one trust row: "Verified by CertREV" left, the two actions right
|
|
241
340
|
`<div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div>` +
|
|
242
341
|
`<div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;">` +
|
|
243
342
|
`<div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);">${logo(15)}<span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div>` +
|
|
244
|
-
`<div style="display:flex;flex-wrap:wrap;gap:
|
|
245
|
-
|
|
246
|
-
|
|
343
|
+
`<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;">` +
|
|
344
|
+
profile +
|
|
345
|
+
(profile && certificate
|
|
346
|
+
? '<span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span>'
|
|
347
|
+
: '') +
|
|
348
|
+
certificate +
|
|
247
349
|
`</div>` +
|
|
248
350
|
`</div>` +
|
|
249
|
-
|
|
351
|
+
scope +
|
|
250
352
|
`</div>`);
|
|
251
353
|
}
|
|
252
354
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
253
355
|
// Mode 2 — SIDEBAR: a reviewer card pinned beside the article body
|
|
254
356
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
255
|
-
function sidebarCard(facts) {
|
|
357
|
+
function sidebarCard(facts, placed) {
|
|
256
358
|
const compensationCue = locked(facts.compensationCue, COMPENSATED_EXPERT_CUE);
|
|
257
359
|
const scopeLine = locked(facts.scopeLine, CERT_SCOPE_LINE);
|
|
258
|
-
const
|
|
259
|
-
? `<
|
|
360
|
+
const header = placed('label')
|
|
361
|
+
? `<div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;">${logo(18)} Expert reviewed</div>`
|
|
362
|
+
: '';
|
|
363
|
+
const sidebarAvatar = placed('reviewerPhoto') ? avatar(initials(facts.reviewerName), 46, 13) : '';
|
|
364
|
+
const bioPara = placed('bio') && facts.bio
|
|
365
|
+
? `<p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">${escapeHtml(facts.bio)}</p>`
|
|
366
|
+
: '';
|
|
367
|
+
const blockquote = placed('memo')
|
|
368
|
+
? `<blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">${escapeHtml(facts.memo)}</blockquote>`
|
|
369
|
+
: '';
|
|
370
|
+
// Disclosure row: cue + ` · ` + scope; the separator only when BOTH land, the row only when either does.
|
|
371
|
+
const cueEl = placed('compensationCue')
|
|
372
|
+
? `<span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">${escapeHtml(compensationCue)}</span>`
|
|
260
373
|
: '';
|
|
261
|
-
|
|
262
|
-
|
|
374
|
+
const scopeEl = placed('scopeLine') ? escapeHtml(scopeLine) : '';
|
|
375
|
+
const disclosure = cueEl || scopeEl
|
|
376
|
+
? `<div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;">${cueEl}${cueEl && scopeEl ? ' · ' : ''}${scopeEl}</div>`
|
|
377
|
+
: '';
|
|
378
|
+
// Footer grid: each cell gated on its link; `1fr` (and no border-right on a lone profile) when only one.
|
|
379
|
+
const hasProfile = placed('profileLink');
|
|
380
|
+
const hasCert = placed('certificateLink');
|
|
381
|
+
const profileBorder = hasProfile && hasCert ? 'border-right:1px solid var(--navy-10);' : '';
|
|
382
|
+
const profileCell = hasProfile
|
|
383
|
+
? profileLink(facts.profileUrl, 'View Profile', `text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;${profileBorder}cursor:pointer;`)
|
|
384
|
+
: '';
|
|
385
|
+
const certCell = hasCert
|
|
386
|
+
? certificateLink(facts.certificateUrl, 'View Certificate', 'text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;')
|
|
387
|
+
: '';
|
|
388
|
+
const footer = hasProfile || hasCert
|
|
389
|
+
? `<div style="display:grid;grid-template-columns:${hasProfile && hasCert ? '1fr 1fr' : '1fr'};border-top:1px solid var(--navy-10);">` +
|
|
390
|
+
profileCell +
|
|
391
|
+
certCell +
|
|
392
|
+
`</div>`
|
|
393
|
+
: '';
|
|
394
|
+
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(--cr-bf);">` +
|
|
395
|
+
header +
|
|
263
396
|
`<div style="padding:20px;">` +
|
|
264
|
-
`<div style="display:flex;align-items:flex-start;gap:13px;">${
|
|
397
|
+
`<div style="display:flex;align-items:flex-start;gap:13px;">${sidebarAvatar}${verificationBlock(facts, false, placed)}</div>` +
|
|
265
398
|
bioPara +
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
`</div>` +
|
|
269
|
-
`<div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);">` +
|
|
270
|
-
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;') +
|
|
271
|
-
certificateLink(facts.certificateUrl, 'View Certificate', 'text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;') +
|
|
399
|
+
blockquote +
|
|
400
|
+
disclosure +
|
|
272
401
|
`</div>` +
|
|
402
|
+
footer +
|
|
273
403
|
`</div>`);
|
|
274
404
|
}
|
|
275
405
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
276
406
|
// Mode 3 — FLOATING: a compact pinned pill (a modal pointer, NOT a claim face)
|
|
277
407
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
278
|
-
function floatingPill(facts) {
|
|
279
|
-
|
|
280
|
-
`<span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span>`
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
408
|
+
function floatingPill(facts, placed) {
|
|
409
|
+
const label = placed('label')
|
|
410
|
+
? `<span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span>`
|
|
411
|
+
: '';
|
|
412
|
+
const profile = placed('profileLink')
|
|
413
|
+
? profileLink(facts.profileUrl, 'Profile', 'font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;')
|
|
414
|
+
: '';
|
|
415
|
+
const cert = placed('certificateLink')
|
|
416
|
+
? certificateLink(facts.certificateUrl, 'Certificate', 'font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;')
|
|
417
|
+
: '';
|
|
418
|
+
// The `·` separator only when BOTH links land.
|
|
419
|
+
const sep = placed('profileLink') && placed('certificateLink') ? `<span style="opacity:.5;">·</span>` : '';
|
|
420
|
+
return (
|
|
421
|
+
// v2 chrome-ink (Phase 1): the minimal-tier pill is the SAME trust surface as the header bar — it
|
|
422
|
+
// follows barInk/barInkFg so a themed brand's compact format matches its card (portal parity; the
|
|
423
|
+
// portal `.certrev-pill` reads the same `--certrev-bar-*` vars). Navy/white fallbacks ⇒ byte-identical
|
|
424
|
+
// when unthemed. NOTE: this moves the pill OFF the accent (`--ba`) it used pre-v2 — see the port report.
|
|
425
|
+
`<div class="certrev-cert__pill" style="display:inline-flex;align-items:center;gap:14px;padding:12px 22px;border-radius:999px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);box-shadow:0 10px 26px var(--navy-10);">` +
|
|
426
|
+
label +
|
|
427
|
+
profile +
|
|
428
|
+
sep +
|
|
429
|
+
cert +
|
|
430
|
+
`</div>`);
|
|
431
|
+
}
|
|
432
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
433
|
+
// Mode 4 — CUSTOM: the engine's free-composition face — ONE vertical stack of
|
|
434
|
+
// EXACTLY the placed fields, in canonical order, from the SAME escaped / locked() /
|
|
435
|
+
// data-certrev-* building blocks the locked layouts use (so the ftc-guard + crawl
|
|
436
|
+
// monitor see identical markers). Never consults `rung`.
|
|
437
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
438
|
+
function renderCustomFace(facts, placed) {
|
|
439
|
+
const parts = [];
|
|
440
|
+
// 1. label — the navy "Expert reviewed" header bar (sidebar's header markup)
|
|
441
|
+
if (placed('label')) {
|
|
442
|
+
parts.push(`<div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;">${logo(18)} Expert reviewed</div>`);
|
|
443
|
+
}
|
|
444
|
+
// 2. authorName — the "Written by" row (subtitle nested iff authorTitle placed + present)
|
|
445
|
+
if (placed('authorName')) {
|
|
446
|
+
const subtitle = placed('authorTitle') && facts.authorTitle
|
|
447
|
+
? `<div style="font-size:13px;color:var(--cr-ink-sub);">${escapeHtml(facts.authorTitle)}</div>`
|
|
448
|
+
: '';
|
|
449
|
+
parts.push(`<div>` +
|
|
450
|
+
`<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>` +
|
|
451
|
+
`<div style="display:flex;align-items:center;gap:12px;">${avatar(initials(facts.authorName), 42, 12)}` +
|
|
452
|
+
`<div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">${escapeHtml(facts.authorName)}</div>${subtitle}</div></div>` +
|
|
453
|
+
`</div>`);
|
|
454
|
+
}
|
|
455
|
+
// 3. reviewer row — optional photo + the self-gating verification block
|
|
456
|
+
const reviewerAvatar = placed('reviewerPhoto') ? avatar(initials(facts.reviewerName), 46, 13) : '';
|
|
457
|
+
const reviewerBlock = verificationBlock(facts, false, placed);
|
|
458
|
+
if (reviewerAvatar || reviewerBlock) {
|
|
459
|
+
parts.push(`<div style="display:flex;align-items:flex-start;gap:13px;">${reviewerAvatar}${reviewerBlock}</div>`);
|
|
460
|
+
}
|
|
461
|
+
// 4. bio (sidebar's bio paragraph)
|
|
462
|
+
if (placed('bio') && facts.bio) {
|
|
463
|
+
parts.push(`<p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">${escapeHtml(facts.bio)}</p>`);
|
|
464
|
+
}
|
|
465
|
+
// 5. memo (sidebar's blockquote)
|
|
466
|
+
if (placed('memo')) {
|
|
467
|
+
parts.push(`<blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">${escapeHtml(facts.memo)}</blockquote>`);
|
|
468
|
+
}
|
|
469
|
+
// 6. disclosures (sidebar's cue · scope row; separator only when BOTH land)
|
|
470
|
+
const cueEl = placed('compensationCue')
|
|
471
|
+
? `<span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">${escapeHtml(locked(facts.compensationCue, COMPENSATED_EXPERT_CUE))}</span>`
|
|
472
|
+
: '';
|
|
473
|
+
const scopeEl = placed('scopeLine') ? escapeHtml(locked(facts.scopeLine, CERT_SCOPE_LINE)) : '';
|
|
474
|
+
if (cueEl || scopeEl) {
|
|
475
|
+
parts.push(`<div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;">${cueEl}${cueEl && scopeEl ? ' · ' : ''}${scopeEl}</div>`);
|
|
476
|
+
}
|
|
477
|
+
// 7. action row — the memo-card stamp links (same data-certrev-modal-open hooks)
|
|
478
|
+
const first = firstNameOf(facts.reviewerName);
|
|
479
|
+
const profile = placed('profileLink')
|
|
480
|
+
? profileLink(facts.profileUrl, `${escapeHtml(first)}'s Profile`, STAMP_LINK_STYLE)
|
|
481
|
+
: '';
|
|
482
|
+
const certificate = placed('certificateLink')
|
|
483
|
+
? certificateLink(facts.certificateUrl, 'Certificate', STAMP_LINK_STYLE)
|
|
484
|
+
: '';
|
|
485
|
+
if (profile || certificate) {
|
|
486
|
+
parts.push(`<div style="display:flex;flex-wrap:wrap;gap:22px;">${profile}${certificate}</div>`);
|
|
487
|
+
}
|
|
488
|
+
return (`<div style="border:1px solid var(--navy-10);border-radius:var(--br,14px);background:var(--surface);padding:20px;font-family:var(--cr-bf);display:flex;flex-direction:column;gap:14px;">` +
|
|
489
|
+
parts.join('') +
|
|
284
490
|
`</div>`);
|
|
285
491
|
}
|
|
286
492
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -293,14 +499,32 @@ function floatingPill(facts) {
|
|
|
293
499
|
*/
|
|
294
500
|
export function renderCertBlock(input) {
|
|
295
501
|
const theme = resolveBlockTheme(input.theme);
|
|
296
|
-
const { facts, mode } = input;
|
|
502
|
+
const { facts, mode, face } = input;
|
|
503
|
+
const placed = placedTester(face);
|
|
297
504
|
switch (mode) {
|
|
298
|
-
case 'banner':
|
|
299
|
-
|
|
505
|
+
case 'banner': {
|
|
506
|
+
// Suppress the memo card entirely when the engine placed none of its fields
|
|
507
|
+
// (avoids an empty bordered shell). Grandfather: all placed ⇒ card renders.
|
|
508
|
+
const memoCard = placed('memo') || placed('profileLink') || placed('certificateLink') || placed('scopeLine')
|
|
509
|
+
? bannerMemoCard(facts, placed)
|
|
510
|
+
: '';
|
|
511
|
+
// First-class memo split (POR-10741): default 'full' is byte-identical to the prior
|
|
512
|
+
// header+memo render; 'header'/'memo' emit only that card so the exporter can place
|
|
513
|
+
// the memo as its own block (retires the CertReviewCard string-split shim).
|
|
514
|
+
const part = input.part ?? 'full';
|
|
515
|
+
const inner = part === 'header'
|
|
516
|
+
? bannerHeaderCard(facts, placed)
|
|
517
|
+
: part === 'memo'
|
|
518
|
+
? memoCard
|
|
519
|
+
: bannerHeaderCard(facts, placed) + memoCard;
|
|
520
|
+
return root(mode, theme, inner, face);
|
|
521
|
+
}
|
|
300
522
|
case 'sidebar':
|
|
301
|
-
return root(mode, theme, sidebarCard(facts));
|
|
523
|
+
return root(mode, theme, sidebarCard(facts, placed), face);
|
|
302
524
|
case 'floating':
|
|
303
|
-
return root(mode, theme, floatingPill(facts));
|
|
525
|
+
return root(mode, theme, floatingPill(facts, placed), face);
|
|
526
|
+
case 'custom':
|
|
527
|
+
return root(mode, theme, renderCustomFace(facts, placed), face);
|
|
304
528
|
}
|
|
305
529
|
}
|
|
306
530
|
//# sourceMappingURL=render-cert-block.js.map
|