@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
|
@@ -1,81 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SSR-render tests for the Builder.io adapter (`@certrev/cert-block/builder`),
|
|
3
|
-
* through `
|
|
4
|
-
* produce crawlable HTML on the edge. These prove the adapter's load-bearing contract:
|
|
2
|
+
* SSR-render tests for the Builder.io adapter (`@certrev/cert-block/builder`), the panel-validated
|
|
3
|
+
* "ambient-URL anchor" model — exercised through `renderToStaticMarkup` (the edge SSR path).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* •
|
|
8
|
-
*
|
|
9
|
-
* •
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* Contract proved here:
|
|
6
|
+
* • the anchor renders the CURRENT page's verified badge — and badge ONLY (JSON-LD is emitted
|
|
7
|
+
* server-side, decoupled from the visual slot);
|
|
8
|
+
* • EVERY non-render path (suppress verdict, no credential for the page) renders nothing in
|
|
9
|
+
* production (fail-closed);
|
|
10
|
+
* • in the Builder editor (`isEditing`) with no live credential, a truthful placeholder shows —
|
|
11
|
+
* never a fake badge, and never any JSON-LD;
|
|
12
|
+
* • the registration is ZERO-input: the editor cannot choose or forge which credential renders.
|
|
14
13
|
*/
|
|
15
14
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
16
15
|
import { describe, expect, it } from 'vitest'
|
|
17
16
|
import { makeMockPayload } from '../../contract/fixtures.js'
|
|
18
17
|
import type { CertVerdict } from '../../contract/kernel.js'
|
|
19
|
-
import {
|
|
18
|
+
import { CertRevAnchor, CertRevProvider, certRevAnchorComponent, type CurrentCredential } from '../index.js'
|
|
20
19
|
|
|
21
20
|
const renderVerdict: CertVerdict = { decision: 'render', payload: makeMockPayload() }
|
|
22
21
|
const suppressVerdict: CertVerdict = { decision: 'suppress', reason: 'revoked' }
|
|
23
22
|
|
|
24
|
-
function
|
|
23
|
+
function render(current: CurrentCredential | null, isEditing = false): string {
|
|
25
24
|
return renderToStaticMarkup(
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
</
|
|
25
|
+
<CertRevProvider current={current} isEditing={isEditing}>
|
|
26
|
+
<CertRevAnchor />
|
|
27
|
+
</CertRevProvider>,
|
|
29
28
|
)
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
describe('Builder adapter — registration', () => {
|
|
33
|
-
it('registers
|
|
34
|
-
expect(
|
|
35
|
-
expect(
|
|
36
|
-
|
|
37
|
-
expect(input).toBeDefined()
|
|
38
|
-
expect(input?.required).toBe(true)
|
|
39
|
-
expect(input?.type).toBe('string')
|
|
31
|
+
describe('Builder adapter — registration (zero-input anchor)', () => {
|
|
32
|
+
it('registers "CertREV Review" with NO inputs (editor places it; system resolves the credential)', () => {
|
|
33
|
+
expect(certRevAnchorComponent.name).toBe('CertREV Review')
|
|
34
|
+
expect(certRevAnchorComponent.component).toBe(CertRevAnchor)
|
|
35
|
+
expect(certRevAnchorComponent.inputs).toEqual([])
|
|
40
36
|
})
|
|
41
37
|
})
|
|
42
38
|
|
|
43
|
-
describe('Builder adapter —
|
|
44
|
-
it('renders the verified badge
|
|
45
|
-
const html =
|
|
39
|
+
describe('Builder adapter — CertRevAnchor render', () => {
|
|
40
|
+
it('renders the current page’s verified badge — and ONLY the badge (no JSON-LD; that is server-side)', () => {
|
|
41
|
+
const html = render({ verdict: renderVerdict, pageUrl: 'https://brand.example.com/a' })
|
|
46
42
|
expect(html).toContain('certrev-badge')
|
|
47
43
|
expect(html).toContain('Dr. Jane Doe')
|
|
48
|
-
expect(html).toContain('
|
|
44
|
+
expect(html).not.toContain('application/ld+json') // JSON-LD decoupled to the article template
|
|
49
45
|
})
|
|
50
46
|
|
|
51
|
-
it('FAIL-CLOSED: a suppress
|
|
52
|
-
expect(
|
|
47
|
+
it('FAIL-CLOSED: a suppress verdict renders nothing', () => {
|
|
48
|
+
expect(render({ verdict: suppressVerdict })).toBe('')
|
|
53
49
|
})
|
|
54
50
|
|
|
55
|
-
it('FAIL-CLOSED:
|
|
56
|
-
expect(
|
|
51
|
+
it('FAIL-CLOSED: no credential for this page renders nothing in production', () => {
|
|
52
|
+
expect(render(null, false)).toBe('')
|
|
57
53
|
})
|
|
58
54
|
|
|
59
|
-
it('
|
|
60
|
-
|
|
55
|
+
it('EDITOR: no live credential + isEditing renders a truthful placeholder (no badge, no JSON-LD)', () => {
|
|
56
|
+
const html = render(null, true)
|
|
57
|
+
expect(html).toContain('data-certrev-editor-placeholder')
|
|
58
|
+
expect(html).toContain('resolves at publish')
|
|
59
|
+
expect(html).not.toContain('certrev-badge')
|
|
60
|
+
expect(html).not.toContain('application/ld+json')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
it('
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
const html = renderToStaticMarkup(
|
|
69
|
-
<CertVerifyProvider value={placements}>
|
|
70
|
-
<div id="valid-block">
|
|
71
|
-
<CertReviewBlock placementId="p-valid" />
|
|
72
|
-
</div>
|
|
73
|
-
<div id="revoked-block">
|
|
74
|
-
<CertReviewBlock placementId="p-revoked" />
|
|
75
|
-
</div>
|
|
76
|
-
</CertVerifyProvider>,
|
|
77
|
-
)
|
|
78
|
-
expect(html).toContain('certrev-badge') // valid rendered
|
|
79
|
-
expect(html).toContain('<div id="revoked-block"></div>') // revoked empty — fail-closed through the CMS
|
|
63
|
+
it('EDITOR: a live verified credential renders the REAL badge even in editing mode (true draft-verify)', () => {
|
|
64
|
+
const html = render({ verdict: renderVerdict }, true)
|
|
65
|
+
expect(html).toContain('certrev-badge')
|
|
66
|
+
expect(html).not.toContain('data-certrev-editor-placeholder')
|
|
80
67
|
})
|
|
81
68
|
})
|
package/src/builder/index.tsx
CHANGED
|
@@ -1,100 +1,122 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @certrev/cert-block/builder —
|
|
2
|
+
* @certrev/cert-block/builder — Builder.io adapter, "ambient-URL anchor" model.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Validated by an independent architecture panel (Codex gpt-5.5 + Gemini, blind): a visual
|
|
5
|
+
* page builder must NOT choose WHICH credential renders — that invites cross-article misuse and
|
|
6
|
+
* re-opens the wrong-subject surface. Instead:
|
|
7
|
+
* • the CMS controls LAYOUT — an editor drags a ZERO-INPUT <CertRevAnchor/> to position the
|
|
8
|
+
* badge on the page;
|
|
9
|
+
* • the SYSTEM controls TRUTH — the headless loader resolves THIS page's credential from its
|
|
10
|
+
* own URL (Track-1 delivery), crypto-verifies it (fail-closed VerdictKernel), and supplies
|
|
11
|
+
* the verdict via context.
|
|
12
|
+
* The anchor renders the current page's verified badge or NOTHING. It cannot be pointed at
|
|
13
|
+
* another article, and there is no `placementId` input to forge or mis-select.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* tampered / expired / wrong-subject placement that an editor drops into Builder renders
|
|
14
|
-
* nothing, even though the block is present in the published content.
|
|
15
|
+
* JSON-LD is DECOUPLED (panel finding): the anchor renders the BADGE only (`omitJsonLd`). The
|
|
16
|
+
* article template emits the schema.org JSON-LD server-side from the same verified verdict, so a
|
|
17
|
+
* page builder can't duplicate, move, or omit structured data. (See the route example.)
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* EDGE-CACHE DISCIPLINE (panel finding): the consumer's loader MUST verify per request as a
|
|
20
|
+
* dynamic edge subrequest and must NOT long-cache the rendered badge HTML — a revoked/expired
|
|
21
|
+
* credential sitting in a stale edge cache would defeat fail-closed. Bound any positive-verdict
|
|
22
|
+
* cache by min(expiry, revocation-TTL, content-version).
|
|
23
|
+
*
|
|
24
|
+
* cert-block carries no Builder dependency: the registration shape is declared structurally, and
|
|
25
|
+
* `isEditing` is a passed-in flag (the consumer supplies Builder's `isPreviewing()`).
|
|
21
26
|
*/
|
|
22
27
|
import { createContext, type ReactNode, useContext } from 'react'
|
|
23
28
|
import { CertReview } from '../components/CertReview.js'
|
|
24
29
|
import type { CertVerdict } from '../contract/kernel.js'
|
|
25
30
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
export interface VerifiedPlacement {
|
|
31
|
+
/** The current page's credential, resolved + verified by the loader (Track-1 delivery). */
|
|
32
|
+
export interface CurrentCredential {
|
|
29
33
|
readonly verdict: CertVerdict
|
|
34
|
+
/** Canonical page URL for JSON-LD @id alignment (used by the server-side JSON-LD, not the anchor). */
|
|
30
35
|
readonly pageUrl?: string
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
interface CertRevContextValue {
|
|
39
|
+
readonly current: CurrentCredential | null
|
|
40
|
+
readonly isEditing: boolean
|
|
41
|
+
}
|
|
36
42
|
|
|
37
|
-
const
|
|
43
|
+
const CertRevContext = createContext<CertRevContextValue>({ current: null, isEditing: false })
|
|
38
44
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Supplies THIS page's loader-verified credential to the anchor. `current` is the verdict for
|
|
47
|
+
* the current page (or null if the page has no delivered credential). `isEditing` is the
|
|
48
|
+
* consumer's Builder `isPreviewing()` result — it ONLY toggles a design-time placeholder and
|
|
49
|
+
* NEVER affects the published render.
|
|
50
|
+
*/
|
|
51
|
+
export function CertRevProvider({
|
|
52
|
+
current,
|
|
53
|
+
isEditing = false,
|
|
43
54
|
children,
|
|
44
55
|
}: {
|
|
45
|
-
readonly
|
|
56
|
+
readonly current: CurrentCredential | null
|
|
57
|
+
readonly isEditing?: boolean
|
|
46
58
|
readonly children: ReactNode
|
|
47
59
|
}) {
|
|
48
|
-
return <
|
|
60
|
+
return <CertRevContext.Provider value={{ current, isEditing }}>{children}</CertRevContext.Provider>
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Design-time placeholder shown ONLY in the Builder editor when the current page has no live
|
|
65
|
+
* verified credential. Truthful (never a fake badge), emits NO JSON-LD, and never renders in
|
|
66
|
+
* production — so it cannot leak to or be crawled on the live site.
|
|
67
|
+
*/
|
|
68
|
+
export function CertRevEditorPlaceholder() {
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
data-certrev-editor-placeholder=""
|
|
72
|
+
style={{
|
|
73
|
+
border: '1px dashed #e6007e',
|
|
74
|
+
borderRadius: 8,
|
|
75
|
+
padding: '10px 14px',
|
|
76
|
+
font: '13px system-ui',
|
|
77
|
+
color: '#6b7280',
|
|
78
|
+
background: '#fff5fa',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
CertREV Review — <b>resolves at publish</b> from issuer verification. The expert badge appears here once this
|
|
82
|
+
page’s article is certified, and only while the credential is valid.
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
88
|
+
* The Builder block. ZERO inputs — a layout anchor only. Renders the current page's VERIFIED
|
|
89
|
+
* badge (badge only; JSON-LD is emitted server-side), the editor placeholder in design mode, or
|
|
90
|
+
* NOTHING in production (fail-closed). The credential is whatever Track-1 delivered + verified for
|
|
91
|
+
* THIS page; the editor can neither choose nor forge it.
|
|
61
92
|
*/
|
|
62
|
-
export function
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
export function CertRevAnchor() {
|
|
94
|
+
const { current, isEditing } = useContext(CertRevContext)
|
|
95
|
+
if (current && current.verdict.decision === 'render') {
|
|
96
|
+
return <CertReview verdict={current.verdict} pageUrl={current.pageUrl} omitJsonLd />
|
|
97
|
+
}
|
|
98
|
+
if (isEditing) return <CertRevEditorPlaceholder />
|
|
99
|
+
return null
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
/**
|
|
70
|
-
* Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` —
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>` call site.
|
|
103
|
+
* Structural shape of `@builder.io/sdk-react`'s `RegisteredComponent` — the subset cert-block
|
|
104
|
+
* populates. Declared locally so cert-block needs no Builder build/runtime dependency; assignable
|
|
105
|
+
* to Builder's `RegisteredComponent` at the consumer's `<Content customComponents={[...]}>`.
|
|
74
106
|
*/
|
|
75
|
-
export interface
|
|
76
|
-
component: typeof
|
|
107
|
+
export interface CertRevBuilderRegistration {
|
|
108
|
+
component: typeof CertRevAnchor
|
|
77
109
|
name: string
|
|
78
|
-
inputs:
|
|
110
|
+
inputs: never[]
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
/**
|
|
82
|
-
* The registration
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* Builder Write API (no visual-editor registration required for rendering).
|
|
114
|
+
* The registration to pass to Builder's `<Content customComponents={[...]}>`. ZERO inputs by
|
|
115
|
+
* design — the editor places it (layout); the system resolves the credential from the page URL
|
|
116
|
+
* (truth). Matched by NAME at render, so Write-API content works without visual-editor setup.
|
|
86
117
|
*/
|
|
87
|
-
export const
|
|
88
|
-
component:
|
|
118
|
+
export const certRevAnchorComponent: CertRevBuilderRegistration = {
|
|
119
|
+
component: CertRevAnchor,
|
|
89
120
|
name: 'CertREV Review',
|
|
90
|
-
inputs: [
|
|
91
|
-
{
|
|
92
|
-
name: 'placementId',
|
|
93
|
-
type: 'string',
|
|
94
|
-
required: true,
|
|
95
|
-
helperText:
|
|
96
|
-
'CertREV placement id (which certified article this badge attests). The headless ' +
|
|
97
|
-
'loader verifies that placement’s signed envelope; only a render-verdict shows a badge.',
|
|
98
|
-
},
|
|
99
|
-
],
|
|
121
|
+
inputs: [],
|
|
100
122
|
}
|
|
@@ -11,23 +11,44 @@
|
|
|
11
11
|
* an inline custom property (no color-only meaning — the credential text carries it), an
|
|
12
12
|
* expert photo with meaningful alt text, and a verify link with a descriptive label.
|
|
13
13
|
*
|
|
14
|
-
* `badgeStyle: 'compact'` collapses to a single line (name + credentials
|
|
15
|
-
* `'full'` shows the photo, certified/updated dates, and the memo (per display flags).
|
|
14
|
+
* `badgeStyle: 'compact'` collapses the reviewer block to a single line (name + credentials);
|
|
15
|
+
* `'full'` shows the photo, certified/updated dates, and the memo (per display flags). BOTH
|
|
16
|
+
* styles carry the compensation cue + FTC scope line (the always-on compliance disclosures).
|
|
17
|
+
*
|
|
18
|
+
* THEMING (WS6): presentation defaults come from `payload.content.display`; an optional
|
|
19
|
+
* `renderDef` prop layers a brand's validated themeable tokens (emitted as `--certrev-*`
|
|
20
|
+
* CSS custom properties) and its subtractive field-visibility on top. No `renderDef` → the
|
|
21
|
+
* CertREV preset default (theme output byte-identical to un-themed).
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
24
|
import type { CertContent, CertPayload } from '../contract/kernel.js'
|
|
19
25
|
import { safeHttpUrl } from './escape.js'
|
|
20
26
|
import { credentialSuffix, expertNameWithCredentials, formatDate, resolveDisplay } from './format.js'
|
|
27
|
+
import {
|
|
28
|
+
CERT_SCOPE_LINE,
|
|
29
|
+
CERT_STRINGS_VERSION,
|
|
30
|
+
type CertRenderDef,
|
|
31
|
+
resolveRenderTheme,
|
|
32
|
+
themeCssVarDeclarations,
|
|
33
|
+
} from './render-def.js'
|
|
21
34
|
|
|
22
35
|
export interface CertBadgeProps {
|
|
23
36
|
/** The cryptographically-verified payload (from `getVerifiedEnvelope`). */
|
|
24
37
|
readonly payload: CertPayload
|
|
25
|
-
/** Optional accent override (else `display.accentColor`, else CertREV default). */
|
|
38
|
+
/** Optional accent override (else render-def, else `display.accentColor`, else CertREV default). */
|
|
26
39
|
readonly accentColor?: string
|
|
27
40
|
/** Extra class names appended to the root element's class list. */
|
|
28
41
|
readonly className?: string
|
|
29
42
|
/** Override the badge style independent of the signed display config. */
|
|
30
43
|
readonly badgeStyle?: 'full' | 'compact'
|
|
44
|
+
/**
|
|
45
|
+
* The brand render-def (WS6). ABSENT → the badge renders the CertREV preset
|
|
46
|
+
* default (fail-safe; theme output byte-identical to un-themed). PRESENT → its
|
|
47
|
+
* validated themeable tokens are emitted as `--certrev-*` CSS custom properties
|
|
48
|
+
* and its subtractive hide-set removes brand-toggleable fields. Invalid tokens
|
|
49
|
+
* are dropped (defense-in-depth over the write-path gate).
|
|
50
|
+
*/
|
|
51
|
+
readonly renderDef?: CertRenderDef
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
const ROOT_CLASS = 'certrev-badge'
|
|
@@ -70,7 +91,8 @@ function ExpertPhoto({ content, show }: { content: CertContent; show: boolean })
|
|
|
70
91
|
export function CertBadge(props: CertBadgeProps) {
|
|
71
92
|
const { payload } = props
|
|
72
93
|
const content = payload.content
|
|
73
|
-
const display = resolveDisplay(content.display, props.accentColor)
|
|
94
|
+
const display = resolveDisplay(content.display, props.accentColor, props.renderDef)
|
|
95
|
+
const theme = resolveRenderTheme(props.renderDef)
|
|
74
96
|
const style = props.badgeStyle ?? display.badgeStyle
|
|
75
97
|
const verifyUrl = safeHttpUrl(content.verifyUrl)
|
|
76
98
|
const profileUrl = safeHttpUrl(content.expert.profileUrl)
|
|
@@ -82,10 +104,13 @@ export function CertBadge(props: CertBadgeProps) {
|
|
|
82
104
|
const rootClass = `${ROOT_CLASS} ${ROOT_CLASS}--${style}${props.className ? ` ${props.className}` : ''}`
|
|
83
105
|
// Accent is exposed as a custom property so brand themes can read it, and applied
|
|
84
106
|
// directly to the accent border so the component is self-styling without a stylesheet.
|
|
107
|
+
// A THEMED brand additionally emits the surface/radius/font vars; with no render-def
|
|
108
|
+
// `themeCssVarDeclarations` is empty, so the un-themed style is byte-identical.
|
|
85
109
|
const rootStyle: Record<string, string> = {
|
|
86
110
|
['--certrev-accent']: accent,
|
|
87
111
|
borderInlineStartColor: accent,
|
|
88
112
|
}
|
|
113
|
+
for (const [name, value] of themeCssVarDeclarations(theme)) rootStyle[name] = value
|
|
89
114
|
|
|
90
115
|
const expertName = (
|
|
91
116
|
<span className={`${ROOT_CLASS}__expert-name`}>
|
|
@@ -99,6 +124,8 @@ export function CertBadge(props: CertBadgeProps) {
|
|
|
99
124
|
className={rootClass}
|
|
100
125
|
style={rootStyle}
|
|
101
126
|
data-certrev-cert-id={payload.certId}
|
|
127
|
+
data-certrev-strings-version={CERT_STRINGS_VERSION}
|
|
128
|
+
data-certrev-def-version={theme.version}
|
|
102
129
|
aria-label={`Content reviewed by ${expertNameWithCredentials(content)}`}
|
|
103
130
|
>
|
|
104
131
|
<div className={`${ROOT_CLASS}__header`}>
|
|
@@ -149,6 +176,31 @@ export function CertBadge(props: CertBadgeProps) {
|
|
|
149
176
|
</>
|
|
150
177
|
) : null}
|
|
151
178
|
|
|
179
|
+
{/* Design guide §3: the compensation cue + FTC scope line are LOCKED on every
|
|
180
|
+
claim-bearing face (named / credentialed / memo) — with or without a memo —
|
|
181
|
+
so a brand can never show an endorsement impression without its disclosures.
|
|
182
|
+
cert-block only ever renders a claim face (it always carries the reviewer
|
|
183
|
+
byline), so both lines render on both the full AND compact styles. This is
|
|
184
|
+
the compliance BASELINE — it lands regardless of a render-def, matching the
|
|
185
|
+
Shopify/WordPress engine fix. The cue's "expert" links to the reviewer's
|
|
186
|
+
CertREV profile (the whole cue tag-strips to the byte-verbatim
|
|
187
|
+
COMPENSATED_EXPERT_CUE). The scope line is byte-verbatim CERT_SCOPE_LINE. */}
|
|
188
|
+
<div className={`${ROOT_CLASS}__disclosure`}>
|
|
189
|
+
<span className={`${ROOT_CLASS}__compensated-cue`}>
|
|
190
|
+
Compensated{' '}
|
|
191
|
+
{profileUrl ? (
|
|
192
|
+
<a className={`${ROOT_CLASS}__cue-link`} href={profileUrl} rel="noopener">
|
|
193
|
+
expert
|
|
194
|
+
</a>
|
|
195
|
+
) : (
|
|
196
|
+
'expert'
|
|
197
|
+
)}
|
|
198
|
+
</span>
|
|
199
|
+
<p className={`${ROOT_CLASS}__ftc`} data-ftc-disclosure>
|
|
200
|
+
<span data-ftc-line>{CERT_SCOPE_LINE}</span>
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
152
204
|
{verifyUrl ? (
|
|
153
205
|
<a
|
|
154
206
|
className={`${ROOT_CLASS}__verify`}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { CertVerdict } from '../contract/kernel.js'
|
|
12
12
|
import { CertBadge } from './CertBadge.js'
|
|
13
13
|
import { CertJsonLd } from './CertJsonLd.js'
|
|
14
|
+
import type { CertRenderDef } from './render-def.js'
|
|
14
15
|
|
|
15
16
|
export interface CertReviewProps {
|
|
16
17
|
/** The verdict from `getVerifiedEnvelope` (or a direct kernel call). */
|
|
@@ -20,6 +21,12 @@ export interface CertReviewProps {
|
|
|
20
21
|
readonly accentColor?: string
|
|
21
22
|
readonly className?: string
|
|
22
23
|
readonly badgeStyle?: 'full' | 'compact'
|
|
24
|
+
/**
|
|
25
|
+
* The brand render-def (WS6). Forwarded to `<CertBadge>` for themeable-token
|
|
26
|
+
* emission + subtractive visibility; absent → the CertREV preset default. The
|
|
27
|
+
* JSON-LD is presentation-blind, so the def never touches the structured data.
|
|
28
|
+
*/
|
|
29
|
+
readonly renderDef?: CertRenderDef
|
|
23
30
|
/** Omit the JSON-LD (e.g. the page already emits it elsewhere). Default false. */
|
|
24
31
|
readonly omitJsonLd?: boolean
|
|
25
32
|
}
|
|
@@ -35,6 +42,7 @@ export function CertReview(props: CertReviewProps) {
|
|
|
35
42
|
accentColor={props.accentColor}
|
|
36
43
|
className={props.className}
|
|
37
44
|
badgeStyle={props.badgeStyle}
|
|
45
|
+
renderDef={props.renderDef}
|
|
38
46
|
/>
|
|
39
47
|
{props.omitJsonLd ? null : <CertJsonLd payload={payload} pageUrl={props.pageUrl} />}
|
|
40
48
|
</>
|
package/src/components/format.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CertContent, CertDisplayConfig } from '../contract/kernel.js'
|
|
7
|
+
import { type CertRenderDef, isFieldHidden, resolveRenderTheme } from './render-def.js'
|
|
7
8
|
|
|
8
9
|
/** Default accent if the brand didn't pin one (CertREV teal). */
|
|
9
10
|
export const DEFAULT_ACCENT = '#0f766e'
|
|
@@ -16,13 +17,29 @@ export interface ResolvedDisplay {
|
|
|
16
17
|
readonly badgeStyle: 'full' | 'compact'
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the optional display config + optional brand render-def to concrete
|
|
22
|
+
* presentation flags + accent.
|
|
23
|
+
*
|
|
24
|
+
* Accent PRECEDENCE mirrors the Shopify Liquid engine exactly: an explicit
|
|
25
|
+
* `accentOverride` (a block/prop override) wins, then the render-def's validated
|
|
26
|
+
* accent (a THEMED brand), then the signed display's accent, else the CertREV
|
|
27
|
+
* default. Visibility is SUBTRACTIVE: the render-def's hide-set can only turn a
|
|
28
|
+
* preset-placed field OFF (never on) — a hidden `reviewerPhoto` / `memo` wins over
|
|
29
|
+
* a display flag that shows it. When no render-def is supplied the theme resolves
|
|
30
|
+
* empty, so accent + visibility are byte-identical to the pre-theming behavior.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveDisplay(
|
|
33
|
+
display: CertDisplayConfig | undefined,
|
|
34
|
+
accentOverride?: string,
|
|
35
|
+
renderDef?: CertRenderDef,
|
|
36
|
+
): ResolvedDisplay {
|
|
37
|
+
const theme = resolveRenderTheme(renderDef)
|
|
21
38
|
return {
|
|
22
|
-
accentColor: accentOverride ?? display?.accentColor ?? DEFAULT_ACCENT,
|
|
23
|
-
showExpertPhoto: display?.showExpertPhoto ?? true,
|
|
39
|
+
accentColor: accentOverride ?? theme.accent ?? display?.accentColor ?? DEFAULT_ACCENT,
|
|
40
|
+
showExpertPhoto: (display?.showExpertPhoto ?? true) && !isFieldHidden(renderDef, 'reviewerPhoto'),
|
|
24
41
|
showAuthor: display?.showAuthor ?? true,
|
|
25
|
-
showMemo: display?.showMemo ?? true,
|
|
42
|
+
showMemo: (display?.showMemo ?? true) && !isFieldHidden(renderDef, 'memo'),
|
|
26
43
|
badgeStyle: display?.badgeStyle ?? 'full',
|
|
27
44
|
}
|
|
28
45
|
}
|