@ciderpress/ui 1.0.0-rc.3 → 1.0.0-rc.4

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 (65) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.mjs +4 -3
  3. package/dist/node.d.ts +1 -1
  4. package/dist/node.mjs +316 -50
  5. package/dist/theme/components/footer/site-footer.tsx +68 -9
  6. package/dist/theme/components/home/feature-card.tsx +5 -8
  7. package/dist/theme/components/home/feature.tsx +31 -11
  8. package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
  9. package/dist/theme/components/home/hero-demo.css +27 -0
  10. package/dist/theme/components/home/layout.tsx +149 -26
  11. package/dist/theme/components/home/split-visual-custom.tsx +26 -0
  12. package/dist/theme/components/home/workspaces.tsx +39 -3
  13. package/dist/theme/components/nav/branch-tag.tsx +7 -7
  14. package/dist/theme/components/nav/ciderpress-header.css +2 -1
  15. package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
  16. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
  17. package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
  18. package/dist/theme/components/nav/header-icon.css +24 -0
  19. package/dist/theme/components/nav/header-icon.tsx +119 -0
  20. package/dist/theme/components/nav/header-logo.css +16 -0
  21. package/dist/theme/components/nav/header-logo.tsx +200 -0
  22. package/dist/theme/components/nav/layout.tsx +45 -7
  23. package/dist/theme/components/nav/nav-logo.tsx +5 -1
  24. package/dist/theme/components/shared/card-icon.tsx +37 -0
  25. package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
  26. package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
  27. package/dist/theme/components/shared/section-card.tsx +9 -6
  28. package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
  29. package/dist/theme/components/theme-provider.tsx +27 -11
  30. package/dist/theme/components/workspaces/card.tsx +7 -9
  31. package/dist/theme/hooks/use-ciderpress.ts +60 -5
  32. package/dist/theme/lib/read-social-links.ts +4 -3
  33. package/dist/theme/lib/theme-favicon.ts +1 -1
  34. package/dist/theme/lib/with-mount-base.ts +31 -0
  35. package/package.json +13 -13
  36. package/src/theme/components/footer/site-footer.tsx +68 -9
  37. package/src/theme/components/home/feature-card.tsx +5 -8
  38. package/src/theme/components/home/feature.tsx +31 -11
  39. package/src/theme/components/home/hero-demo-custom.tsx +116 -0
  40. package/src/theme/components/home/hero-demo.css +27 -0
  41. package/src/theme/components/home/layout.tsx +149 -26
  42. package/src/theme/components/home/split-visual-custom.tsx +26 -0
  43. package/src/theme/components/home/workspaces.tsx +39 -3
  44. package/src/theme/components/nav/branch-tag.tsx +7 -7
  45. package/src/theme/components/nav/ciderpress-header.css +2 -1
  46. package/src/theme/components/nav/ciderpress-header.tsx +26 -3
  47. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
  48. package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
  49. package/src/theme/components/nav/header-icon.css +24 -0
  50. package/src/theme/components/nav/header-icon.tsx +119 -0
  51. package/src/theme/components/nav/header-logo.css +16 -0
  52. package/src/theme/components/nav/header-logo.tsx +200 -0
  53. package/src/theme/components/nav/layout.tsx +45 -7
  54. package/src/theme/components/nav/nav-logo.tsx +5 -1
  55. package/src/theme/components/shared/card-icon.tsx +37 -0
  56. package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
  57. package/src/theme/components/shared/resolve-card-icon.ts +53 -12
  58. package/src/theme/components/shared/section-card.tsx +9 -6
  59. package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
  60. package/src/theme/components/theme-provider.tsx +27 -11
  61. package/src/theme/components/workspaces/card.tsx +7 -9
  62. package/src/theme/hooks/use-ciderpress.ts +60 -5
  63. package/src/theme/lib/read-social-links.ts +4 -3
  64. package/src/theme/lib/theme-favicon.ts +1 -1
  65. package/src/theme/lib/with-mount-base.ts +31 -0
@@ -1,10 +1,10 @@
1
- import type { SiteEditConfig, SiteReportConfig } from '@ciderpress/config'
2
1
  import { useFrontmatter, useSite } from '@rspress/core/runtime'
