@certrev/cert-block 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -1
- package/dist/builder/cert-chrome-data.d.ts +129 -0
- package/dist/builder/cert-chrome-data.d.ts.map +1 -0
- package/dist/builder/cert-chrome-data.js +59 -0
- package/dist/builder/cert-chrome-data.js.map +1 -0
- package/dist/builder/cert-review-card.d.ts +68 -0
- package/dist/builder/cert-review-card.d.ts.map +1 -0
- package/dist/builder/cert-review-card.js +129 -0
- package/dist/builder/cert-review-card.js.map +1 -0
- package/dist/builder/index.d.ts +2 -0
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +3 -0
- package/dist/builder/index.js.map +1 -1
- package/dist/components/preview-block.d.ts +43 -0
- package/dist/components/preview-block.d.ts.map +1 -0
- package/dist/components/preview-block.js +53 -0
- package/dist/components/preview-block.js.map +1 -0
- package/dist/components/render-cert-block.d.ts +50 -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 +10 -4
- package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +10 -6
- package/src/__tests__/preview-block.test.ts +84 -0
- package/src/__tests__/render-cert-block.test.ts +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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* `BuilderCertChromeData` — the CANONICAL headless cert-chrome wire type
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The brand-render projection of the cert envelope (portal POR-10091) — the OPTIONS
|
|
7
|
+
* of a brand's native cert component (e.g. e.l.f.'s registered `CertReviewCard`) on
|
|
8
|
+
* a headless blocks-mode entry (Builder.io). CertREV serves the data + the ready-made
|
|
9
|
+
* strings; the brand renders it within the design-guide ladder (enforced live by the
|
|
10
|
+
* crawl monitor + the MSA anti-stripping teeth).
|
|
11
|
+
*
|
|
12
|
+
* SINGLE SOURCE (POR-10721 W1): this type is HOMED HERE so the portal exporter
|
|
13
|
+
* (`src/lib/builder/cert-chrome.ts`) and the kit component (`CertReviewCard`) +
|
|
14
|
+
* `BUILDER_REGISTRATION.inputs` all reference ONE definition — killing the drift the
|
|
15
|
+
* hand-rolled Hydrogen-replica component suffered (it carried a stale separate
|
|
16
|
+
* `credential`/`credentialVerifiedAt` shape from before the fused-pair projection).
|
|
17
|
+
* Keep byte-for-byte in sync with the portal interface until the portal migrates to
|
|
18
|
+
* import THIS (that migration is gated on publishing the package that carries it).
|
|
19
|
+
*
|
|
20
|
+
* PURITY: types + a pure key tuple only — no runtime deps, tree-shakeable.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The baked render-def projection (the Shopify `render_def` metafield shape) the
|
|
25
|
+
* exporter delivers so a brand's Builder face themes byte-identically to its Shopify
|
|
26
|
+
* face. The component maps `{ tokens, version }` onto `renderCertBlock`'s theme +
|
|
27
|
+
* `data-certrev-def-version` stamp. Structurally matches the portal's
|
|
28
|
+
* `ShopifyRenderDefMetafield` (single-sourcing that metafield type too is a follow-up).
|
|
29
|
+
*/
|
|
30
|
+
export interface CertChromeRenderDef {
|
|
31
|
+
/** Monotonic per-brand def version → the `data-certrev-def-version` stamp. */
|
|
32
|
+
readonly v: number
|
|
33
|
+
readonly preset: string
|
|
34
|
+
readonly rung: string
|
|
35
|
+
/**
|
|
36
|
+
* The resolved themeable tokens (the baked metafield's `tokens`) — includes the v2
|
|
37
|
+
* `barInk`/`inkColor`, so this is a superset of the input `CertThemeTokens` (which
|
|
38
|
+
* predates brand-ink theming). Inlined structurally to match what the exporter bakes.
|
|
39
|
+
*/
|
|
40
|
+
readonly tokens: {
|
|
41
|
+
readonly accentColor?: string
|
|
42
|
+
readonly surface?: string
|
|
43
|
+
readonly cornerRadius?: string
|
|
44
|
+
readonly fontSlot?: string
|
|
45
|
+
readonly barInk?: string
|
|
46
|
+
readonly inkColor?: string
|
|
47
|
+
}
|
|
48
|
+
/** The auto-contrast bar foreground — present only on a barInk-themed face (v2). */
|
|
49
|
+
readonly barInkFg?: '#141414' | '#ffffff'
|
|
50
|
+
/** The subtractive field-visibility hide-set. */
|
|
51
|
+
readonly hidden: readonly string[]
|
|
52
|
+
/** v2 layout axis. Omitted ⇒ 'banner'. */
|
|
53
|
+
readonly layout?: string
|
|
54
|
+
/** Custom-face composition — present only when layout === 'custom'. */
|
|
55
|
+
readonly customFace?: { readonly placedFields: readonly string[] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The OPTIONS a brand's native cert component receives. Delivered per certified
|
|
60
|
+
* article; until the brand registers the component the block renders nothing.
|
|
61
|
+
*/
|
|
62
|
+
export interface BuilderCertChromeData {
|
|
63
|
+
/**
|
|
64
|
+
* The credentialed byline — credential + dated CertREV verification as ONE
|
|
65
|
+
* inseparable string. Null when the R1 gate fails (a displayed credential is
|
|
66
|
+
* unverified / missing `verified_at`); the brand then has only `bylinePlain`.
|
|
67
|
+
*/
|
|
68
|
+
bylineCredentialed: string | null
|
|
69
|
+
/**
|
|
70
|
+
* The RAW verified credential welded to its dated verification, delivered as ONE
|
|
71
|
+
* INSEPARABLE PAIR (present together) or `null` (omitted together). The WHOLE
|
|
72
|
+
* point: the type hands the brand `credential` and `verifiedAt` only as the two
|
|
73
|
+
* keys of one object, so a bare credential without its dated verification is
|
|
74
|
+
* UNCONSTRUCTIBLE — the fused-verification invariant enforced by construction, not
|
|
75
|
+
* a hopeful side rule. Non-null iff `bylineCredentialed` is. The sidebar/banner
|
|
76
|
+
* faces map this to their `credential` + `credentialVerifiedAt` inputs.
|
|
77
|
+
*/
|
|
78
|
+
credentialVerification: { credential: string; verifiedAt: string } | null
|
|
79
|
+
/** The name-only byline (`Reviewed [date] by [Name].`). Always present. */
|
|
80
|
+
bylinePlain: string
|
|
81
|
+
/** The reviewer's resolved professional name (QA-safe), raw, for a name-only layout. */
|
|
82
|
+
reviewerName: string
|
|
83
|
+
/** Reviewer's short verified bio, or null (verified-only). */
|
|
84
|
+
bio: string | null
|
|
85
|
+
/** Reviewer's longer verified background, or null (verified-only). */
|
|
86
|
+
background: string | null
|
|
87
|
+
/** The QA-SCREENED `verifiedExpertMemo` (never the raw in-flight `expertMemo`), or null. */
|
|
88
|
+
memo: string | null
|
|
89
|
+
/** The compensation cue (`Compensated expert`). Null for a pro-bono reviewer. */
|
|
90
|
+
compensationCue: string | null
|
|
91
|
+
/** The verbatim FTC scope disclaimer. Always present. */
|
|
92
|
+
scopeLine: string
|
|
93
|
+
/** Reviewer photo URL (cert-frozen override ?? live), or null. */
|
|
94
|
+
reviewerPhoto: string | null
|
|
95
|
+
/** The CertREV expert-profile URL (`certrev.com/expert/{id}`) — never LinkedIn. */
|
|
96
|
+
reviewerProfileUrl: string | null
|
|
97
|
+
/** The public certificate-verification URL (`/verify/{id}`). */
|
|
98
|
+
verifyUrl: string
|
|
99
|
+
/** When the article was certified (ISO 8601) — the same date baked into the bylines. */
|
|
100
|
+
certifiedAt: string
|
|
101
|
+
/** The last real content change after certification (ISO 8601), or null. */
|
|
102
|
+
contentModifiedAt: string | null
|
|
103
|
+
/** The article's AUTHOR (a different person from the reviewer; carries no verification claim). */
|
|
104
|
+
authorName: string | null
|
|
105
|
+
authorTitle: string | null
|
|
106
|
+
/** Brand display prefs the rendering component honors. */
|
|
107
|
+
display: {
|
|
108
|
+
showExpertPhoto: boolean
|
|
109
|
+
/** When false the component renders `bylinePlain` even if the credentialed form was delivered. */
|
|
110
|
+
showCredentials: boolean
|
|
111
|
+
}
|
|
112
|
+
/** The brand's ACTIVE resolved render-def projection, or null (⇒ preset default). */
|
|
113
|
+
renderDef: CertChromeRenderDef | null
|
|
114
|
+
/** Version of the delivered string set (`data-certrev-strings-version` for the crawl monitor). */
|
|
115
|
+
stringsVersion: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The ordered keys of `BuilderCertChromeData` — the SINGLE SOURCE the Builder
|
|
120
|
+
* `inputs` array and the W3 exporter⇄inputs⇄wire-type parity lock derive from, so
|
|
121
|
+
* the three can never silently drift again (POR-10721 W3). Adding a field to the
|
|
122
|
+
* interface without adding it here (or vice-versa) fails the `keyof` exhaustiveness
|
|
123
|
+
* check in `assertChromeKeysExhaustive` below.
|
|
124
|
+
*/
|
|
125
|
+
export const BUILDER_CERT_CHROME_KEYS = [
|
|
126
|
+
'bylineCredentialed',
|
|
127
|
+
'credentialVerification',
|
|
128
|
+
'bylinePlain',
|
|
129
|
+
'reviewerName',
|
|
130
|
+
'bio',
|
|
131
|
+
'background',
|
|
132
|
+
'memo',
|
|
133
|
+
'compensationCue',
|
|
134
|
+
'scopeLine',
|
|
135
|
+
'reviewerPhoto',
|
|
136
|
+
'reviewerProfileUrl',
|
|
137
|
+
'verifyUrl',
|
|
138
|
+
'certifiedAt',
|
|
139
|
+
'contentModifiedAt',
|
|
140
|
+
'authorName',
|
|
141
|
+
'authorTitle',
|
|
142
|
+
'display',
|
|
143
|
+
'renderDef',
|
|
144
|
+
'stringsVersion',
|
|
145
|
+
] as const
|
|
146
|
+
|
|
147
|
+
export type BuilderCertChromeKey = (typeof BUILDER_CERT_CHROME_KEYS)[number]
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compile-time proof that `BUILDER_CERT_CHROME_KEYS` === `keyof BuilderCertChromeData`
|
|
151
|
+
* exactly (both directions). If a field is added/removed on the interface without the
|
|
152
|
+
* matching key edit, ONE of these two assignments stops type-checking — so the tuple
|
|
153
|
+
* can never drift from the type silently.
|
|
154
|
+
*/
|
|
155
|
+
const _keysCoverType: Record<keyof BuilderCertChromeData, true> = Object.fromEntries(
|
|
156
|
+
BUILDER_CERT_CHROME_KEYS.map((k) => [k, true]),
|
|
157
|
+
) as Record<BuilderCertChromeKey, true>
|
|
158
|
+
const _typeCoversKeys: Record<BuilderCertChromeKey, true> = _keysCoverType
|
|
159
|
+
void _typeCoversKeys
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* `CertReviewCard` — the ready-to-register Builder.io PUSH cert component (W1)
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* A headless brand (e.l.f.) registers `BUILDER_REGISTRATION` in its Builder
|
|
7
|
+
* `customComponents` and CertREV's exporter emits a block whose options are the
|
|
8
|
+
* `BuilderCertChromeData` projection — so the visible cert chrome renders itself,
|
|
9
|
+
* design-identical to the Shopify Liquid + WordPress PHP faces (same `renderCertBlock`
|
|
10
|
+
* string renderer). This REPLACES the ~200-line hand-rolled component every headless
|
|
11
|
+
* brand copied from the demo (POR-10721 W1).
|
|
12
|
+
*
|
|
13
|
+
* Improvements over the canary demo's hand-rolled version:
|
|
14
|
+
* - consumes the CANONICAL `BuilderCertChromeData` (the fused `credentialVerification`
|
|
15
|
+
* pair — a credential is UNCONSTRUCTIBLE without its dated verification), not the
|
|
16
|
+
* demo's stale separate `credential`/`credentialVerifiedAt` fields;
|
|
17
|
+
* - maps v2 `barInk`/`inkColor` into the theme (the demo predated brand-ink theming);
|
|
18
|
+
* - honors `display.showCredentials`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type CertBlockMode, renderCertBlock } from '../components/render-cert-block.js'
|
|
22
|
+
import type { ResolvedBlockTheme } from '../components/render-def.js'
|
|
23
|
+
import type { BuilderCertChromeData } from './cert-chrome-data.js'
|
|
24
|
+
import { BUILDER_CERT_CHROME_KEYS } from './cert-chrome-data.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The canonical Builder block name. A brand's registration + the portal
|
|
28
|
+
* `certComponent` connection field default to THIS; config exists only for
|
|
29
|
+
* deviation (portal `client-for-brand.ts` gains the default in a follow-up).
|
|
30
|
+
*/
|
|
31
|
+
export const CERT_COMPONENT_NAME = 'CertREV Cert'
|
|
32
|
+
|
|
33
|
+
export interface CertReviewCardProps extends Partial<BuilderCertChromeData> {
|
|
34
|
+
/** Placement face — the ENTRY/editor chooses this, NOT the exporter (not a wire-type field). */
|
|
35
|
+
mode?: CertBlockMode | null
|
|
36
|
+
/**
|
|
37
|
+
* Banner memo placement (not a wire-type field — the ENTRY/editor chooses it). Passed
|
|
38
|
+
* straight through to `renderCertBlock`'s first-class `part` (POR-10741): `'full'`
|
|
39
|
+
* (default) renders header + memo in one block; `'header'`/`'memo'` render only that
|
|
40
|
+
* card so the memo can be placed as its own block under the article body. The old
|
|
41
|
+
* string-splitting shim is gone — `renderCertBlock` composes each part directly.
|
|
42
|
+
*/
|
|
43
|
+
part?: 'full' | 'header' | 'memo' | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function CertReviewCard(props: CertReviewCardProps) {
|
|
47
|
+
const { reviewerName, verifyUrl } = props
|
|
48
|
+
if (!reviewerName || !verifyUrl) return null // no chrome pre-cert
|
|
49
|
+
|
|
50
|
+
// The credential + its verification date arrive ONLY as the fused pair, so we can
|
|
51
|
+
// never obtain one without the other (name-only face when the pair is null, or when
|
|
52
|
+
// the brand set showCredentials:false). This IS the R1 gate, enforced by the type.
|
|
53
|
+
const cv = props.credentialVerification ?? null
|
|
54
|
+
const showCred = props.display?.showCredentials !== false
|
|
55
|
+
const withCred = cv && showCred ? cv : null
|
|
56
|
+
|
|
57
|
+
const tokens = props.renderDef?.tokens
|
|
58
|
+
const theme: Partial<ResolvedBlockTheme> | undefined = tokens
|
|
59
|
+
? {
|
|
60
|
+
accentColor: tokens.accentColor,
|
|
61
|
+
surface: tokens.surface,
|
|
62
|
+
cornerRadius: tokens.cornerRadius,
|
|
63
|
+
fontSlot: tokens.fontSlot as ResolvedBlockTheme['fontSlot'] | undefined,
|
|
64
|
+
// v2 brand-ink (post-demo): the bar + free-content ink follow the def.
|
|
65
|
+
barInk: tokens.barInk,
|
|
66
|
+
inkColor: tokens.inkColor,
|
|
67
|
+
}
|
|
68
|
+
: undefined
|
|
69
|
+
|
|
70
|
+
const html = renderCertBlock({
|
|
71
|
+
mode: (props.mode as CertBlockMode) ?? 'banner',
|
|
72
|
+
theme,
|
|
73
|
+
// First-class banner memo split (POR-10741) — renderCertBlock composes the requested
|
|
74
|
+
// part directly; no string-splitting. Ignored outside banner mode.
|
|
75
|
+
...(props.part ? { part: props.part } : {}),
|
|
76
|
+
facts: {
|
|
77
|
+
authorName: props.authorName ?? 'Editorial',
|
|
78
|
+
authorTitle: props.authorTitle ?? undefined,
|
|
79
|
+
reviewerName,
|
|
80
|
+
credential: withCred ? withCred.credential : '',
|
|
81
|
+
credentialVerifiedAt: withCred ? withCred.verifiedAt : '',
|
|
82
|
+
certifiedAt: props.certifiedAt ?? '',
|
|
83
|
+
memo: props.memo ?? '',
|
|
84
|
+
bio: props.bio ?? undefined,
|
|
85
|
+
profileUrl: props.reviewerProfileUrl ?? '',
|
|
86
|
+
certificateUrl: verifyUrl,
|
|
87
|
+
// Locked strings default byte-verbatim inside renderCertBlock; pass the
|
|
88
|
+
// delivered cue/scope through so a pro-bono (null-cue) delivery is honored.
|
|
89
|
+
...(props.compensationCue ? { compensationCue: props.compensationCue } : {}),
|
|
90
|
+
...(props.scopeLine ? { scopeLine: props.scopeLine } : {}),
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
data-certrev-cert-chrome=""
|
|
97
|
+
data-certrev-strings-version={props.stringsVersion ?? undefined}
|
|
98
|
+
data-certrev-def-version={props.renderDef?.v ?? undefined}
|
|
99
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: renderCertBlock is the escaping-safe SSR string renderer
|
|
100
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
// Builder.io registration — inputs SINGLE-SOURCED from the wire-type keys (W1/W3)
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/** The subset of the Builder.io input schema this kit emits. */
|
|
110
|
+
export interface BuilderInput {
|
|
111
|
+
readonly name: string
|
|
112
|
+
readonly type: 'string' | 'longText' | 'object' | 'boolean' | 'number'
|
|
113
|
+
readonly friendlyName?: string
|
|
114
|
+
readonly helperText?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Builder input TYPE per wire-type key — the exporter serializes these option values. */
|
|
118
|
+
const WIRE_INPUT_TYPE: Record<(typeof BUILDER_CERT_CHROME_KEYS)[number], BuilderInput['type']> = {
|
|
119
|
+
bylineCredentialed: 'string',
|
|
120
|
+
credentialVerification: 'object',
|
|
121
|
+
bylinePlain: 'string',
|
|
122
|
+
reviewerName: 'string',
|
|
123
|
+
bio: 'longText',
|
|
124
|
+
background: 'longText',
|
|
125
|
+
memo: 'longText',
|
|
126
|
+
compensationCue: 'string',
|
|
127
|
+
scopeLine: 'string',
|
|
128
|
+
reviewerPhoto: 'string',
|
|
129
|
+
reviewerProfileUrl: 'string',
|
|
130
|
+
verifyUrl: 'string',
|
|
131
|
+
certifiedAt: 'string',
|
|
132
|
+
contentModifiedAt: 'string',
|
|
133
|
+
authorName: 'string',
|
|
134
|
+
authorTitle: 'string',
|
|
135
|
+
display: 'object',
|
|
136
|
+
renderDef: 'object',
|
|
137
|
+
stringsVersion: 'string',
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The wire-type inputs — DERIVED from `BUILDER_CERT_CHROME_KEYS`, so the 19-field
|
|
142
|
+
* option set can never drift from the type (the W3 lock asserts name-parity). This
|
|
143
|
+
* kills the demo's hand-maintained 17-entry array.
|
|
144
|
+
*/
|
|
145
|
+
export const WIRE_INPUTS: readonly BuilderInput[] = BUILDER_CERT_CHROME_KEYS.map((name) => ({
|
|
146
|
+
name,
|
|
147
|
+
type: WIRE_INPUT_TYPE[name],
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
/** Placement inputs — chosen by the entry/editor, NOT emitted by the exporter (not wire-type keys). */
|
|
151
|
+
export const PLACEMENT_INPUTS: readonly BuilderInput[] = [
|
|
152
|
+
{
|
|
153
|
+
name: 'mode',
|
|
154
|
+
type: 'string',
|
|
155
|
+
friendlyName: 'Placement',
|
|
156
|
+
helperText: 'banner (default) · sidebar · floating',
|
|
157
|
+
},
|
|
158
|
+
{ name: 'part', type: 'string', friendlyName: 'Banner part', helperText: 'full (default) · header · memo' },
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
/** A Builder.io custom-component registration (the general shape; the anchor uses the same). */
|
|
162
|
+
export interface CertBlockBuilderRegistration {
|
|
163
|
+
readonly name: string
|
|
164
|
+
readonly component: typeof CertReviewCard
|
|
165
|
+
readonly inputs: readonly BuilderInput[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Ready to spread into a brand's Builder `customComponents`:
|
|
170
|
+
* `Builder.registerComponent(BUILDER_REGISTRATION.component, BUILDER_REGISTRATION)`.
|
|
171
|
+
* The exporter-emitted option keys === these `inputs` === the wire type — locked by W3.
|
|
172
|
+
*/
|
|
173
|
+
export const BUILDER_REGISTRATION: CertBlockBuilderRegistration = {
|
|
174
|
+
name: CERT_COMPONENT_NAME,
|
|
175
|
+
component: CertReviewCard,
|
|
176
|
+
inputs: [...PLACEMENT_INPUTS, ...WIRE_INPUTS],
|
|
177
|
+
}
|
package/src/builder/index.tsx
CHANGED
|
@@ -120,3 +120,21 @@ export const certRevAnchorComponent: CertRevBuilderRegistration = {
|
|
|
120
120
|
name: 'CertREV Review',
|
|
121
121
|
inputs: [],
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
// ── POR-10721 W1/W3: the Builder.io PUSH kit (CertReviewCard) + single-sourced wire type ──
|
|
125
|
+
export {
|
|
126
|
+
BUILDER_CERT_CHROME_KEYS,
|
|
127
|
+
type BuilderCertChromeData,
|
|
128
|
+
type BuilderCertChromeKey,
|
|
129
|
+
type CertChromeRenderDef,
|
|
130
|
+
} from './cert-chrome-data.js'
|
|
131
|
+
export {
|
|
132
|
+
BUILDER_REGISTRATION,
|
|
133
|
+
type BuilderInput,
|
|
134
|
+
CERT_COMPONENT_NAME,
|
|
135
|
+
type CertBlockBuilderRegistration,
|
|
136
|
+
CertReviewCard,
|
|
137
|
+
type CertReviewCardProps,
|
|
138
|
+
PLACEMENT_INPUTS,
|
|
139
|
+
WIRE_INPUTS,
|
|
140
|
+
} from './cert-review-card.js'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `renderPreviewBlock` — a STATIC, NON-DELIVERY preview of the cert block for pre-sales
|
|
3
|
+
* look-demos (POR-10743, from the e.l.f. Builder demo).
|
|
4
|
+
*
|
|
5
|
+
* The visual is BYTE-FOR-BYTE `renderCertBlock` (pixel-parity — "see it on your own site
|
|
6
|
+
* today"), wrapped in a `[data-certrev-preview]` container carrying a REQUIRED expiry.
|
|
7
|
+
*
|
|
8
|
+
* Why it is safe by construction — and can NEVER be delivery:
|
|
9
|
+
* - A preview has NO signed envelope and NO modal. CertREV's verify / badge-revalidation /
|
|
10
|
+
* crawl-audit all key off a signed `CertDeliveryEnvelope`; an unsigned block is SUPPRESSED
|
|
11
|
+
* on any live verify, never rendered as a certification. So a preview literally cannot pass
|
|
12
|
+
* as a delivered cert — the "never delivery" property is structural, not a policy.
|
|
13
|
+
* - The container marker (`data-certrev-preview="1"`) + the default-on visible label make it
|
|
14
|
+
* unmistakable to humans AND tooling; the required `data-certrev-preview-expires` bounds its
|
|
15
|
+
* life so a stale snapshot can be refused downstream.
|
|
16
|
+
* - It is NOT revocable — the kill-switch cannot reach a static HTML snapshot — so it must stay
|
|
17
|
+
* demo / draft-only. That is the CALLER's duty; this renderer bakes the markers that let the
|
|
18
|
+
* surrounding systems enforce it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { escapeAttribute, escapeHtml } from './escape.js'
|
|
22
|
+
import { type RenderCertBlockInput, renderCertBlock } from './render-cert-block.js'
|
|
23
|
+
|
|
24
|
+
export interface PreviewBlockOptions {
|
|
25
|
+
/**
|
|
26
|
+
* REQUIRED — when this preview expires (ISO 8601). A preview MUST be time-bounded so a stale
|
|
27
|
+
* snapshot can be refused downstream; there is deliberately NO default (an unbounded preview
|
|
28
|
+
* is the drift-into-shadow-delivery risk this option exists to prevent).
|
|
29
|
+
*/
|
|
30
|
+
readonly expiresAt: string
|
|
31
|
+
/**
|
|
32
|
+
* Show the visible caption. Default `true` — the SAFE default. Opt out (`false`) ONLY for a
|
|
33
|
+
* true pixel-parity look-demo; the machine marker + expiry + non-delivery guarantee remain.
|
|
34
|
+
*/
|
|
35
|
+
readonly showLabel?: boolean
|
|
36
|
+
/** Override the caption text (default below). */
|
|
37
|
+
readonly labelText?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_LABEL = 'Preview — not a verified certification'
|
|
41
|
+
|
|
42
|
+
/** Extract a preview block's baked expiry (ISO), or null if the HTML isn't a preview block. */
|
|
43
|
+
export function previewExpiry(html: string): string | null {
|
|
44
|
+
const m = html.match(/data-certrev-preview-expires="([^"]*)"/)
|
|
45
|
+
return m ? m[1] : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render a static, clearly-labeled, time-bounded preview of the cert block. See the module doc
|
|
50
|
+
* for the safety model. `expiresAt` is required; throws without it.
|
|
51
|
+
*/
|
|
52
|
+
export function renderPreviewBlock(input: RenderCertBlockInput, options: PreviewBlockOptions): string {
|
|
53
|
+
const expires = options.expiresAt.trim()
|
|
54
|
+
if (!expires) {
|
|
55
|
+
throw new Error('renderPreviewBlock: `expiresAt` is required — a preview must be time-bounded.')
|
|
56
|
+
}
|
|
57
|
+
// The bound must be REAL — an unparseable expiry is a toothless bound (downstream can't refuse a
|
|
58
|
+
// stale preview it can't date). Reject anything Date can't parse, so `previewExpiry()` consumers
|
|
59
|
+
// always get a usable timestamp.
|
|
60
|
+
if (Number.isNaN(new Date(expires).getTime())) {
|
|
61
|
+
throw new Error(`renderPreviewBlock: \`expiresAt\` must be a parseable date (got "${expires}").`)
|
|
62
|
+
}
|
|
63
|
+
const showLabel = options.showLabel !== false
|
|
64
|
+
const label = showLabel
|
|
65
|
+
? `<div data-certrev-preview-label style="font:500 11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.08em;text-transform:uppercase;color:#9a9a9a;margin-bottom:8px;">${escapeHtml(options.labelText ?? DEFAULT_LABEL)}</div>`
|
|
66
|
+
: ''
|
|
67
|
+
return (
|
|
68
|
+
`<!-- CertREV PREVIEW — static, non-revocable, NOT a verified certification. Expires ${escapeHtml(expires)}. Never use as delivery. -->` +
|
|
69
|
+
`<div data-certrev-preview="1" data-certrev-preview-expires="${escapeAttribute(expires)}">` +
|
|
70
|
+
label +
|
|
71
|
+
renderCertBlock(input) +
|
|
72
|
+
`</div>`
|
|
73
|
+
)
|
|
74
|
+
}
|