@ciderpress/ui 1.0.0-rc.11 → 1.0.0-rc.14
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/head/css/themes/honeycrisp.css +1 -1
- package/dist/node.mjs +719 -26
- package/dist/plugins/mermaid/MermaidRenderer.tsx +2 -2
- package/dist/theme/components/home/hero-banner.tsx +225 -0
- package/dist/theme/components/home/hero-demo.css +492 -61
- package/dist/theme/components/home/hero-demo.test.tsx +159 -0
- package/dist/theme/components/home/hero-demo.tsx +335 -32
- package/dist/theme/components/home/hero.css +142 -9
- package/dist/theme/components/home/hero.test.tsx +105 -0
- package/dist/theme/components/home/hero.tsx +201 -4
- package/dist/theme/components/home/home-visual.css +98 -0
- package/dist/theme/components/home/home-visual.test.tsx +27 -0
- package/dist/theme/components/home/home-visual.tsx +20 -13
- package/dist/theme/components/home/layout.tsx +125 -1
- package/dist/theme/components/home/split.css +2 -2
- package/dist/theme/components/home/tabs.css +1 -1
- package/dist/theme/components/home/trust-strip.css +12 -0
- package/dist/theme/components/home/trust-strip.test.tsx +33 -0
- package/dist/theme/components/home/trust-strip.tsx +76 -15
- package/dist/theme/components/nav/layout.tsx +10 -14
- package/dist/theme/components/nav/nav-logo.tsx +2 -3
- package/dist/theme/components/seo-head-data.test.ts +196 -0
- package/dist/theme/components/seo-head-data.ts +490 -0
- package/dist/theme/components/seo-head.tsx +102 -0
- package/dist/theme/components/shared/IssueLinkIcon.tsx +42 -0
- package/dist/theme/components/shared/issue-link.css +74 -0
- package/dist/theme/components/sidebar/sidebar-filter.test.ts +49 -9
- package/dist/theme/components/sidebar/sidebar-filter.ts +24 -4
- package/dist/theme/index.tsx +9 -1
- package/dist/theme/lib/rich-text-parse.ts +0 -2
- package/dist/theme/lib/seo-url.ts +28 -0
- package/dist/theme/lib/theme-favicon.ts +0 -3
- package/dist/theme/styles/themes/honeycrisp.css +20 -20
- package/package.json +23 -21
- package/src/theme/components/home/hero-banner.tsx +225 -0
- package/src/theme/components/home/hero-demo.css +492 -61
- package/src/theme/components/home/hero-demo.test.tsx +159 -0
- package/src/theme/components/home/hero-demo.tsx +335 -32
- package/src/theme/components/home/hero.css +142 -9
- package/src/theme/components/home/hero.test.tsx +105 -0
- package/src/theme/components/home/hero.tsx +201 -4
- package/src/theme/components/home/home-visual.css +98 -0
- package/src/theme/components/home/home-visual.test.tsx +27 -0
- package/src/theme/components/home/home-visual.tsx +20 -13
- package/src/theme/components/home/layout.tsx +125 -1
- package/src/theme/components/home/split.css +2 -2
- package/src/theme/components/home/tabs.css +1 -1
- package/src/theme/components/home/trust-strip.css +12 -0
- package/src/theme/components/home/trust-strip.test.tsx +33 -0
- package/src/theme/components/home/trust-strip.tsx +76 -15
- package/src/theme/components/nav/layout.tsx +10 -14
- package/src/theme/components/nav/nav-logo.tsx +2 -3
- package/src/theme/components/seo-head-data.test.ts +196 -0
- package/src/theme/components/seo-head-data.ts +490 -0
- package/src/theme/components/seo-head.tsx +102 -0
- package/src/theme/components/shared/IssueLinkIcon.tsx +42 -0
- package/src/theme/components/shared/issue-link.css +74 -0
- package/src/theme/components/sidebar/sidebar-filter.test.ts +49 -9
- package/src/theme/components/sidebar/sidebar-filter.ts +24 -4
- package/src/theme/index.tsx +9 -1
- package/src/theme/lib/rich-text-parse.ts +0 -2
- package/src/theme/lib/seo-url.ts +28 -0
- package/src/theme/lib/theme-favicon.ts +0 -3
- package/src/theme/styles/themes/honeycrisp.css +20 -20
|
@@ -6,6 +6,7 @@ import type React from 'react'
|
|
|
6
6
|
import { withMountBase } from '../../lib/with-mount-base.ts'
|
|
7
7
|
|
|
8
8
|
import './hero-demo.css'
|
|
9
|
+
import './home-visual.css'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Surface a {@link HomeVisual} is rendered into.
|
|
@@ -47,7 +48,7 @@ export function HomeVisualView(props: HomeVisualViewProps): React.ReactElement |
|
|
|
47
48
|
return match(visual)
|
|
48
49
|
.with({ type: 'code' }, (v) => renderCode(v.code, v.language, context))
|
|
49
50
|
.with({ type: 'image' }, (v) => renderImage(v, context))
|
|
50
|
-
.with({ type: 'terminal' }, (v) => <VisualTerminal config={v} />)
|
|
51
|
+
.with({ type: 'terminal' }, (v) => <VisualTerminal config={v} context={context} />)
|
|
51
52
|
.otherwise(() => null)
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -122,6 +123,7 @@ function renderImage(image: VisualImageFields, context: HomeVisualContext): Reac
|
|
|
122
123
|
|
|
123
124
|
interface VisualTerminalProps {
|
|
124
125
|
readonly config: HomeVisualTerminal
|
|
126
|
+
readonly context: HomeVisualContext
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
@@ -135,6 +137,9 @@ interface VisualTerminalProps {
|
|
|
135
137
|
*/
|
|
136
138
|
function VisualTerminal(props: VisualTerminalProps): React.ReactElement {
|
|
137
139
|
const { windowTitle, command, lines } = props.config
|
|
140
|
+
const className = match(props.context)
|
|
141
|
+
.with('hero', () => 'cp-home-terminal cp-home-terminal--framed')
|
|
142
|
+
.otherwise(() => 'cp-home-terminal')
|
|
138
143
|
// Hand-authored frontmatter bypasses the schema; a non-string here
|
|
139
144
|
// reaches React as a child and throws mid-render.
|
|
140
145
|
const commandText = match(command)
|
|
@@ -153,17 +158,19 @@ function VisualTerminal(props: VisualTerminalProps): React.ReactElement {
|
|
|
153
158
|
)
|
|
154
159
|
.otherwise(() => [])
|
|
155
160
|
return (
|
|
156
|
-
<div className="
|
|
157
|
-
<div className="cp-
|
|
158
|
-
<span className="cp-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
<div className={className} role="group" aria-label="Terminal output">
|
|
162
|
+
<div className="cp-home-terminal__bar">
|
|
163
|
+
<span className="cp-home-terminal__dots" aria-hidden="true">
|
|
164
|
+
<span className="cp-home-terminal__dot cp-home-terminal__dot--close" />
|
|
165
|
+
<span className="cp-home-terminal__dot cp-home-terminal__dot--minimize" />
|
|
166
|
+
<span className="cp-home-terminal__dot cp-home-terminal__dot--maximize" />
|
|
167
|
+
</span>
|
|
161
168
|
{match(title)
|
|
162
|
-
.with(P.string, (w) => <span className="cp-
|
|
169
|
+
.with(P.string, (w) => <span className="cp-home-terminal__title">{w}</span>)
|
|
163
170
|
.otherwise(() => null)}
|
|
164
171
|
</div>
|
|
165
|
-
<pre className="cp-
|
|
166
|
-
<span className="cp-
|
|
172
|
+
<pre className="cp-home-terminal__body">
|
|
173
|
+
<span className="cp-home-terminal__prompt">$ </span>
|
|
167
174
|
{commandText}
|
|
168
175
|
{'\n\n'}
|
|
169
176
|
{output.map((line, i) => (
|
|
@@ -208,10 +215,10 @@ function TerminalLine(props: TerminalLineProps): React.ReactElement {
|
|
|
208
215
|
// hand-authored frontmatter — fall back to the neutral marker rather
|
|
209
216
|
// than throwing on an unknown value.
|
|
210
217
|
const className = match(props.line.kind)
|
|
211
|
-
.with('ok', () => 'cp-
|
|
212
|
-
.with('cmt', () => 'cp-
|
|
213
|
-
.with('err', () => 'cp-
|
|
214
|
-
.otherwise(() => 'cp-
|
|
218
|
+
.with('ok', () => 'cp-home-terminal__ok')
|
|
219
|
+
.with('cmt', () => 'cp-home-terminal__cmt')
|
|
220
|
+
.with('err', () => 'cp-home-terminal__err')
|
|
221
|
+
.otherwise(() => 'cp-home-terminal__info')
|
|
215
222
|
const glyph = match(props.line.kind)
|
|
216
223
|
.with('ok', () => ' ✓')
|
|
217
224
|
.with('cmt', () => ' ↻')
|
|
@@ -9,7 +9,14 @@ import { CTA } from './cta'
|
|
|
9
9
|
import { HomeFeature } from './feature'
|
|
10
10
|
import type { FeatureItem } from './feature-card'
|
|
11
11
|
import { Hero } from './hero'
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
HeroAction,
|
|
14
|
+
HeroBackground,
|
|
15
|
+
HeroBackgroundImage,
|
|
16
|
+
HeroBackgroundSource,
|
|
17
|
+
HeroBackgroundVariants,
|
|
18
|
+
} from './hero'
|
|
19
|
+
import { HeroBanner } from './hero-banner'
|
|
13
20
|
import { HeroDemo } from './hero-demo'
|
|
14
21
|
import { HomeVisualView } from './home-visual'
|
|
15
22
|
import { PageRail } from './page-rail'
|
|
@@ -151,6 +158,10 @@ export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
|
|
|
151
158
|
// false → suppress entirely
|
|
152
159
|
// object → render the user-supplied custom variant
|
|
153
160
|
const heroDemoFm = fm.heroDemo as false | HomeVisual | undefined
|
|
161
|
+
// heroBackground frontmatter arrives as an unvalidated object (or `null`
|
|
162
|
+
// when hand-authored with a blank `heroBackground:` line). Treat anything
|
|
163
|
+
// without a string `src` or valid dark/light sources as absent.
|
|
164
|
+
const heroBackground = parseHeroBackground(fm.heroBackground)
|
|
154
165
|
const blocks = (fm.blocks as readonly FrontmatterBlock[] | undefined) ?? []
|
|
155
166
|
|
|
156
167
|
// `P.nullish`, not `undefined`: a blank `heroDemo:` in a hand-authored
|
|
@@ -170,6 +181,10 @@ export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
|
|
|
170
181
|
tagline={renderOptionalRichText(h.tagline)}
|
|
171
182
|
actions={mapButtonsToHeroActions(h.actions)}
|
|
172
183
|
demo={heroDemoEl}
|
|
184
|
+
background={heroBackground}
|
|
185
|
+
backgroundContent={match(heroBackground)
|
|
186
|
+
.with(undefined, () => <HeroBanner />)
|
|
187
|
+
.otherwise(() => undefined)}
|
|
173
188
|
/>
|
|
174
189
|
))
|
|
175
190
|
|
|
@@ -186,6 +201,115 @@ export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
|
|
|
186
201
|
)
|
|
187
202
|
}
|
|
188
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Validate untrusted hero background frontmatter before rendering it as CSS.
|
|
206
|
+
*
|
|
207
|
+
* @private
|
|
208
|
+
* @param value - Raw frontmatter value.
|
|
209
|
+
* @returns A renderable background, or undefined for malformed input.
|
|
210
|
+
*/
|
|
211
|
+
function parseHeroBackground(value: unknown): HeroBackground | undefined {
|
|
212
|
+
if (!isRecord(value)) {
|
|
213
|
+
return undefined
|
|
214
|
+
}
|
|
215
|
+
if (isHeroBackgroundImage(value)) {
|
|
216
|
+
return value
|
|
217
|
+
}
|
|
218
|
+
if (isHeroBackgroundVariants(value)) {
|
|
219
|
+
return value
|
|
220
|
+
}
|
|
221
|
+
return undefined
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Validate one fixed-mode background image.
|
|
226
|
+
*
|
|
227
|
+
* @private
|
|
228
|
+
* @param value - Raw background record.
|
|
229
|
+
* @returns Whether the record is a valid fixed-mode image.
|
|
230
|
+
*/
|
|
231
|
+
function isHeroBackgroundImage(value: unknown): value is HeroBackgroundImage {
|
|
232
|
+
if (!isRecord(value)) {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
return (
|
|
236
|
+
isHeroBackgroundSource(value) &&
|
|
237
|
+
(value.mode === undefined || value.mode === 'dark' || value.mode === 'light')
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Validate dark and light background source variants.
|
|
243
|
+
*
|
|
244
|
+
* @private
|
|
245
|
+
* @param value - Raw background record.
|
|
246
|
+
* @returns Whether both variants are valid sources.
|
|
247
|
+
*/
|
|
248
|
+
function isHeroBackgroundVariants(value: unknown): value is HeroBackgroundVariants {
|
|
249
|
+
if (!isRecord(value)) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
return isHeroBackgroundSource(value.dark) && isHeroBackgroundSource(value.light)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate a background source and every optional CSS field.
|
|
257
|
+
*
|
|
258
|
+
* @private
|
|
259
|
+
* @param value - Raw source value.
|
|
260
|
+
* @returns Whether the value is safe to pass into inline styles.
|
|
261
|
+
*/
|
|
262
|
+
function isHeroBackgroundSource(value: unknown): value is HeroBackgroundSource {
|
|
263
|
+
if (!isRecord(value) || typeof value.src !== 'string') {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
return (
|
|
267
|
+
isOptionalString(value.position) &&
|
|
268
|
+
isOptionalString(value.size) &&
|
|
269
|
+
isOptionalString(value.repeat) &&
|
|
270
|
+
isResponsiveSources(value.sources)
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Validate optional tablet and mobile source overrides.
|
|
276
|
+
*
|
|
277
|
+
* @private
|
|
278
|
+
* @param value - Raw responsive sources value.
|
|
279
|
+
* @returns Whether the value is absent or contains only valid source strings.
|
|
280
|
+
*/
|
|
281
|
+
function isResponsiveSources(value: unknown): boolean {
|
|
282
|
+
if (value === undefined) {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
if (!isRecord(value)) {
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
return isOptionalString(value.tablet) && isOptionalString(value.mobile)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Test whether an unknown value is absent or a string.
|
|
293
|
+
*
|
|
294
|
+
* @private
|
|
295
|
+
* @param value - Unknown optional field.
|
|
296
|
+
* @returns Whether the value is safe for a string-valued option.
|
|
297
|
+
*/
|
|
298
|
+
function isOptionalString(value: unknown): value is string | undefined {
|
|
299
|
+
return value === undefined || typeof value === 'string'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Narrow an unknown value to an object record.
|
|
304
|
+
*
|
|
305
|
+
* @private
|
|
306
|
+
* @param value - Unknown value.
|
|
307
|
+
* @returns Whether the value is a non-null, non-array object.
|
|
308
|
+
*/
|
|
309
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
310
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
311
|
+
}
|
|
312
|
+
|
|
189
313
|
/**
|
|
190
314
|
* Render a single home block by its discriminated `type`.
|
|
191
315
|
*
|
|
@@ -158,9 +158,9 @@ html[data-cp-variant='dark'] .cp-split__eyebrow {
|
|
|
158
158
|
overflow-x: auto;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
/* Terminal visual
|
|
161
|
+
/* Terminal visual supplies its own chrome; the split frame already
|
|
162
162
|
owns the border, radius, and shadow, so flatten the inner shell. */
|
|
163
|
-
.cp-split__visual .cp-
|
|
163
|
+
.cp-split__visual .cp-home-terminal {
|
|
164
164
|
max-width: none;
|
|
165
165
|
border: 0;
|
|
166
166
|
border-radius: 0;
|
|
@@ -38,6 +38,18 @@
|
|
|
38
38
|
vertical-align: middle;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.cp-trust__logo--light {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
html[data-cp-variant='light'] .cp-trust__logo--dark {
|
|
46
|
+
display: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
html[data-cp-variant='light'] .cp-trust__logo--light {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/*
|
|
42
54
|
* Mono variant: paint a solid block of the current text color, clipped
|
|
43
55
|
* to the asset's alpha channel. One file then tracks every theme and
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { TrustStrip } from './trust-strip.tsx'
|
|
5
|
+
|
|
6
|
+
vi.mock('@rspress/core/runtime', () => ({ withBase: (value: string) => value }))
|
|
7
|
+
|
|
8
|
+
describe('TrustStrip', () => {
|
|
9
|
+
it('should render one image for a shared logo source', () => {
|
|
10
|
+
const output = renderToStaticMarkup(
|
|
11
|
+
<TrustStrip names={[{ src: '/logos/acme.svg', alt: 'Acme' }]} />
|
|
12
|
+
)
|
|
13
|
+
expect(output).toContain('class="cp-trust__logo"')
|
|
14
|
+
expect(output.match(/<img/g)).toHaveLength(1)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should render dark and light images for a variant logo source', () => {
|
|
18
|
+
const output = renderToStaticMarkup(
|
|
19
|
+
<TrustStrip
|
|
20
|
+
names={[
|
|
21
|
+
{
|
|
22
|
+
src: { dark: '/logos/acme-dark.svg', light: '/logos/acme-light.svg' },
|
|
23
|
+
alt: 'Acme',
|
|
24
|
+
},
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
expect(output).toContain('class="cp-trust__logo cp-trust__logo--dark"')
|
|
29
|
+
expect(output).toContain('src="/logos/acme-dark.svg"')
|
|
30
|
+
expect(output).toContain('class="cp-trust__logo cp-trust__logo--light"')
|
|
31
|
+
expect(output).toContain('src="/logos/acme-light.svg"')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -13,14 +13,19 @@ import './trust-strip.css'
|
|
|
13
13
|
const DEFAULT_LOGO_HEIGHT = 20
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* A logo entry in the trust strip.
|
|
17
|
-
*
|
|
16
|
+
* A logo entry in the trust strip. Supports shared, variant-specific,
|
|
17
|
+
* full-color, and monochrome artwork.
|
|
18
18
|
*/
|
|
19
19
|
export interface TrustLogo {
|
|
20
20
|
/**
|
|
21
21
|
* Public URL of the logo asset.
|
|
22
22
|
*/
|
|
23
|
-
readonly src:
|
|
23
|
+
readonly src:
|
|
24
|
+
| string
|
|
25
|
+
| {
|
|
26
|
+
readonly dark: string
|
|
27
|
+
readonly light: string
|
|
28
|
+
}
|
|
24
29
|
/**
|
|
25
30
|
* Accessible name for the logo.
|
|
26
31
|
*/
|
|
@@ -99,7 +104,11 @@ export function TrustStrip(props: TrustStripProps): React.ReactElement | null {
|
|
|
99
104
|
function itemKey(item: TrustItem): string {
|
|
100
105
|
return match(item)
|
|
101
106
|
.with(P.string, (name) => name)
|
|
102
|
-
.otherwise((logo) =>
|
|
107
|
+
.otherwise((logo) =>
|
|
108
|
+
match(logo.src)
|
|
109
|
+
.with(P.string, (src) => src)
|
|
110
|
+
.otherwise((src) => `${src.dark}:${src.light}`)
|
|
111
|
+
)
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
/**
|
|
@@ -153,14 +162,14 @@ function renderLogo(logo: TrustLogo): React.ReactNode {
|
|
|
153
162
|
*/
|
|
154
163
|
function renderColorMark(logo: TrustLogo): React.ReactNode {
|
|
155
164
|
const { src, alt, height = DEFAULT_LOGO_HEIGHT } = logo
|
|
156
|
-
return (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
return match(src)
|
|
166
|
+
.with(P.string, (source) => renderColorSource({ source, alt, height }))
|
|
167
|
+
.otherwise((sources) => (
|
|
168
|
+
<>
|
|
169
|
+
{renderColorSource({ source: sources.dark, alt, height, variant: 'dark' })}
|
|
170
|
+
{renderColorSource({ source: sources.light, alt, height, variant: 'light' })}
|
|
171
|
+
</>
|
|
172
|
+
))
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
/**
|
|
@@ -175,18 +184,70 @@ function renderColorMark(logo: TrustLogo): React.ReactNode {
|
|
|
175
184
|
*/
|
|
176
185
|
function renderMonoMark(logo: TrustLogo): React.ReactNode {
|
|
177
186
|
const { src, alt, height = DEFAULT_LOGO_HEIGHT } = logo
|
|
187
|
+
return match(src)
|
|
188
|
+
.with(P.string, (source) => renderMonoSource({ source, alt, height }))
|
|
189
|
+
.otherwise((sources) => (
|
|
190
|
+
<>
|
|
191
|
+
{renderMonoSource({ source: sources.dark, alt, height, variant: 'dark' })}
|
|
192
|
+
{renderMonoSource({ source: sources.light, alt, height, variant: 'light' })}
|
|
193
|
+
</>
|
|
194
|
+
))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface RenderLogoSourceParams {
|
|
198
|
+
readonly source: string
|
|
199
|
+
readonly alt: string
|
|
200
|
+
readonly height: number
|
|
201
|
+
readonly variant?: 'dark' | 'light'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Render one color logo source.
|
|
206
|
+
*
|
|
207
|
+
* @private
|
|
208
|
+
* @param params - Source, accessible name, height, and optional site variant
|
|
209
|
+
* @returns Image element for the source
|
|
210
|
+
*/
|
|
211
|
+
function renderColorSource(params: RenderLogoSourceParams): React.ReactNode {
|
|
212
|
+
const { source, alt, height, variant } = params
|
|
213
|
+
const className = match(variant)
|
|
214
|
+
.with(P.string, (value) => `cp-trust__logo cp-trust__logo--${value}`)
|
|
215
|
+
.otherwise(() => 'cp-trust__logo')
|
|
216
|
+
return (
|
|
217
|
+
<img
|
|
218
|
+
alt={alt}
|
|
219
|
+
className={className}
|
|
220
|
+
src={withMountBase(source)}
|
|
221
|
+
style={{ height: `${height}px` }}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render one monochrome logo source.
|
|
228
|
+
*
|
|
229
|
+
* @private
|
|
230
|
+
* @param params - Source, accessible name, height, and optional site variant
|
|
231
|
+
* @returns Masked logo element for the source
|
|
232
|
+
*/
|
|
233
|
+
function renderMonoSource(params: RenderLogoSourceParams): React.ReactNode {
|
|
234
|
+
const { source, alt, height, variant } = params
|
|
178
235
|
// `encodeURI` so a `"` or `)` in the filename cannot terminate the
|
|
179
236
|
// `url()` token — the CSSOM would reject the whole declaration, and a
|
|
180
237
|
// mask that fails to load leaves the fill unclipped, painting a solid
|
|
181
238
|
// text-coloured rectangle where the logo should be.
|
|
182
|
-
const
|
|
239
|
+
const resolvedSource = withMountBase(source)
|
|
240
|
+
const mask = `url("${encodeURI(resolvedSource)}")`
|
|
241
|
+
const className = match(variant)
|
|
242
|
+
.with(P.string, (value) => `cp-trust__logo cp-trust__logo--mono cp-trust__logo--${value}`)
|
|
243
|
+
.otherwise(() => 'cp-trust__logo cp-trust__logo--mono')
|
|
183
244
|
// The inner image is hidden but still laid out, so the masked wrapper
|
|
184
245
|
// inherits the asset's intrinsic aspect ratio. A mask alone has no
|
|
185
246
|
// intrinsic size, so without it every logo would collapse to zero width.
|
|
186
247
|
return (
|
|
187
248
|
<span
|
|
188
249
|
aria-label={alt}
|
|
189
|
-
className=
|
|
250
|
+
className={className}
|
|
190
251
|
role="img"
|
|
191
252
|
style={{
|
|
192
253
|
height: `${height}px`,
|
|
@@ -194,7 +255,7 @@ function renderMonoMark(logo: TrustLogo): React.ReactNode {
|
|
|
194
255
|
WebkitMaskImage: mask,
|
|
195
256
|
}}
|
|
196
257
|
>
|
|
197
|
-
<img alt="" aria-hidden="true" src={
|
|
258
|
+
<img alt="" aria-hidden="true" src={resolvedSource} />
|
|
198
259
|
</span>
|
|
199
260
|
)
|
|
200
261
|
}
|
|
@@ -272,22 +272,18 @@ function collectMetaActions(params: {
|
|
|
272
272
|
const { edit, report, pagePath } = params
|
|
273
273
|
const editAction = match(edit)
|
|
274
274
|
.with(undefined, () => null)
|
|
275
|
-
.otherwise(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
})
|
|
281
|
-
)
|
|
275
|
+
.otherwise((e): MetaAction => ({
|
|
276
|
+
label: e.label ?? 'Edit this page on GitHub',
|
|
277
|
+
href: buildEditUrl(e, pagePath),
|
|
278
|
+
icon: <EditIcon />,
|
|
279
|
+
}))
|
|
282
280
|
const reportAction = match(report)
|
|
283
281
|
.with(undefined, () => null)
|
|
284
|
-
.otherwise(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
})
|
|
290
|
-
)
|
|
282
|
+
.otherwise((r): MetaAction => ({
|
|
283
|
+
label: r.label ?? 'Report an issue',
|
|
284
|
+
href: buildReportUrl(r),
|
|
285
|
+
icon: <AlertIcon />,
|
|
286
|
+
}))
|
|
291
287
|
return [editAction, reportAction].filter((a): a is MetaAction => a !== null)
|
|
292
288
|
}
|
|
293
289
|
|
|
@@ -74,9 +74,9 @@ export function NavLogo(): React.ReactElement | null {
|
|
|
74
74
|
// so a single querySelector at effect time misses the element. We
|
|
75
75
|
// belt-and-suspenders with both rAF polling AND MutationObserver in
|
|
76
76
|
// case the polling stops before the nav appears (e.g. tab inactive).
|
|
77
|
-
// oxlint-disable-next-line
|
|
77
|
+
// oxlint-disable-next-line ciderpress/no-let -- raf id retained for cleanup
|
|
78
78
|
let rafId: number | null = null
|
|
79
|
-
// oxlint-disable-next-line
|
|
79
|
+
// oxlint-disable-next-line ciderpress/no-let -- captured below for closure equality
|
|
80
80
|
let resolved = false
|
|
81
81
|
|
|
82
82
|
function tryResolve() {
|
|
@@ -85,7 +85,6 @@ export function NavLogo(): React.ReactElement | null {
|
|
|
85
85
|
}
|
|
86
86
|
const link = findTarget()
|
|
87
87
|
if (link !== null) {
|
|
88
|
-
// oxlint-disable-next-line functional/immutable-data -- flag flip to stop polling
|
|
89
88
|
resolved = true
|
|
90
89
|
setTarget(link)
|
|
91
90
|
return
|