@certrev/cert-block 1.2.0-canary.0 → 1.2.0-canary.2
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/CHANGELOG.md +71 -0
- package/README.md +33 -10
- package/dist/reader/reader-css.generated.d.ts +1 -1
- package/dist/reader/reader-css.generated.d.ts.map +1 -1
- package/dist/reader/reader-css.generated.js +1 -1
- package/dist/reader/reader-css.generated.js.map +1 -1
- package/dist/reader/render.d.ts.map +1 -1
- package/dist/reader/render.js +36 -14
- package/dist/reader/render.js.map +1 -1
- package/dist/reader/tokens.d.ts.map +1 -1
- package/dist/reader/tokens.js +30 -14
- package/dist/reader/tokens.js.map +1 -1
- package/package.json +1 -1
- package/src/reader/reader-css.generated.ts +1 -1
- package/src/reader/reader.css +626 -121
- package/src/reader/render.ts +35 -15
- package/src/reader/tokens.ts +32 -14
package/src/reader/render.ts
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* its words are fixed. `illustrative` (a CertREV preview, never a live page) fills the cue's slot with the demo's
|
|
22
22
|
* "Illustrative example" label when the envelope has no cue; a real cue always wins the slot;
|
|
23
23
|
* - the scope line is never hideable: the card carries it when there is no memo, the memo card and the sidebar always;
|
|
24
|
+
* - the text leaves are the demo's (what React server-renders): the memo name is the name, "," and the credential; the
|
|
25
|
+
* profile link is "View", the first name and "’s profile"; the sidebar name is the name, "," and the credential in
|
|
26
|
+
* one `<b>`; the certified date is direct text of its span beside the "Certified" cue (no `<time>`); the scope
|
|
27
|
+
* sentence is direct text of the scope element, which carries `data-ftc-line` itself. The `<!-- -->` marks between
|
|
28
|
+
* adjacent leaves are React's, kept so the bytes equal the Liquid snippets' and the portal's goldens;
|
|
24
29
|
* - the def's `hidden` set removes the avatars (`reviewerPhoto`), `display.showExpertPhoto === false` keeps the
|
|
25
30
|
* silhouette and never requests the photo, `display.showAuthor === false` drops the author column.
|
|
26
31
|
*
|
|
@@ -153,10 +158,14 @@ function sectionOpen(f: Facts, badge: boolean, aria: string): string {
|
|
|
153
158
|
const anchor = (href: string, modal: string, label: string): string =>
|
|
154
159
|
`<a href="${esc(href)}" target="_blank" rel="${CERTREV_LINK_REL}" data-certrev-modal-open${modal}>${label}</a>`
|
|
155
160
|
|
|
156
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* The words the memo-less banner and the memo link row use for the profile link: the demo's three leaves ("View", the
|
|
163
|
+
* first name, "’s profile"), set apart by the `<!-- -->` React paints between adjacent text expressions. With no first
|
|
164
|
+
* name the wording stays one leaf.
|
|
165
|
+
*/
|
|
157
166
|
function profileLabel(f: Facts): string {
|
|
158
167
|
const first = cleaned(firstName(f.ename))
|
|
159
|
-
return first !== '' ? `View
|
|
168
|
+
return first !== '' ? `View <!-- -->${esc(first)}<!-- -->’s profile` : 'View profile'
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
const credentialAria = (f: Facts): string => `Content reviewed by ${f.ename}${f.creds !== '' ? `, ${f.creds}` : ''}`
|
|
@@ -168,15 +177,27 @@ function scopeLead(f: Facts): string {
|
|
|
168
177
|
return ''
|
|
169
178
|
}
|
|
170
179
|
|
|
171
|
-
|
|
180
|
+
/**
|
|
181
|
+
* The scope sentence is DIRECT text of the scope element (the demo's `<span class="dg-scope">`), which carries the
|
|
182
|
+
* `data-ftc-line` marker itself: no inner span, so the element's `.certrev-r-scope em` cue and the sentence are leaves of
|
|
183
|
+
* one element, as the demo paints them. The marker is on exactly one element per render, as before.
|
|
184
|
+
*/
|
|
185
|
+
const SCOPE = CERT_SCOPE_LINE
|
|
172
186
|
|
|
173
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* The muted credential line, empty when the reviewer has no displayed credential (so no "verified" claim prints).
|
|
189
|
+
*
|
|
190
|
+
* The banner's is the demo's three leaves in one `<em>`: the credential, " · ", and the attribution, which reads
|
|
191
|
+
* "Credential verified by CertREV on {date}" or, with no date, just "Credential verified by CertREV". The spans carry
|
|
192
|
+
* no style of their own. The sidebar names the credential in its bold line already, so its `<em>` is the attribution
|
|
193
|
+
* alone.
|
|
194
|
+
*/
|
|
174
195
|
function verifiedLine(f: Facts, withCredentials: boolean): string {
|
|
175
196
|
if (f.creds === '') return ''
|
|
176
197
|
const date = cleaned(isBlank(f.expert.credentialVerifiedAt) ? '' : longDate(f.expert.credentialVerifiedAt))
|
|
177
198
|
const on = date !== '' ? ` on ${esc(date)}` : ''
|
|
178
|
-
|
|
179
|
-
return `<em>${
|
|
199
|
+
if (!withCredentials) return `<em>Credential verified by CertREV${on}</em>`
|
|
200
|
+
return `<em><span>${esc(f.creds)}</span> · <span>Credential verified by CertREV${on}</span></em>`
|
|
180
201
|
}
|
|
181
202
|
|
|
182
203
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -218,10 +239,9 @@ function bannerCard(f: Facts): string {
|
|
|
218
239
|
|
|
219
240
|
let foot = ''
|
|
220
241
|
if (hasFoot) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
: '<span></span>'
|
|
242
|
+
// The demo's `<span><em>Certified</em>{date}</span>`: the date is direct text of the span (no `<time>`), so the
|
|
243
|
+
// span paints the mono cue and the date's own face.
|
|
244
|
+
const when = certified !== '' ? `<span><em>Certified</em>${esc(certified)}</span>` : '<span></span>'
|
|
225
245
|
let cue = ''
|
|
226
246
|
if (f.hasCue) {
|
|
227
247
|
cue = `<span>Compensated ${f.purl !== '' ? anchor(f.purl, '="expert"', 'expert') : 'expert'}</span>`
|
|
@@ -232,7 +252,7 @@ function bannerCard(f: Facts): string {
|
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
// POR-9317: the memo card carries the scope line when there is a memo; with none this card is the sole claim face.
|
|
235
|
-
const scope = f.noMemo ? `<div class="certrev-r-scope" data-ftc-disclosure>${SCOPE}</div>` : ''
|
|
255
|
+
const scope = f.noMemo ? `<div class="certrev-r-scope" data-ftc-disclosure data-ftc-line>${SCOPE}</div>` : ''
|
|
236
256
|
|
|
237
257
|
return (
|
|
238
258
|
`${sectionOpen(f, true, credentialAria(f))}\n <div class="certrev-r-card">\n` +
|
|
@@ -256,11 +276,11 @@ function bannerMemo(f: Facts): string {
|
|
|
256
276
|
`${sectionOpen(f, false, `Expert memo by ${f.ename}`)}\n <div class="certrev-r-card pad">\n` +
|
|
257
277
|
' <div class="certrev-r-kick">Expert memo</div>\n' +
|
|
258
278
|
` <div class="certrev-r-memo">${avatar}<div>\n` +
|
|
259
|
-
` <div class="certrev-r-memo-name"><b>${esc(f.ename)}</b>${f.creds !== '' ? `,
|
|
279
|
+
` <div class="certrev-r-memo-name"><b>${esc(f.ename)}</b>${f.creds !== '' ? `, <!-- -->${esc(f.creds)}` : ''}</div>\n` +
|
|
260
280
|
` <blockquote>${esc(f.content.memo)}</blockquote>${links}</div>\n </div>\n` +
|
|
261
281
|
' <div class="certrev-r-rule"></div>\n <div class="certrev-r-vfoot">\n' +
|
|
262
282
|
` <span class="certrev-r-verified">${readerLogo('mono', 17, '')}Verified by <b>CertREV</b></span>\n` +
|
|
263
|
-
` <span class="certrev-r-scope" data-ftc-disclosure>${scopeLead(f)}${SCOPE}</span>\n </div>\n </div>\n</section>`
|
|
283
|
+
` <span class="certrev-r-scope" data-ftc-disclosure data-ftc-line>${scopeLead(f)}${SCOPE}</span>\n </div>\n </div>\n</section>`
|
|
264
284
|
)
|
|
265
285
|
}
|
|
266
286
|
|
|
@@ -284,10 +304,10 @@ function sidebar(f: Facts): string {
|
|
|
284
304
|
`${sectionOpen(f, true, credentialAria(f))}\n <div class="certrev-r-side">\n` +
|
|
285
305
|
` <div class="certrev-r-cap center">${readerLogo(f.fg === '#ffffff' ? 'white' : 'mono', 18, 'CertREV')}Expert reviewed</div>\n` +
|
|
286
306
|
` <div class="certrev-r-side-body">\n <div class="certrev-r-by">${avatar}<div>\n` +
|
|
287
|
-
` <b>${esc(f.ename)}${f.creds !== '' ?
|
|
307
|
+
` <b>${esc(f.ename)}${f.creds !== '' ? `<!-- -->, <!-- -->${esc(f.creds)}` : ''}</b>${verifiedLine(f, false)}</div>\n </div>` +
|
|
288
308
|
`${bio !== '' ? `<p>${esc(bio)}</p>` : ''}` +
|
|
289
309
|
`${isBlank(f.content.memo) ? '' : `<blockquote>${esc(f.content.memo)}</blockquote>`}` +
|
|
290
|
-
`<div class="certrev-r-scope" data-ftc-disclosure>${scopeLead(f)}${SCOPE}</div>\n </div>` +
|
|
310
|
+
`<div class="certrev-r-scope" data-ftc-disclosure data-ftc-line>${scopeLead(f)}${SCOPE}</div>\n </div>` +
|
|
291
311
|
`${links}</div>\n</section>`
|
|
292
312
|
)
|
|
293
313
|
}
|
package/src/reader/tokens.ts
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* --cr-br the corner radius
|
|
7
7
|
* --cr-bf the font stack for the brand's `fontSlot`
|
|
8
8
|
*
|
|
9
|
-
* The values land inside a double-quoted `style` attribute,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The values land inside a double-quoted `style` attribute, and they are held to the allow-list the Liquid snippet and
|
|
10
|
+
* the PHP twin's def parser hold them to: a colour is `#RGB` or `#RRGGBB`, a radius one non-negative px / rem / em / %
|
|
11
|
+
* length or a bare `0`, and anything else paints the default (navy, 14px). A denylist of characters is not enough: a
|
|
12
|
+
* quote, a backslash, a comment opener or a bracket left in a value hides the `;` that ends it and swallows the
|
|
13
|
+
* declarations after it. The variable names are the reader stylesheet's (`reader.css`), which `.certrev-reader`
|
|
14
|
+
* declares defaults for.
|
|
12
15
|
*/
|
|
13
16
|
import { asText } from '../components/escape.js'
|
|
14
17
|
import { esc, rec, textOr } from './helpers.js'
|
|
@@ -21,16 +24,17 @@ const DARK_INK = '#141414'
|
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* The font stack per `fontSlot`, in one table so the Liquid `certrev-reader-tokens` snippet and this one are compared
|
|
24
|
-
* on a single list. An unknown or missing slot is the sans stack, as in the snippet. The
|
|
25
|
-
* the reader
|
|
26
|
-
*
|
|
27
|
+
* on a single list. An unknown or missing slot is the sans stack, as in the snippet. The stacks are the demo's own, byte
|
|
28
|
+
* for byte: the reader block of `reader.css` declares certrev.com's own faces under these family names (DM Sans,
|
|
29
|
+
* Playfair Display, Plus Jakarta Sans, JetBrains Mono) at `font-stretch: 101%`, which the reader root asks for, so the
|
|
30
|
+
* legacy faces a page may also load are untouched. `system` has no demo stack and keeps the legacy one.
|
|
27
31
|
*/
|
|
28
32
|
function fontStack(slot: string): string {
|
|
29
33
|
switch (slot) {
|
|
30
34
|
case 'serif':
|
|
31
|
-
return "'
|
|
35
|
+
return "'Playfair Display', ui-serif, Georgia, serif"
|
|
32
36
|
case 'grotesk':
|
|
33
|
-
return "'
|
|
37
|
+
return "'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif"
|
|
34
38
|
case 'mono':
|
|
35
39
|
return "'JetBrains Mono', ui-monospace, monospace"
|
|
36
40
|
case 'system':
|
|
@@ -40,10 +44,24 @@ function fontStack(slot: string): string {
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
/** A
|
|
44
|
-
|
|
47
|
+
/** A colour is `#` and then 3 or 6 hex digits (ASCII only, no trailing newline), else '' (which the caller defaults). */
|
|
48
|
+
const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
|
|
49
|
+
/**
|
|
50
|
+
* A radius is a bare `0`, or digits with at most one `.` (not trailing) and then px, rem, em or %, else ''. `\d` is
|
|
51
|
+
* ASCII here (no `u` flag), as the snippet's `0123456789.` is; the unit is case-sensitive.
|
|
52
|
+
*/
|
|
53
|
+
const LENGTH = /^(?:\d+|\d+\.\d+|\.\d+)(?:px|rem|em|%)$/
|
|
54
|
+
|
|
55
|
+
/** A colour token that is a hex colour, or '' (which the caller defaults). */
|
|
56
|
+
function hexColour(value: unknown): string {
|
|
57
|
+
const text = textOr(value)
|
|
58
|
+
return HEX.test(text) ? text : ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A radius token that is a bare 0 or a non-negative length, or '' (which the caller defaults). */
|
|
62
|
+
function radius(value: unknown): string {
|
|
45
63
|
const text = textOr(value)
|
|
46
|
-
return
|
|
64
|
+
return text === '0' || LENGTH.test(text) ? text : ''
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
export interface ReaderTokens {
|
|
@@ -57,8 +75,8 @@ export function readerTokens(def: unknown): ReaderTokens {
|
|
|
57
75
|
const rd = rec(def)
|
|
58
76
|
const tokens = rec(rd.tokens)
|
|
59
77
|
|
|
60
|
-
const barInk =
|
|
61
|
-
const accent =
|
|
78
|
+
const barInk = hexColour(tokens.barInk)
|
|
79
|
+
const accent = hexColour(tokens.accentColor)
|
|
62
80
|
const ba = barInk || accent || DEFAULT_ACCENT
|
|
63
81
|
|
|
64
82
|
// The ink follows the value that painted the accent: a def with a bar ink takes the foreground baked for it
|
|
@@ -70,7 +88,7 @@ export function readerTokens(def: unknown): ReaderTokens {
|
|
|
70
88
|
fg = DARK_INK
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
const br =
|
|
91
|
+
const br = radius(tokens.cornerRadius) || DEFAULT_RADIUS
|
|
74
92
|
const bf = fontStack(asText(tokens.fontSlot))
|
|
75
93
|
return { style: `--cr-ba:${esc(ba)};--cr-ba-fg:${fg};--cr-br:${esc(br)};--cr-bf:${bf}`, fg }
|
|
76
94
|
}
|