@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.
Files changed (64) hide show
  1. package/dist/head/css/themes/honeycrisp.css +1 -1
  2. package/dist/node.mjs +719 -26
  3. package/dist/plugins/mermaid/MermaidRenderer.tsx +2 -2
  4. package/dist/theme/components/home/hero-banner.tsx +225 -0
  5. package/dist/theme/components/home/hero-demo.css +492 -61
  6. package/dist/theme/components/home/hero-demo.test.tsx +159 -0
  7. package/dist/theme/components/home/hero-demo.tsx +335 -32
  8. package/dist/theme/components/home/hero.css +142 -9
  9. package/dist/theme/components/home/hero.test.tsx +105 -0
  10. package/dist/theme/components/home/hero.tsx +201 -4
  11. package/dist/theme/components/home/home-visual.css +98 -0
  12. package/dist/theme/components/home/home-visual.test.tsx +27 -0
  13. package/dist/theme/components/home/home-visual.tsx +20 -13
  14. package/dist/theme/components/home/layout.tsx +125 -1
  15. package/dist/theme/components/home/split.css +2 -2
  16. package/dist/theme/components/home/tabs.css +1 -1
  17. package/dist/theme/components/home/trust-strip.css +12 -0
  18. package/dist/theme/components/home/trust-strip.test.tsx +33 -0
  19. package/dist/theme/components/home/trust-strip.tsx +76 -15
  20. package/dist/theme/components/nav/layout.tsx +10 -14
  21. package/dist/theme/components/nav/nav-logo.tsx +2 -3
  22. package/dist/theme/components/seo-head-data.test.ts +196 -0
  23. package/dist/theme/components/seo-head-data.ts +490 -0
  24. package/dist/theme/components/seo-head.tsx +102 -0
  25. package/dist/theme/components/shared/IssueLinkIcon.tsx +42 -0
  26. package/dist/theme/components/shared/issue-link.css +74 -0
  27. package/dist/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  28. package/dist/theme/components/sidebar/sidebar-filter.ts +24 -4
  29. package/dist/theme/index.tsx +9 -1
  30. package/dist/theme/lib/rich-text-parse.ts +0 -2
  31. package/dist/theme/lib/seo-url.ts +28 -0
  32. package/dist/theme/lib/theme-favicon.ts +0 -3
  33. package/dist/theme/styles/themes/honeycrisp.css +20 -20
  34. package/package.json +23 -21
  35. package/src/theme/components/home/hero-banner.tsx +225 -0
  36. package/src/theme/components/home/hero-demo.css +492 -61
  37. package/src/theme/components/home/hero-demo.test.tsx +159 -0
  38. package/src/theme/components/home/hero-demo.tsx +335 -32
  39. package/src/theme/components/home/hero.css +142 -9
  40. package/src/theme/components/home/hero.test.tsx +105 -0
  41. package/src/theme/components/home/hero.tsx +201 -4
  42. package/src/theme/components/home/home-visual.css +98 -0
  43. package/src/theme/components/home/home-visual.test.tsx +27 -0
  44. package/src/theme/components/home/home-visual.tsx +20 -13
  45. package/src/theme/components/home/layout.tsx +125 -1
  46. package/src/theme/components/home/split.css +2 -2
  47. package/src/theme/components/home/tabs.css +1 -1
  48. package/src/theme/components/home/trust-strip.css +12 -0
  49. package/src/theme/components/home/trust-strip.test.tsx +33 -0
  50. package/src/theme/components/home/trust-strip.tsx +76 -15
  51. package/src/theme/components/nav/layout.tsx +10 -14
  52. package/src/theme/components/nav/nav-logo.tsx +2 -3
  53. package/src/theme/components/seo-head-data.test.ts +196 -0
  54. package/src/theme/components/seo-head-data.ts +490 -0
  55. package/src/theme/components/seo-head.tsx +102 -0
  56. package/src/theme/components/shared/IssueLinkIcon.tsx +42 -0
  57. package/src/theme/components/shared/issue-link.css +74 -0
  58. package/src/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  59. package/src/theme/components/sidebar/sidebar-filter.ts +24 -4
  60. package/src/theme/index.tsx +9 -1
  61. package/src/theme/lib/rich-text-parse.ts +0 -2
  62. package/src/theme/lib/seo-url.ts +28 -0
  63. package/src/theme/lib/theme-favicon.ts +0 -3
  64. 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="cp-hero-demo">
157
- <div className="cp-hero-demo__bar">
158
- <span className="cp-hero-demo__dot" />
159
- <span className="cp-hero-demo__dot" />
160
- <span className="cp-hero-demo__dot" />
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-hero-demo__title">{w}</span>)
169
+ .with(P.string, (w) => <span className="cp-home-terminal__title">{w}</span>)
163
170
  .otherwise(() => null)}
164
171
  </div>
165
- <pre className="cp-hero-demo__body">
166
- <span className="cp-hero-demo__prompt">$ </span>
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-hero-demo__ok')
212
- .with('cmt', () => 'cp-hero-demo__cmt')
213
- .with('err', () => 'cp-hero-demo__err')
214
- .otherwise(() => 'cp-hero-demo__info')
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 { HeroAction } from './hero'
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 reuses the hero demo chrome; the split frame already
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-hero-demo {
163
+ .cp-split__visual .cp-home-terminal {
164
164
  max-width: none;
165
165
  border: 0;
166
166
  border-radius: 0;
@@ -285,7 +285,7 @@
285
285
  overflow-x: auto;
286
286
  }
287
287
 
288
- .cp-tabs__visual .cp-hero-demo {
288
+ .cp-tabs__visual .cp-home-terminal {
289
289
  max-width: none;
290
290
  border: 0;
291
291
  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. Drawn as a mask filled with the
17
- * current text color, so one asset covers every theme and both variants.
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: string
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) => logo.src)
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
- <img
158
- alt={alt}
159
- className="cp-trust__logo"
160
- src={withMountBase(src)}
161
- style={{ height: `${height}px` }}
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 mask = `url("${encodeURI(withMountBase(src))}")`
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="cp-trust__logo cp-trust__logo--mono"
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={withMountBase(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
- (e): MetaAction => ({
277
- label: e.label ?? 'Edit this page on GitHub',
278
- href: buildEditUrl(e, pagePath),
279
- icon: <EditIcon />,
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
- (r): MetaAction => ({
286
- label: r.label ?? 'Report an issue',
287
- href: buildReportUrl(r),
288
- icon: <AlertIcon />,
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 functional/no-let -- raf id retained for cleanup
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 functional/no-let -- captured below for closure equality
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