3
2
  import { Layout as OriginalLayout } from '@rspress/core/theme-original'
4
3
  import { match, P } from 'massaman/match'
5
4
  import type React from 'react'
6
5
  import { useEffect } from 'react'
7
6
 
7
+ import type { CiderpressSiteBlock } from '../../hooks/use-ciderpress'
8
8
  import { useCiderpress } from '../../hooks/use-ciderpress'
9
9
  import { useNavItems } from '../../hooks/use-nav-items'
10
10
  import { readSocialLinks } from '../../lib/read-social-links'
@@ -120,6 +120,30 @@ export function Layout(): React.ReactElement {
120
120
  </ContentFooterPortal>
121
121
  )
122
122
 
123
+ // Rspress's SSG-MD pipeline renders the React tree through
124
+ // `react-render-to-markdown` to produce the `.md` files served to
125
+ // `<LlmsCopyButton />`. If we render our chrome (header, docs bar,
126
+ // footer slot wrappers) during that pass, the topbar logo, search,
127
+ // and nav items all get serialized as markdown and end up at the
128
+ // top of the copied content. Short-circuit to just the article body
129
+ // via `<OriginalLayout />` (Rspress's stock layout already returns a
130
+ // Fragment-only output for SSG-MD). The visible HTML SSG path
131
+ // (`SSG_MD` undefined) still gets the full chrome.
132
+ if (import.meta.env.SSG_MD) {
133
+ return (
134
+ <OriginalLayout
135
+ top={null}
136
+ beforeNavMenu={null}
137
+ afterNavMenu={null}
138
+ beforeSidebar={null}
139
+ afterSidebar={null}
140
+ beforeDoc={null}
141
+ afterDoc={null}
142
+ bottom={null}
143
+ />
144
+ )
145
+ }
146
+
123
147
  return (
124
148
  <>
125
149
  <CiderpressHeader
@@ -169,16 +193,17 @@ function readNavItems(site: unknown): readonly CiderpressNavMenuItem[] {
169
193
 
170
194
  /**
171
195
  * Build the list of `MetaAction`s to render under each doc page,
172
- * derived from `site.edit` and `site.report`. Returns an empty array
173
- * when neither is configured.
196
+ * derived from the serialised `site.edit` / `site.report` blocks
197
+ * (originally configured via top-level `editLink` / `reportLink`).
198
+ * Returns an empty array when neither is configured.
174
199
  *
175
200
  * @private
176
201
  * @param params - Site edit/report config plus current page path
177
202
  * @returns Ordered list of meta actions
178
203
  */
179
204
  function collectMetaActions(params: {
180
- readonly edit: SiteEditConfig | undefined
181
- readonly report: SiteReportConfig | undefined
205
+ readonly edit: CiderpressSiteBlock['edit']
206
+ readonly report: CiderpressSiteBlock['report']
182
207
  readonly pagePath: string
183
208
  }): readonly MetaAction[] {
184
209
  const { edit, report, pagePath } = params
@@ -204,9 +229,17 @@ function collectMetaActions(params: {
204
229
  }
205
230
 
206
231
  /**
232
+ * Construct the per-page "edit this page" URL from a serialised
233
+ * `editLink` block. The `buildSiteBlock` helper guarantees `repo` is
234
+ * present whenever the block itself is — callers should already have
235
+ * skipped the action when the block was `undefined`.
236
+ *
207
237
  * @private
238
+ * @param edit - Site edit block (non-undefined when called)
239
+ * @param pagePath - Current page's source path (with leading slash)
240
+ * @returns Fully-qualified URL string
208
241
  */
209
- function buildEditUrl(edit: SiteEditConfig, pagePath: string): string {
242
+ function buildEditUrl(edit: NonNullable<CiderpressSiteBlock['edit']>, pagePath: string): string {
210
243
  if (edit.repo.startsWith('http')) {
211
244
  return edit.repo
212
245
  }
@@ -219,9 +252,14 @@ function buildEditUrl(edit: SiteEditConfig, pagePath: string): string {
219
252
  }
220
253
 
221
254
  /**
255
+ * Construct the "report an issue" URL from a serialised `reportLink`
256
+ * block. Same `repo`-presence contract as {@link buildEditUrl}.
257
+ *
222
258
  * @private
259
+ * @param report - Site report block (non-undefined when called)
260
+ * @returns Fully-qualified URL string
223
261
  */
224
- function buildReportUrl(report: SiteReportConfig): string {
262
+ function buildReportUrl(report: NonNullable<CiderpressSiteBlock['report']>): string {
225
263
  if (report.repo.startsWith('http')) {
226
264
  return report.repo
227
265
  }
@@ -153,7 +153,11 @@ function readLogoConfig(mod: unknown): string | LogoFn | undefined {
153
153
  asRecord.default !== null && asRecord.default !== undefined
154
154
  ? (asRecord.default as Partial<CiderpressConfig>)
155
155
  : (mod as Partial<CiderpressConfig>)
156
- const { logo } = candidate
156
+ const brand = candidate.brand
157
+ if (brand === undefined) {
158
+ return undefined
159
+ }
160
+ const { logo } = brand
157
161
  if (typeof logo === 'string') {
158
162
  return logo
159
163
  }
@@ -0,0 +1,37 @@
1
+ import { match } from 'massaman/match'
2
+ import type React from 'react'
3
+
4
+ import { Icon } from './icon'
5
+ import type { ResolvedCardIcon } from './resolve-card-icon'
6
+
7
+ interface CardIconProps {
8
+ readonly resolved: ResolvedCardIcon
9
+ readonly className?: string
10
+ }
11
+
12
+ /**
13
+ * Card-surface icon renderer. Branches on the discriminated
14
+ * `ResolvedCardIcon`:
15
+ *
16
+ * - `kind: 'iconify'` → `<Icon icon={id} />` wrapped in a colour swatch.
17
+ * - `kind: 'image'` → `<img src alt>` painted at the same chip size
18
+ * without colour tinting (the image carries its own art direction).
19
+ *
20
+ * @param props - Resolved icon + optional className applied to the swatch wrapper
21
+ * @returns Branded icon element
22
+ */
23
+ export function CardIcon(props: CardIconProps): React.ReactElement {
24
+ const wrapperClass = props.className ?? 'cp-card__icon'
25
+ return match(props.resolved)
26
+ .with({ kind: 'iconify' }, (r) => (
27
+ <span className={`${wrapperClass} ${wrapperClass}--${r.color}`}>
28
+ <Icon icon={r.id} />
29
+ </span>
30
+ ))
31
+ .with({ kind: 'image' }, (r) => (
32
+ <span className={`${wrapperClass} ${wrapperClass}--image`}>
33
+ <img src={r.src} alt={r.alt} className={`${wrapperClass}-img`} />
34
+ </span>
35
+ ))
36
+ .exhaustive()
37
+ }
@@ -1,5 +1,9 @@
1
1
  import { BRAND_COLORS, DEFAULT_THEME_NAME } from '@ciderpress/theme'
2
- import type React from 'react'
2
+ // Value import (not `import type`) Rslib emits the classic JSX runtime
3
+ // (`React.createElement(...)`), so the `React` binding must survive
4
+ // compilation. With a type-only import, the bundle throws `React is not
5
+ // defined` at runtime.
6
+ import React from 'react'
3
7
 
4
8
  const FONT_STACK = "'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace"
5
9
  /**
@@ -1,28 +1,69 @@
1
+ import type { SerializedIcon } from '@ciderpress/config'
2
+ import { match } from 'massaman/match'
3
+
4
+ /**
5
+ * Render-ready image icon — `src` plus `alt`.
6
+ */
7
+ export interface ResolvedCardImageIcon {
8
+ readonly kind: 'image'
9
+ readonly src: string
10
+ readonly alt: string
11
+ }
12
+
1
13
  /**
2
- * Resolved icon with id and color.
14
+ * Render-ready Iconify icon `id` plus a colour-rotation key.
3
15
  */
4
- export interface ResolvedCardIcon {
16
+ export interface ResolvedCardIconifyIcon {
17
+ readonly kind: 'iconify'
5
18
  readonly id: string
6
19
  readonly color: string
7
20
  }
8
21
 
9
22
  /**
10
- * Resolve a unified icon config into id + color.
23
+ * Discriminated union of resolved card-icon variants. Render sites
24
+ * switch on `kind` to decide between an Iconify `<Icon>` and an `<img>`.
25
+ */
26
+ export type ResolvedCardIcon = ResolvedCardIconifyIcon | ResolvedCardImageIcon
27
+
28
+ /**
29
+ * Card-icon input shape — the `SerializedIcon` shape emitted by the
30
+ * sync engine (workspaces, sections, features), re-exported here under
31
+ * a local alias to keep render-site call signatures readable. Always
32
+ * comes from `@ciderpress/config` — there is no separate source of
33
+ * truth.
34
+ */
35
+ export type CardIconInput = SerializedIcon | undefined
36
+
37
+ /**
38
+ * Resolve a serialized icon value into a discriminated icon ready for
39
+ * render.
11
40
  *
12
- * String icons get the default `'purple'` color. Object icons pass through.
13
- * Returns `undefined` for undefined input.
41
+ * - `string` `{ kind: 'iconify', id, color: 'purple' }`
42
+ * - `{ id, color }` `{ kind: 'iconify', id, color }`
43
+ * - `{ kind: 'image', src, alt }` → pass-through
44
+ * - `undefined` → `undefined`
14
45
  *
15
- * @param icon - Icon config (string, object, or undefined)
16
- * @returns Resolved icon or undefined
46
+ * @param icon - Serialized icon value or `undefined`
47
+ * @returns Discriminated resolved icon, or `undefined`
17
48
  */
18
- export function resolveCardIcon(
19
- icon: string | { readonly id: string; readonly color: string } | undefined
20
- ): ResolvedCardIcon | undefined {
49
+ export function resolveCardIcon(icon: CardIconInput): ResolvedCardIcon | undefined {
21
50
  if (icon === undefined) {
22
51
  return undefined
23
52
  }
24
53
  if (typeof icon === 'string') {
25
- return { id: icon, color: 'purple' }
54
+ return { kind: 'iconify', id: icon, color: 'purple' }
26
55
  }
27
- return icon
56
+ // Match on the discriminant value rather than `'kind' in icon` so a
57
+ // future variant that also carries a `kind` field (or a malformed
58
+ // runtime input) can't silently slip into the image branch.
59
+ return match(icon)
60
+ .with({ kind: 'image' }, (img) => ({
61
+ kind: 'image' as const,
62
+ src: img.src,
63
+ // Defence-in-depth: even if a downstream caller hand-builds the
64
+ // object and forgets `alt`, fall back to an empty string so React
65
+ // never sees `alt={undefined}` (which silences screen readers).
66
+ alt: img.alt ?? '',
67
+ }))
68
+ .otherwise((i) => ({ kind: 'iconify' as const, id: i.id, color: i.color }))
28
69
  }
@@ -2,14 +2,15 @@ import { match, P } from 'massaman/match'
2
2
  import type React from 'react'
3
3
 
4
4
  import { Card } from './card'
5
- import { Icon } from './icon'
5
+ import { CardIcon } from './card-icon'
6
+ import type { CardIconInput } from './resolve-card-icon'
6
7
  import { resolveCardIcon } from './resolve-card-icon'
7
8
 
8
9
  export interface SectionCardProps {
9
10
  readonly href: string
10
11
  readonly title: string
11
12
  readonly description?: string
12
- readonly icon?: string | { readonly id: string; readonly color: string }
13
+ readonly icon?: CardIconInput
13
14
  }
14
15
 
15
16
  /**
@@ -25,7 +26,11 @@ export function SectionCard({
25
26
  description,
26
27
  icon = 'pixelarticons:file',
27
28
  }: SectionCardProps): React.ReactElement {
28
- const resolved = resolveCardIcon(icon) ?? { id: 'pixelarticons:file', color: 'purple' }
29
+ const resolved = resolveCardIcon(icon) ?? {
30
+ kind: 'iconify' as const,
31
+ id: 'pixelarticons:file',
32
+ color: 'purple',
33
+ }
29
34
  const descEl = match(description)
30
35
  .with(P.nonNullable, (d) => <span className="cp-section-card__desc">{d}</span>)
31
36
  .otherwise(() => null)
@@ -33,9 +38,7 @@ export function SectionCard({
33
38
  return (
34
39
  <Card href={href} className="cp-section-card">
35
40
  <div className="cp-section-card__header">
36
- <span className={`cp-section-card__icon cp-section-card__icon--${resolved.color}`}>
37
- <Icon icon={resolved.id} />
38
- </span>
41
+ <CardIcon resolved={resolved} className="cp-section-card__icon" />
39
42
  <span className="cp-section-card__title">{title}</span>
40
43
  </div>
41
44
  {descEl}
@@ -1,3 +1,4 @@
1
+ import type { SerializedIcon } from '@ciderpress/config'
1
2
  import { Link } from '@rspress/core/runtime'
2
3
  import { match, P } from 'massaman/match'
3
4
  import type React from 'react'
@@ -10,7 +11,7 @@ import './sidebar-links.css'
10
11
  interface SidebarLinkItem {
11
12
  readonly text: string
12
13
  readonly link: string
13
- readonly icon?: string | { readonly id: string; readonly color: string }
14
+ readonly icon?: SerializedIcon
14
15
  readonly style?: 'brand' | 'alt' | 'ghost'
15
16
  readonly shape?: 'square' | 'rounded' | 'circle'
16
17
  }
@@ -51,7 +52,10 @@ export function SidebarLinks(props: SidebarLinksProps): React.ReactElement | nul
51
52
  function renderIcon(icon: SidebarLinkItem['icon']): React.ReactElement | null {
52
53
  return match(icon)
53
54
  .with(P.string, (id) => <Icon icon={id} className="cp-sidebar-link-icon" />)
54
- .with({ id: P.string, color: P.string }, (i) => (
55
+ .with({ kind: 'image' }, (img) => (
56
+ <img src={img.src} alt={img.alt} className="cp-sidebar-link-icon cp-sidebar-link-icon--img" />
57
+ ))
58
+ .with({ id: P.string }, (i) => (
55
59
  <Icon icon={i.id} className="cp-sidebar-link-icon" style={{ color: i.color }} />
56
60
  ))
57
61
  .otherwise(() => null)
@@ -11,6 +11,8 @@ declare const __CIDERPRESS_DEFAULT_VARIANT__: string
11
11
  declare const __CIDERPRESS_THEME_COLORS__: string
12
12
  declare const __CIDERPRESS_THEME_DARK_COLORS__: string
13
13
  declare const __CIDERPRESS_THEME_REGISTRY__: string
14
+ declare const __CIDERPRESS_HAS_USER_FAVICON__: boolean
15
+ declare const __CIDERPRESS_LOADER_MIN_MS__: number
14
16
 
15
17
  interface RegistryEntry {
16
18
  readonly name: string
@@ -26,7 +28,7 @@ type ThemeColorKey = keyof ThemeColors
26
28
 
27
29
  /**
28
30
  * Parsed theme registry — built-in themes plus any user themes from
29
- * `config.themes`. Read from the build-time define so the client bundle
31
+ * `theme.themes`. Read from the build-time define so the client bundle
30
32
  * does not pull `@ciderpress/theme`'s factory + Zod into the runtime path.
31
33
  */
32
34
  const REGISTRY_ENTRIES: readonly RegistryEntry[] = parseRegistry(__CIDERPRESS_THEME_REGISTRY__)
@@ -123,9 +125,11 @@ const COLOR_VAR_MAP: Readonly<Record<ThemeColorKey, readonly string[]>> = Object
123
125
  const ALL_CSS_VARS: readonly string[] = Object.values(COLOR_VAR_MAP).flat()
124
126
 
125
127
  /**
126
- * Minimum time (ms) the loading overlay stays visible before fading out.
128
+ * Minimum time (ms) the loading overlay stays visible before fading
129
+ * out. Resolved at build time from `brand.loader.minDisplayMs` —
130
+ * defaults to 150ms in `packages/ui/src/config.ts`.
127
131
  */
128
- const LOADER_MIN_DISPLAY_MS = 150
132
+ const LOADER_MIN_DISPLAY_MS = __CIDERPRESS_LOADER_MIN_MS__
129
133
 
130
134
  /**
131
135
  * Duration (ms) of the CSS fade-out transition. Must match the
@@ -199,12 +203,14 @@ export function ThemeProvider(): React.ReactElement | null {
199
203
  applyColorOverrides(html, darkColors)
200
204
  }
201
205
 
202
- // Sync the document favicon with the active theme's brand colour.
203
- // Browsers cache static icon assets and ignore CSS, so the only way to
204
- // keep the tab mark in lockstep with the chosen theme is to swap
205
- // `<link rel="icon">` to a data-URI SVG carrying the resolved
206
- // `--cp-c-brand-1`.
207
- syncThemeFavicon(html)
206
+ // Sync the document favicon with the active theme's brand colour
207
+ // only when the user has NOT set their own `favicon` in config. When
208
+ // they have, Rspress already points `<link rel="icon">` at the
209
+ // user's asset, and overwriting it with the themed apple data-URI
210
+ // would silently restore ciderpress branding.
211
+ if (!__CIDERPRESS_HAS_USER_FAVICON__) {
212
+ syncThemeFavicon(html)
213
+ }
208
214
 
209
215
  // Observe `.rp-dark` class changes so Rspress's built-in dark toggle
210
216
  // stays the single source of truth for variant flips. The new variant
@@ -253,8 +259,11 @@ export function ThemeProvider(): React.ReactElement | null {
253
259
  }
254
260
  // Re-sync the favicon — brand colour is constant across variants for
255
261
  // built-in themes, but surface overrides on the dark variant can
256
- // affect the chip background colour we bake into the SVG.
257
- syncThemeFavicon(html)
262
+ // affect the chip background colour we bake into the SVG. Skipped
263
+ // when the user supplied their own favicon (see initial sync above).
264
+ if (!__CIDERPRESS_HAS_USER_FAVICON__) {
265
+ syncThemeFavicon(html)
266
+ }
258
267
  })
259
268
  observer.observe(html, { attributes: true, attributeFilter: ['class'] })
260
269
 
@@ -628,6 +637,13 @@ function dismissLoader(html: HTMLElement): () => void {
628
637
  clearDotsInterval()
629
638
  }, LOADER_MIN_DISPLAY_MS + LOADER_FADE_MS)
630
639
 
640
+ // The forced-dismiss fallback (covering the case where React never
641
+ // hydrates) lives in the inline head script — see `buildHeadScriptBody`
642
+ // in `packages/ui/src/config.ts`. That timer always runs, so a
643
+ // duplicate React-side fallback here would only fire AFTER ThemeProvider
644
+ // has already mounted — at which point the normal fade path above has
645
+ // already won.
646
+
631
647
  return () => {
632
648
  clearTimeout(fadeTimer)
633
649
  clearTimeout(removeTimer)
@@ -2,7 +2,8 @@ import { match, P } from 'massaman/match'
2
2
  import type React from 'react'
3
3
 
4
4
  import { Card } from '../shared/card'
5
- import { Icon } from '../shared/icon'
5
+ import { CardIcon } from '../shared/card-icon'
6
+ import type { CardIconInput } from '../shared/resolve-card-icon'
6
7
  import { resolveCardIcon } from '../shared/resolve-card-icon'
7
8
  import { TechTag } from '../shared/tech-tag'
8
9
 
@@ -16,9 +17,10 @@ export interface WorkspaceCardProps {
16
17
  */
17
18
  readonly href: string
18
19
  /**
19
- * Icon config — Iconify identifier string or `{ id, color }` object.
20
+ * Icon config — Iconify identifier string, `{ id, color }` object, or
21
+ * `{ kind: 'image', src, alt }` for a static image asset.
20
22
  */
21
- readonly icon?: string | { readonly id: string; readonly color: string }
23
+ readonly icon?: CardIconInput
22
24
  /**
23
25
  * Scope prefix shown above the name (e.g. "apps/").
24
26
  */
@@ -67,13 +69,9 @@ export function WorkspaceCard({
67
69
  const resolved = resolveCardIcon(icon)
68
70
 
69
71
  const iconEl = match(resolved)
70
- .with(P.nonNullable, (r) => <Icon icon={r.id} />)
72
+ .with(P.nonNullable, (r) => <CardIcon resolved={r} className="cp-card__icon" />)
71
73
  .otherwise(() => null)
72
74
 
73
- const iconColor = match(resolved)
74
- .with(P.nonNullable, (r) => r.color)
75
- .otherwise(() => 'purple')
76
-
77
75
  const scopeEl = match(scope)
78
76
  .with(
79
77
  P.when((s): s is string => s !== undefined && s.length > 0),
@@ -117,7 +115,7 @@ export function WorkspaceCard({
117
115
  <Card href={href} className="cp-workspace-card">
118
116
  <div className="cp-workspace-card__header">
119
117
  <div className="cp-workspace-card__identity">
120
- <span className={`cp-card__icon cp-card__icon--${iconColor}`}>{iconEl}</span>
118
+ {iconEl}
121
119
  <div className="cp-workspace-card__title">
122
120
  {scopeEl}
123
121
  <span
@@ -1,17 +1,17 @@
1
- import type { FooterConfig, HomeConfig, SiteConfig } from '@ciderpress/config'
1
+ import type { FooterConfig, HomeConfig, SerializedIcon } from '@ciderpress/config'
2
2
  import { useSite } from '@rspress/core/runtime'
3
3
 
4
4
  export interface CiderpressSidebarItem {
5
5
  readonly text?: string
6
6
  readonly link?: string
7
- readonly icon?: string
7
+ readonly icon?: SerializedIcon
8
8
  readonly items?: readonly CiderpressSidebarItem[]
9
9
  }
10
10
 
11
11
  export interface CiderpressSidebarLink {
12
12
  readonly text: string
13
13
  readonly link: string
14
- readonly icon?: string | { readonly id: string; readonly color: string }
14
+ readonly icon?: SerializedIcon
15
15
  readonly style?: 'brand' | 'alt' | 'ghost'
16
16
  readonly shape?: 'square' | 'rounded' | 'circle'
17
17
  }
@@ -19,7 +19,7 @@ export interface CiderpressSidebarLink {
19
19
  export interface WorkspaceCardData {
20
20
  readonly title: string
21
21
  readonly href: string
22
- readonly icon: string | { readonly id: string; readonly color: string } | undefined
22
+ readonly icon: SerializedIcon | undefined
23
23
  readonly scope: string | undefined
24
24
  readonly description: string | undefined
25
25
  readonly tags: readonly string[]
@@ -33,6 +33,61 @@ export interface WorkspaceGroupData {
33
33
  readonly cards: readonly WorkspaceCardData[]
34
34
  }
35
35
 
36
+ /**
37
+ * Serialised view of the unified `CiderpressConfig` that the runtime
38
+ * theme components still read through. The `site` field is rebuilt in
39
+ * `packages/ui/src/config.ts` (`buildSiteBlock`) from the new top-level
40
+ * `editLink` / `reportLink` / `topbar` / `sidebar` / `footer` / `version`
41
+ * config — kept under the legacy `site.*` name so theme components don't
42
+ * need to be rewired to look up each field individually.
43
+ */
44
+ export interface CiderpressSiteBlock {
45
+ readonly version: string | undefined
46
+ readonly edit:
47
+ | {
48
+ readonly repo: string
49
+ readonly branch?: string
50
+ readonly directory?: string
51
+ readonly label?: string
52
+ }
53
+ | undefined
54
+ readonly report:
55
+ | {
56
+ readonly repo: string
57
+ readonly branch?: string
58
+ readonly directory?: string
59
+ readonly label?: string
60
+ }
61
+ | undefined
62
+ readonly topbarCta: { readonly text: string; readonly href: string } | undefined
63
+ readonly sidebarPromo:
64
+ | {
65
+ readonly title: string
66
+ readonly body: string
67
+ readonly cta: { readonly text: string; readonly href: string }
68
+ }
69
+ | undefined
70
+ readonly announcement:
71
+ | {
72
+ readonly id?: string
73
+ readonly lead?: string
74
+ readonly message: string
75
+ readonly cta?: { readonly href: string; readonly label: string }
76
+ readonly persistent?: boolean
77
+ }
78
+ | undefined
79
+ readonly footer:
80
+ | {
81
+ readonly columns?: readonly {
82
+ readonly heading: string
83
+ readonly links: readonly { readonly text: string; readonly href: string }[]
84
+ }[]
85
+ readonly tagline?: string
86
+ readonly brandMark?: string
87
+ }
88
+ | undefined
89
+ }
90
+
36
91
  interface CiderpressThemeConfig {
37
92
  readonly sidebar: Record<string, readonly CiderpressSidebarItem[]>
38
93
  readonly sidebarAbove: readonly CiderpressSidebarLink[] | undefined
@@ -41,7 +96,7 @@ interface CiderpressThemeConfig {
41
96
  readonly standaloneScopePaths: readonly string[] | undefined
42
97
  readonly home: HomeConfig | undefined
43
98
  readonly ciderpressFooter: FooterConfig | undefined
44
- readonly site: SiteConfig | undefined
99
+ readonly site: CiderpressSiteBlock | undefined
45
100
  }
46
101
 
47
102
  /**
@@ -2,8 +2,9 @@ import type { CiderpressSocialLink } from '../components/nav/ciderpress-nav-soci
2
2
 
3
3
  /**
4
4
  * Pull social-link entries (GitHub, npm, etc) from Rspress's `site`
5
- * shape. Lives on `site.themeConfig.socialLinks` after Rspress
6
- * normalisation but isn't surfaced on the public typings.
5
+ * shape. Lives on `site.themeConfig.socialLinks` after the
6
+ * `packages/ui/src/config.ts` serialiser writes the unified `socials`
7
+ * config into the runtime theme config.
7
8
  *
8
9
  * @param site - Rspress site data (typically the result of `useSite().site`)
9
10
  * @returns Array of social-link entries (empty array when not configured)
@@ -22,6 +23,6 @@ export function readSocialLinks(site: unknown): readonly CiderpressSocialLink[]
22
23
  typeof item === 'object' &&
23
24
  item !== null &&
24
25
  typeof (item as { icon?: unknown }).icon === 'string' &&
25
- typeof (item as { content?: unknown }).content === 'string'
26
+ typeof (item as { url?: unknown }).url === 'string'
26
27
  )
27
28
  }
@@ -107,7 +107,7 @@ function buildIconDataUri(brand: string, shadow: string, surface: string): strin
107
107
  /**
108
108
  * Locate the existing `<link rel="icon">` element, creating one if the
109
109
  * document head doesn't have one yet (a defensive fallback — Rspress
110
- * normally renders one from `config.icon`).
110
+ * normally renders one from `brand.favicon`).
111
111
  *
112
112
  * @private
113
113
  * @returns Mutable link element
@@ -0,0 +1,31 @@
1
+ import { withBase } from '@rspress/core/runtime'
2
+
3
+ /**
4
+ * Prefix `value` with the configured Rspress `base` only when it's an
5
+ * absolute path that would otherwise escape the mount. Pass-through for
6
+ * data URIs, fully-qualified URLs, hash anchors, and relative paths —
7
+ * applying `withBase()` to those produces broken results (the kitchen-
8
+ * sink data-URI logo regression that the examples e2e caught).
9
+ *
10
+ * Use this wrapper at every render site that takes a user-supplied asset
11
+ * URL from the ciderpress config (`logo`, `heroDemo.src`, brand chips
12
+ * etc.), instead of calling `withBase()` directly.
13
+ *
14
+ * @param value - Asset URL or path from user config (may be `undefined`)
15
+ * @returns The base-prefixed URL when applicable, otherwise `value` as-is
16
+ */
17
+ export function withMountBase(value: string | undefined): string {
18
+ if (value === undefined) {
19
+ return ''
20
+ }
21
+ if (!value.startsWith('/')) {
22
+ return value
23
+ }
24
+ // Data URIs (`data:image/svg+xml;...`) and protocol-relative or
25
+ // explicit-protocol URLs are absolute in their own namespace — never
26
+ // prefix.
27
+ if (value.startsWith('//') || /^[a-z][a-z0-9+\-.]*:/i.test(value)) {
28
+ return value
29
+ }
30
+ return withBase(value)
31
+ }