@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.
- package/dist/builder/index.d.ts +54 -52
- package/dist/builder/index.d.ts.map +1 -1
- package/dist/builder/index.js +64 -47
- package/dist/builder/index.js.map +1 -1
- package/dist/components/CertBadge.d.ts +18 -3
- package/dist/components/CertBadge.d.ts.map +1 -1
- package/dist/components/CertBadge.js +8 -2
- package/dist/components/CertBadge.js.map +1 -1
- package/dist/components/CertReview.d.ts +7 -0
- package/dist/components/CertReview.d.ts.map +1 -1
- package/dist/components/CertReview.js +1 -1
- package/dist/components/CertReview.js.map +1 -1
- package/dist/components/format.d.ts +14 -2
- package/dist/components/format.d.ts.map +1 -1
- package/dist/components/format.js +18 -5
- package/dist/components/format.js.map +1 -1
- package/dist/components/render-cert-block.d.ts +89 -0
- package/dist/components/render-cert-block.d.ts.map +1 -0
- package/dist/components/render-cert-block.js +282 -0
- package/dist/components/render-cert-block.js.map +1 -0
- package/dist/components/render-def.d.ts +178 -0
- package/dist/components/render-def.d.ts.map +1 -0
- package/dist/components/render-def.js +204 -0
- package/dist/components/render-def.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/webcomponent/render-badge-html.d.ts +8 -0
- package/dist/webcomponent/render-badge-html.d.ts.map +1 -1
- package/dist/webcomponent/render-badge-html.js +31 -4
- package/dist/webcomponent/render-badge-html.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +13 -0
- package/src/__tests__/components.test.tsx +16 -1
- package/src/__tests__/render-cert-block.test.ts +259 -0
- package/src/__tests__/render-def.test.tsx +286 -0
- package/src/__tests__/webcomponent.test.tsx +10 -0
- package/src/builder/__tests__/builder.test.tsx +38 -51
- package/src/builder/index.tsx +86 -64
- package/src/components/CertBadge.tsx +56 -4
- package/src/components/CertReview.tsx +8 -0
- package/src/components/format.ts +22 -5
- package/src/components/render-cert-block.ts +403 -0
- package/src/components/render-def.ts +286 -0
- package/src/index.ts +35 -0
- package/src/webcomponent/render-badge-html.ts +46 -4
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Themeable token vocabulary (mirrors portal src/lib/design-guide/tokens.ts)
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** The font ENUM. Resolves to a FIXED slot stack — never a free-form family. */
|
|
49
|
+
export const FONT_SLOTS = ['sans', 'serif', 'system'] as const
|
|
50
|
+
export type FontSlot = (typeof FONT_SLOTS)[number]
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `fontSlot` → fixed font-stack literal. BYTE-IDENTICAL to the Shopify Liquid
|
|
54
|
+
* engine's stacks (`shopify/extensions/certrev-schema/snippets/certrev-badge.liquid`
|
|
55
|
+
* — the only font families the twins use), so a themed brand's Builder projection
|
|
56
|
+
* paints the same typography as its Shopify face. A raw family string in a
|
|
57
|
+
* `font-family` CSS var is a CSS-injection surface AND breaks the "CertREV hosts no
|
|
58
|
+
* fonts" invariant, so the enum → stack indirection is the security boundary.
|
|
59
|
+
*/
|
|
60
|
+
export const FONT_STACKS: Readonly<Record<FontSlot, string>> = {
|
|
61
|
+
sans: "'DM Sans', system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
62
|
+
serif: "'Playfair Display', Georgia, 'Times New Roman', serif",
|
|
63
|
+
system: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A fully-resolved themeable token set (a brand overrides a subset of these). */
|
|
67
|
+
export interface CertThemeTokens {
|
|
68
|
+
readonly accentColor: string
|
|
69
|
+
readonly surface: string
|
|
70
|
+
readonly cornerRadius: string
|
|
71
|
+
readonly fontSlot: FontSlot
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The badge fields a render-def may SUBTRACTIVELY hide (brand-toggleable, free). */
|
|
75
|
+
export const HIDEABLE_FIELDS = ['reviewerPhoto', 'memo'] as const
|
|
76
|
+
export type BadgeVisibilityFieldId = (typeof HIDEABLE_FIELDS)[number]
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A brand render-def as it reaches the render edge — the RESOLVED projection the
|
|
80
|
+
* portal's `getActiveRenderDef` → cert-chrome/builder-export path carries (mirrors
|
|
81
|
+
* the Shopify `certrev.render_def` metafield `{v, preset, rung, tokens, hidden}`;
|
|
82
|
+
* cert-block needs only the theming inputs). Every field is OPTIONAL: an absent
|
|
83
|
+
* `renderDef`, an absent token, or an invalid token all fall back to the preset
|
|
84
|
+
* default (fail-safe).
|
|
85
|
+
*/
|
|
86
|
+
export interface CertRenderDef {
|
|
87
|
+
/** Partial themeable-token overrides. Absent/invalid keys use the preset default. */
|
|
88
|
+
readonly tokens?: Partial<CertThemeTokens>
|
|
89
|
+
/** SUBTRACTIVE hide-set — the ONLY visibility a brand can express. */
|
|
90
|
+
readonly hidden?: readonly BadgeVisibilityFieldId[]
|
|
91
|
+
/** Monotonic per-brand def version, stamped as `data-certrev-def-version`. */
|
|
92
|
+
readonly version?: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// Per-token validators (strict, CSS-injection-safe — no trim, exact match)
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** 3- or 6-digit hex with a leading `#` — opaque by construction (no alpha). */
|
|
100
|
+
const HEX_OPAQUE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
|
|
101
|
+
|
|
102
|
+
/** A single CSS length: a non-negative number with a `px|rem|em|%` unit, or bare `0`. */
|
|
103
|
+
const RADIUS = /^(?:0|(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|%))$/
|
|
104
|
+
|
|
105
|
+
/** `accentColor` / `surface` — an opaque hex (surface opacity is FORCED for contrast soundness). */
|
|
106
|
+
export function isOpaqueHexColor(value: string): boolean {
|
|
107
|
+
return HEX_OPAQUE.test(value)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** `cornerRadius` — a single unit-bearing CSS length (or bare `0`). */
|
|
111
|
+
export function isCornerRadius(value: string): boolean {
|
|
112
|
+
return RADIUS.test(value)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Is `value` a known font slot? */
|
|
116
|
+
export function isFontSlot(value: string): value is FontSlot {
|
|
117
|
+
return (FONT_SLOTS as readonly string[]).includes(value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// The two always-on compliance strings (byte-verbatim — never re-word here)
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The compensation cue (design guide "line A"). Rendered beside the reviewer on
|
|
126
|
+
* EVERY claim-bearing face. Byte-exact; the tag-stripped visible text of the cue
|
|
127
|
+
* markup must equal this constant. Mirrors portal `COMPENSATED_EXPERT_CUE`.
|
|
128
|
+
*/
|
|
129
|
+
export const COMPENSATED_EXPERT_CUE = 'Compensated expert'
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The FTC scope disclaimer (design guide "line B"). Byte-verbatim; the curly
|
|
133
|
+
* apostrophe (U+2019) in "article’s" is LOAD-BEARING (the MSA §3 references this
|
|
134
|
+
* copy in the present tense). Mirrors portal `FTC_DISCLOSURE_LINE` / `CERT_SCOPE_LINE`.
|
|
135
|
+
*/
|
|
136
|
+
export const CERT_SCOPE_LINE =
|
|
137
|
+
'Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.'
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The delivered display-string EDITION, stamped as `data-certrev-strings-version`
|
|
141
|
+
* so the crawl monitor can assert which verbatim strings a live face should carry.
|
|
142
|
+
* MUST equal the portal's `CERT_DISPLAY_STRINGS_VERSION` (display-strings.ts).
|
|
143
|
+
* Bump in lockstep whenever the cue or scope line changes.
|
|
144
|
+
*/
|
|
145
|
+
export const CERT_STRINGS_VERSION = '2026-07'
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// Resolution
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The validated theme a render-def resolves to. Every field is present ONLY when
|
|
153
|
+
* the def supplied a VALID value for it (an absent/invalid token is dropped so the
|
|
154
|
+
* preset default shows through). `accent` is surfaced for the accent-precedence
|
|
155
|
+
* chain in `resolveDisplay`; `surface`/`cornerRadius`/`fontStack` have no legacy
|
|
156
|
+
* display equivalent and are emitted ONLY on a themed face.
|
|
157
|
+
*/
|
|
158
|
+
export interface ResolvedRenderTheme {
|
|
159
|
+
readonly accent?: string
|
|
160
|
+
readonly surface?: string
|
|
161
|
+
readonly cornerRadius?: string
|
|
162
|
+
readonly fontStack?: string
|
|
163
|
+
readonly version?: number
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Resolve + re-validate a render-def's tokens. Invalid values are DROPPED (fail-safe). */
|
|
167
|
+
export function resolveRenderTheme(def?: CertRenderDef): ResolvedRenderTheme {
|
|
168
|
+
if (!def) return {}
|
|
169
|
+
const t = def.tokens ?? {}
|
|
170
|
+
const accent = t.accentColor && isOpaqueHexColor(t.accentColor) ? t.accentColor : undefined
|
|
171
|
+
const surface = t.surface && isOpaqueHexColor(t.surface) ? t.surface : undefined
|
|
172
|
+
const cornerRadius = t.cornerRadius && isCornerRadius(t.cornerRadius) ? t.cornerRadius : undefined
|
|
173
|
+
const fontStack = t.fontSlot && isFontSlot(t.fontSlot) ? FONT_STACKS[t.fontSlot] : undefined
|
|
174
|
+
const version = typeof def.version === 'number' && Number.isFinite(def.version) ? def.version : undefined
|
|
175
|
+
return { accent, surface, cornerRadius, fontStack, version }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Is a brand-toggleable field hidden by this render-def's subtractive hide-set? */
|
|
179
|
+
export function isFieldHidden(def: CertRenderDef | undefined, id: BadgeVisibilityFieldId): boolean {
|
|
180
|
+
return !!def?.hidden?.includes(id)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Cert-render BLOCK theme — the resolved-token mirror of portal ResolvedRenderDef
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
//
|
|
187
|
+
// `renderCertBlock` (render-cert-block.ts) paints the LOCKED CertREV cert design
|
|
188
|
+
// (banner / sidebar / floating) from an ALREADY-RESOLVED token set — the portal
|
|
189
|
+
// bakes `ResolvedRenderDef` (render-def.ts + tokens.ts) and the projection carries
|
|
190
|
+
// its theme half here. This is a WIDER slot vocabulary than the `CertThemeTokens`
|
|
191
|
+
// above (which the legacy `<CertBadge>` uses): it mirrors the portal's FULL
|
|
192
|
+
// five-member `FontSlot` enum + the pre-computed `accentFg` ink, so the block can
|
|
193
|
+
// reproduce the guide's four-font design (DM Sans body, Playfair memo, JetBrains
|
|
194
|
+
// mono labels, Plus Jakarta wordmark). It is kept DISTINCT from `FONT_SLOTS` /
|
|
195
|
+
// `FONT_STACKS` above so the legacy badge's three-slot contract stays byte-frozen.
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The full portal font-slot enum (mirrors portal `src/lib/design-guide/tokens.ts`
|
|
199
|
+
* `FONT_SLOTS`). `grotesk` ⇐ the guide's wordmark slot; `mono` ⇐ the label slot.
|
|
200
|
+
*/
|
|
201
|
+
export const RENDER_BLOCK_FONT_SLOTS = ['sans', 'serif', 'system', 'grotesk', 'mono'] as const
|
|
202
|
+
export type RenderBlockFontSlot = (typeof RENDER_BLOCK_FONT_SLOTS)[number]
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The `RenderBlockFontSlot` → fixed family stack. BYTE-IDENTICAL to the portal's
|
|
206
|
+
* `FONT_SLOT_STACKS` (the guide's stacks — the cert-render parity anchor across the
|
|
207
|
+
* twins). Resolved family strings, never `var(...)` indirections. A raw family in a
|
|
208
|
+
* `font-family` CSS var is a CSS-injection surface, so the enum → stack indirection
|
|
209
|
+
* stays the security boundary (a caller can only pass an enum member).
|
|
210
|
+
*/
|
|
211
|
+
export const RENDER_BLOCK_FONT_STACKS: Readonly<Record<RenderBlockFontSlot, string>> = {
|
|
212
|
+
sans: "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
213
|
+
serif: "'Playfair Display', Georgia, 'Times New Roman', serif",
|
|
214
|
+
grotesk: "'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif",
|
|
215
|
+
mono: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
216
|
+
system: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Is `value` a known render-block font slot? */
|
|
220
|
+
export function isRenderBlockFontSlot(value: string): value is RenderBlockFontSlot {
|
|
221
|
+
return (RENDER_BLOCK_FONT_SLOTS as readonly string[]).includes(value)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The resolved themeable-token set `renderCertBlock` paints — the render-edge mirror
|
|
226
|
+
* of the portal `ResolvedRenderDef` theme half (`tokens` ⊕ the precomputed
|
|
227
|
+
* `accentFg`). `accentFg` is the ink painted over the accent fill — the portal's
|
|
228
|
+
* guide-faithful Rec.601 auto-contrast pick (`#141414` or `#ffffff`), carried
|
|
229
|
+
* pre-computed so this dumb twin never recomputes it.
|
|
230
|
+
*/
|
|
231
|
+
export interface ResolvedBlockTheme {
|
|
232
|
+
readonly accentColor: string
|
|
233
|
+
readonly surface: string
|
|
234
|
+
readonly cornerRadius: string
|
|
235
|
+
readonly fontSlot: RenderBlockFontSlot
|
|
236
|
+
readonly accentFg: '#141414' | '#ffffff'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The CertREV navy default theme (the un-themed face — matches the locked reference `:root`). */
|
|
240
|
+
export const DEFAULT_BLOCK_THEME: ResolvedBlockTheme = {
|
|
241
|
+
accentColor: '#0a1b3f',
|
|
242
|
+
surface: '#ffffff',
|
|
243
|
+
cornerRadius: '14px',
|
|
244
|
+
fontSlot: 'sans',
|
|
245
|
+
accentFg: '#ffffff',
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Re-validate a (possibly partial / untrusted) block theme at the render edge —
|
|
250
|
+
* defense in depth over the portal's fail-closed bake. Any invalid token is DROPPED
|
|
251
|
+
* to the navy default (never emitted into storefront CSS). Mirrors the
|
|
252
|
+
* `resolveRenderTheme` fail-safe posture.
|
|
253
|
+
*/
|
|
254
|
+
export function resolveBlockTheme(theme?: Partial<ResolvedBlockTheme>): ResolvedBlockTheme {
|
|
255
|
+
const t = theme ?? {}
|
|
256
|
+
return {
|
|
257
|
+
accentColor: t.accentColor && isOpaqueHexColor(t.accentColor) ? t.accentColor : DEFAULT_BLOCK_THEME.accentColor,
|
|
258
|
+
surface: t.surface && isOpaqueHexColor(t.surface) ? t.surface : DEFAULT_BLOCK_THEME.surface,
|
|
259
|
+
cornerRadius: t.cornerRadius && isCornerRadius(t.cornerRadius) ? t.cornerRadius : DEFAULT_BLOCK_THEME.cornerRadius,
|
|
260
|
+
fontSlot: t.fontSlot && isRenderBlockFontSlot(t.fontSlot) ? t.fontSlot : DEFAULT_BLOCK_THEME.fontSlot,
|
|
261
|
+
accentFg: t.accentFg === '#141414' || t.accentFg === '#ffffff' ? t.accentFg : DEFAULT_BLOCK_THEME.accentFg,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The locked verification ATTRIBUTION phrase, rendered grouped with the dated
|
|
267
|
+
* verification (`<name>, <credential>` → this → `on <date>`). Byte-exact; mirrors the
|
|
268
|
+
* fixed clause inside portal `buildBylineCredentialed` ("Credential verified by
|
|
269
|
+
* CertREV on …"). Never re-word without contract + legal re-review.
|
|
270
|
+
*/
|
|
271
|
+
export const CREDENTIAL_VERIFIED_ATTRIBUTION = 'Credential verified by CertREV'
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The extra `--certrev-*` custom-property declarations a THEMED face emits, in the
|
|
275
|
+
* engine's order (surface → radius → font; accent is emitted separately so the
|
|
276
|
+
* no-def output stays byte-identical). Returns `[]` when no theme is present, so
|
|
277
|
+
* the un-themed badge emits ZERO extra vars. The data-bearing values are already
|
|
278
|
+
* strict-validated; the font is a controlled literal stack.
|
|
279
|
+
*/
|
|
280
|
+
export function themeCssVarDeclarations(theme: ResolvedRenderTheme): ReadonlyArray<readonly [string, string]> {
|
|
281
|
+
const out: Array<readonly [string, string]> = []
|
|
282
|
+
if (theme.surface) out.push(['--certrev-surface', theme.surface])
|
|
283
|
+
if (theme.cornerRadius) out.push(['--certrev-radius', theme.cornerRadius])
|
|
284
|
+
if (theme.fontStack) out.push(['--certrev-font', theme.fontStack])
|
|
285
|
+
return out
|
|
286
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,41 @@ export {
|
|
|
36
36
|
type ResolvedDisplay,
|
|
37
37
|
resolveDisplay,
|
|
38
38
|
} from './components/format.js'
|
|
39
|
+
// ── Cert-render BLOCK — the LOCKED 3-mode cert design as an SSR-safe HTML string ──
|
|
40
|
+
export {
|
|
41
|
+
type CertBlockFacts,
|
|
42
|
+
type CertBlockMode,
|
|
43
|
+
type RenderCertBlockInput,
|
|
44
|
+
renderCertBlock,
|
|
45
|
+
} from './components/render-cert-block.js'
|
|
46
|
+
// ── Design-guide render-def support (WS6: themeable tokens + subtractive visibility) ──
|
|
47
|
+
export {
|
|
48
|
+
type BadgeVisibilityFieldId,
|
|
49
|
+
CERT_SCOPE_LINE,
|
|
50
|
+
CERT_STRINGS_VERSION,
|
|
51
|
+
type CertRenderDef,
|
|
52
|
+
type CertThemeTokens,
|
|
53
|
+
COMPENSATED_EXPERT_CUE,
|
|
54
|
+
CREDENTIAL_VERIFIED_ATTRIBUTION,
|
|
55
|
+
DEFAULT_BLOCK_THEME,
|
|
56
|
+
FONT_SLOTS,
|
|
57
|
+
FONT_STACKS,
|
|
58
|
+
type FontSlot,
|
|
59
|
+
HIDEABLE_FIELDS,
|
|
60
|
+
isCornerRadius,
|
|
61
|
+
isFieldHidden,
|
|
62
|
+
isFontSlot,
|
|
63
|
+
isOpaqueHexColor,
|
|
64
|
+
isRenderBlockFontSlot,
|
|
65
|
+
RENDER_BLOCK_FONT_SLOTS,
|
|
66
|
+
RENDER_BLOCK_FONT_STACKS,
|
|
67
|
+
type RenderBlockFontSlot,
|
|
68
|
+
type ResolvedBlockTheme,
|
|
69
|
+
type ResolvedRenderTheme,
|
|
70
|
+
resolveBlockTheme,
|
|
71
|
+
resolveRenderTheme,
|
|
72
|
+
themeCssVarDeclarations,
|
|
73
|
+
} from './components/render-def.js'
|
|
39
74
|
// ── Contract surface (types + kernel) — re-export the binding point ────────────────
|
|
40
75
|
// NOTE: the test/dev fixtures (makeSignedEnvelope, makeMockPayload, FIXTURE_KID) are
|
|
41
76
|
// Node-only (they sign with node:crypto) and ship from the './fixtures' subpath, NOT here.
|
|
@@ -14,11 +14,25 @@
|
|
|
14
14
|
|
|
15
15
|
import { escapeAttribute, escapeHtml, safeCssColor, safeHttpUrl } from '../components/escape.js'
|
|
16
16
|
import { credentialSuffix, formatDate, resolveDisplay } from '../components/format.js'
|
|
17
|
+
import {
|
|
18
|
+
CERT_SCOPE_LINE,
|
|
19
|
+
CERT_STRINGS_VERSION,
|
|
20
|
+
type CertRenderDef,
|
|
21
|
+
resolveRenderTheme,
|
|
22
|
+
themeCssVarDeclarations,
|
|
23
|
+
} from '../components/render-def.js'
|
|
17
24
|
import type { CertPayload } from '../contract/kernel.js'
|
|
18
25
|
|
|
19
26
|
export interface RenderBadgeOptions {
|
|
20
27
|
readonly accentColor?: string
|
|
21
28
|
readonly badgeStyle?: 'full' | 'compact'
|
|
29
|
+
/**
|
|
30
|
+
* The brand render-def (WS6). ABSENT → the badge renders the CertREV preset
|
|
31
|
+
* default (theme output byte-identical to un-themed). PRESENT → validated
|
|
32
|
+
* themeable tokens are emitted as `--certrev-*` CSS vars + subtractive
|
|
33
|
+
* visibility applies. Mirrors `<CertBadge>`'s `renderDef` prop exactly.
|
|
34
|
+
*/
|
|
35
|
+
readonly renderDef?: CertRenderDef
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
const C = 'certrev-badge'
|
|
@@ -35,7 +49,8 @@ function attr(name: string, value: string | null | undefined): string {
|
|
|
35
49
|
*/
|
|
36
50
|
export function renderBadgeHtml(payload: CertPayload, opts: RenderBadgeOptions = {}): string {
|
|
37
51
|
const content = payload.content
|
|
38
|
-
const display = resolveDisplay(content.display, opts.accentColor)
|
|
52
|
+
const display = resolveDisplay(content.display, opts.accentColor, opts.renderDef)
|
|
53
|
+
const theme = resolveRenderTheme(opts.renderDef)
|
|
39
54
|
const style = opts.badgeStyle ?? display.badgeStyle
|
|
40
55
|
const accent = safeCssColor(display.accentColor) ?? '#0f766e'
|
|
41
56
|
const verifyUrl = safeHttpUrl(content.verifyUrl)
|
|
@@ -91,16 +106,43 @@ export function renderBadgeHtml(payload: CertPayload, opts: RenderBadgeOptions =
|
|
|
91
106
|
if (dates.length) body += `<dl class="${C}__dates">${dates.join('')}</dl>`
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
// Design guide §3: the compensation cue + FTC scope line are LOCKED on every
|
|
110
|
+
// claim-bearing face (cert-block always carries the reviewer byline, so both
|
|
111
|
+
// render on the full AND compact styles). The compliance BASELINE — present
|
|
112
|
+
// regardless of a render-def, matching the Shopify/WordPress engine fix. The
|
|
113
|
+
// cue's "expert" links to the reviewer profile; the whole cue tag-strips to the
|
|
114
|
+
// byte-verbatim COMPENSATED_EXPERT_CUE. The scope line is byte-verbatim
|
|
115
|
+
// CERT_SCOPE_LINE (its curly apostrophe U+2019 survives escapeHtml untouched).
|
|
116
|
+
const cueInner = profileUrl
|
|
117
|
+
? `Compensated <a class="${C}__cue-link" href="${escapeAttribute(profileUrl)}" rel="noopener">expert</a>`
|
|
118
|
+
: 'Compensated expert'
|
|
119
|
+
const disclosure =
|
|
120
|
+
`<div class="${C}__disclosure">` +
|
|
121
|
+
`<span class="${C}__compensated-cue">${cueInner}</span>` +
|
|
122
|
+
`<p class="${C}__ftc" data-ftc-disclosure><span data-ftc-line>${escapeHtml(CERT_SCOPE_LINE)}</span></p>` +
|
|
123
|
+
`</div>`
|
|
124
|
+
|
|
94
125
|
const verify = verifyUrl
|
|
95
126
|
? `<a class="${C}__verify" href="${escapeAttribute(verifyUrl)}" rel="noopener" aria-label="Verify this certification on CertREV">Verify on CertREV</a>`
|
|
96
127
|
: ''
|
|
97
128
|
|
|
98
129
|
const ariaLabel = `Content reviewed by ${content.expert.displayName}${suffix ? `, ${suffix}` : ''}`
|
|
99
|
-
|
|
130
|
+
// Accent LEADS so the un-themed output is byte-identical to the pre-WS6 single-var
|
|
131
|
+
// form. A THEMED brand appends surface/radius/font: the data-bearing tokens ride
|
|
132
|
+
// escaped (a no-op for validated hex/length — defense in depth), the font is a
|
|
133
|
+
// controlled literal stack emitted raw. The whole string is NOT re-escaped
|
|
134
|
+
// (the font stack's single quotes are valid inside a double-quoted attribute and
|
|
135
|
+
// re-escaping would corrupt them).
|
|
136
|
+
let rootStyle = `--certrev-accent:${escapeAttribute(accent)};border-inline-start-color:${escapeAttribute(accent)}`
|
|
137
|
+
for (const [name, value] of themeCssVarDeclarations(theme)) {
|
|
138
|
+
rootStyle += name === '--certrev-font' ? `;${name}:${value}` : `;${name}:${escapeAttribute(value)}`
|
|
139
|
+
}
|
|
100
140
|
|
|
101
141
|
return (
|
|
102
|
-
`<section class="${C} ${C}--${escapeAttribute(style)}" style="${
|
|
142
|
+
`<section class="${C} ${C}--${escapeAttribute(style)}" style="${rootStyle}"` +
|
|
103
143
|
attr('data-certrev-cert-id', payload.certId) +
|
|
104
|
-
`
|
|
144
|
+
` data-certrev-strings-version="${CERT_STRINGS_VERSION}"` +
|
|
145
|
+
attr('data-certrev-def-version', theme.version != null ? String(theme.version) : null) +
|
|
146
|
+
` aria-label="${escapeAttribute(ariaLabel)}">${header}${body}${disclosure}${verify}</section>`
|
|
105
147
|
)
|
|
106
148
|
}
|