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

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 (67) 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/icon.tsx +119 -35
  27. package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
  28. package/dist/theme/components/shared/section-card.tsx +9 -6
  29. package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
  30. package/dist/theme/components/theme-provider.tsx +27 -11
  31. package/dist/theme/components/workspaces/card.tsx +7 -9
  32. package/dist/theme/hooks/use-ciderpress.ts +60 -5
  33. package/dist/theme/lib/read-social-links.ts +4 -3
  34. package/dist/theme/lib/theme-favicon.ts +1 -1
  35. package/dist/theme/lib/with-mount-base.ts +31 -0
  36. package/package.json +13 -13
  37. package/src/theme/components/footer/site-footer.tsx +68 -9
  38. package/src/theme/components/home/feature-card.tsx +5 -8
  39. package/src/theme/components/home/feature.tsx +31 -11
  40. package/src/theme/components/home/hero-demo-custom.tsx +116 -0
  41. package/src/theme/components/home/hero-demo.css +27 -0
  42. package/src/theme/components/home/layout.tsx +149 -26
  43. package/src/theme/components/home/split-visual-custom.tsx +26 -0
  44. package/src/theme/components/home/workspaces.tsx +39 -3
  45. package/src/theme/components/nav/branch-tag.tsx +7 -7
  46. package/src/theme/components/nav/ciderpress-header.css +2 -1
  47. package/src/theme/components/nav/ciderpress-header.tsx +26 -3
  48. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
  49. package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
  50. package/src/theme/components/nav/header-icon.css +24 -0
  51. package/src/theme/components/nav/header-icon.tsx +119 -0
  52. package/src/theme/components/nav/header-logo.css +16 -0
  53. package/src/theme/components/nav/header-logo.tsx +200 -0
  54. package/src/theme/components/nav/layout.tsx +45 -7
  55. package/src/theme/components/nav/nav-logo.tsx +5 -1
  56. package/src/theme/components/shared/card-icon.tsx +37 -0
  57. package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
  58. package/src/theme/components/shared/icon.tsx +119 -35
  59. package/src/theme/components/shared/resolve-card-icon.ts +53 -12
  60. package/src/theme/components/shared/section-card.tsx +9 -6
  61. package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
  62. package/src/theme/components/theme-provider.tsx +27 -11
  63. package/src/theme/components/workspaces/card.tsx +7 -9
  64. package/src/theme/hooks/use-ciderpress.ts +60 -5
  65. package/src/theme/lib/read-social-links.ts +4 -3
  66. package/src/theme/lib/theme-favicon.ts +1 -1
  67. package/src/theme/lib/with-mount-base.ts +31 -0
@@ -1,6 +1,13 @@
1
+ import type {
2
+ ButtonConfig,
3
+ HomeHeroDemoConfig,
4
+ HomeSectionId,
5
+ HomeSplitConfig,
6
+ } from '@ciderpress/config'
7
+ import { DEFAULT_HOME_LAYOUT } from '@ciderpress/config'
1
8
  import { useFrontmatter } from '@rspress/core/runtime'
2
- import { match } from 'massaman/match'
3
- import type React from 'react'
9
+ import { match, P } from 'massaman/match'
10
+ import React from 'react'
4
11
 
5
12
  import { SiteFooter } from '../footer/site-footer'
6
13
  import { CTA } from './cta'
@@ -8,8 +15,10 @@ import { HomeFeature } from './feature'
8
15
  import { Hero } from './hero'
9
16
  import type { HeroAction } from './hero'
10
17
  import { HeroDemo } from './hero-demo'
18
+ import { CustomHeroDemo } from './hero-demo-custom'
11
19
  import { PageRail } from './page-rail'
12
20
  import { HomeSplit } from './split'
21
+ import { CustomSplitVisual } from './split-visual-custom'
13
22
  import { TrustStrip } from './trust-strip'
14
23
  import { HomeWorkspaces } from './workspaces'
15
24
 
@@ -24,11 +33,11 @@ interface FrontmatterHero {
24
33
  readonly name?: string
25
34
  readonly text?: string
26
35
  readonly tagline?: string
27
- readonly actions?: readonly HeroAction[]
28
- readonly eyebrow?: string
36
+ readonly actions?: readonly ButtonConfig[]
37
+ readonly label?: string
29
38
  }
30
39
 
31
- interface FrontmatterTrust {
40
+ interface FrontmatterProof {
32
41
  readonly lead?: string
33
42
  readonly names?: readonly string[]
34
43
  }
@@ -36,7 +45,7 @@ interface FrontmatterTrust {
36
45
  interface FrontmatterCTA {
37
46
  readonly title?: string
38
47
  readonly subtitle?: string
39
- readonly actions?: readonly HeroAction[]
48
+ readonly actions?: readonly ButtonConfig[]
40
49
  }
41
50
 
42
51
  /**
@@ -52,25 +61,48 @@ interface FrontmatterCTA {
52
61
  */
53
62
  export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
54
63
  const { frontmatter } = useFrontmatter()
64
+
65
+ // SSG-MD short-circuit: the React tree is walked by
66
+ // `react-render-to-markdown` to produce `.md` files served to
67
+ // `<LlmsCopyButton />`. Rendering the home shell would dump hero
68
+ // text, demo blocks, and footer columns into every copied page.
69
+ // Return an empty fragment for SSG-MD — the home page has no
70
+ // canonical markdown body. The `useFrontmatter` hook above runs in
71
+ // both passes so hook order stays consistent.
72
+ if (import.meta.env.SSG_MD) {
73
+ return <></>
74
+ }
75
+
55
76
  const fm = frontmatter as Record<string, unknown>
56
77
 
57
78
  const hero = fm.hero as FrontmatterHero | undefined
58
- const trust = fm.trust as FrontmatterTrust | undefined
79
+ const proof = fm.proof as FrontmatterProof | undefined
59
80
  const cta = fm.cta as FrontmatterCTA | undefined
81
+ // heroDemo / split frontmatter:
82
+ // undefined → render the framework default
83
+ // false → suppress entirely
84
+ // object → render the user-supplied custom variant
85
+ const heroDemoFm = fm.heroDemo as false | HomeHeroDemoConfig | undefined
86
+ const splitFm = fm.split as false | HomeSplitConfig | undefined
87
+
88
+ const heroDemoEl = match(heroDemoFm)
89
+ .with(false, () => null)
90
+ .with(undefined, () => <HeroDemo />)
91
+ .otherwise((d) => <CustomHeroDemo config={d} />)
60
92
 
61
93
  const heroSection = match(hero)
62
94
  .with(undefined, () => null)
63
95
  .otherwise((h) => (
64
96
  <Hero
65
- eyebrow={h.eyebrow}
97
+ eyebrow={h.label}
66
98
  title={renderTitle(h.text ?? h.name ?? '')}
67
99
  tagline={h.tagline}
68
- actions={h.actions}
69
- demo={<HeroDemo />}
100
+ actions={mapButtonsToHeroActions(h.actions)}
101
+ demo={heroDemoEl}
70
102
  />
71
103
  ))
72
104
 
73
- const trustSection = match(trust)
105
+ const proofSection = match(proof)
74
106
  .with(undefined, () => null)
75
107
  .otherwise((t) => {
76
108
  const names = t.names ?? []
@@ -84,18 +116,18 @@ export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
84
116
  .otherwise((c) =>
85
117
  match(c.title === undefined)
86
118
  .with(true, () => null)
87
- .otherwise(() => <CTA title={c.title ?? ''} subtitle={c.subtitle} actions={c.actions} />)
119
+ .otherwise(() => (
120
+ <CTA
121
+ title={c.title ?? ''}
122
+ subtitle={c.subtitle}
123
+ actions={mapButtonsToHeroActions(c.actions)}
124
+ />
125
+ ))
88
126
  )
89
127
 
90
- return (
91
- <PageRail>
92
- {props.beforeHero}
93
- {heroSection}
94
- {props.afterHero}
95
- {trustSection}
96
- {props.beforeFeatures}
97
- <HomeFeature />
98
- {props.afterFeatures}
128
+ const splitSection = match(splitFm)
129
+ .with(false, () => null)
130
+ .with(undefined, () => (
99
131
  <HomeSplit
100
132
  eyebrow="Configuration"
101
133
  title="One file. Validated. Type-safe."
@@ -109,8 +141,57 @@ export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
109
141
  action={{ theme: 'brand', text: 'Read the docs', link: '/getting-started/quick-start' }}
110
142
  visual={<ConfigPreview />}
111
143
  />
112
- <HomeWorkspaces />
113
- {ctaSection}
144
+ ))
145
+ .with(P.nonNullable, (s) => (
146
+ <HomeSplit
147
+ eyebrow={s.label}
148
+ title={s.title}
149
+ body={s.body}
150
+ bullets={s.bullets ?? []}
151
+ action={match(s.cta)
152
+ .with(undefined, () => undefined)
153
+ .otherwise((c) => ({ theme: 'brand' as const, text: c.text, link: c.href }))}
154
+ visual={match(s.visual)
155
+ .with(undefined, () => null)
156
+ .otherwise((v) => (
157
+ <CustomSplitVisual code={v.code} language={v.language} />
158
+ ))}
159
+ />
160
+ ))
161
+ .exhaustive()
162
+
163
+ // Section render order. `home.layout` (when provided) controls both
164
+ // order and visibility — sections omitted from the array are not
165
+ // rendered. The framework default (`DEFAULT_HOME_LAYOUT`) preserves
166
+ // the historical fixed order. Unknown ids are ignored at render time
167
+ // (the schema rejects them at config-load anyway).
168
+ const layout = (fm.layout as readonly HomeSectionId[] | undefined) ?? DEFAULT_HOME_LAYOUT
169
+ const sectionsById: Readonly<Record<HomeSectionId, React.ReactNode>> = {
170
+ hero: (
171
+ <>
172
+ {props.beforeHero}
173
+ {heroSection}
174
+ {props.afterHero}
175
+ </>
176
+ ),
177
+ proof: proofSection,
178
+ features: (
179
+ <>
180
+ {props.beforeFeatures}
181
+ <HomeFeature />
182
+ {props.afterFeatures}
183
+ </>
184
+ ),
185
+ split: splitSection,
186
+ showcase: <HomeWorkspaces />,
187
+ cta: ctaSection,
188
+ }
189
+
190
+ return (
191
+ <PageRail>
192
+ {layout.map((id) => (
193
+ <React.Fragment key={id}>{sectionsById[id]}</React.Fragment>
194
+ ))}
114
195
  <SiteFooter />
115
196
  </PageRail>
116
197
  )
@@ -134,16 +215,16 @@ function ConfigPreview(): React.ReactElement {
134
215
  {' title: '}
135
216
  <span className="tok-str">'Acme Docs'</span>
136
217
  {',\n'}
137
- {' sections: [\n'}
218
+ {' pages: [\n'}
138
219
  {' { title: '}
139
220
  <span className="tok-str">'Guides'</span>
140
221
  {', include: '}
141
222
  <span className="tok-str">'docs/guides/*.md'</span>
142
223
  {' },\n'}
143
224
  {' ],\n'}
144
- {' theme: { name: '}
225
+ {' theme: { themes: ['}
145
226
  <span className="tok-str">'mulled'</span>
146
- {' },\n'}
227
+ {'] },\n'}
147
228
  {'})'}
148
229
  </pre>
149
230
  )
@@ -181,3 +262,45 @@ function renderTitle(raw: string): React.ReactNode {
181
262
  })
182
263
  })
183
264
  }
265
+
266
+ /**
267
+ * Map the unified `ButtonConfig[]` shape (used by the new `home.hero`
268
+ * / `home.cta` configs) into the legacy `HeroAction[]` shape still
269
+ * consumed by `<Hero />` and `<CTA />`. `'primary'` → `'brand'`,
270
+ * `'secondary' | 'ghost'` → `'alt'`, `undefined` → `undefined`.
271
+ *
272
+ * @private
273
+ * @param actions - Optional list of unified button configs from frontmatter
274
+ * @returns Hero-action array consumed by the existing component API
275
+ */
276
+ function mapButtonsToHeroActions(
277
+ actions: readonly ButtonConfig[] | undefined
278
+ ): readonly HeroAction[] | undefined {
279
+ if (actions === undefined) {
280
+ return undefined
281
+ }
282
+ return actions.map((action) => ({
283
+ text: action.text,
284
+ link: action.href,
285
+ theme: mapButtonVariantToHeroTheme(action.variant),
286
+ }))
287
+ }
288
+
289
+ /**
290
+ * Project the new `'primary' | 'secondary' | 'ghost'` variant token
291
+ * back into the legacy `'brand' | 'alt'` token that `<Hero />` accepts.
292
+ *
293
+ * @private
294
+ * @param variant - Optional variant from the unified button config
295
+ * @returns `'brand'`, `'alt'`, or `undefined`
296
+ */
297
+ function mapButtonVariantToHeroTheme(
298
+ variant: ButtonConfig['variant']
299
+ ): 'brand' | 'alt' | undefined {
300
+ return match(variant)
301
+ .with(undefined, () => undefined)
302
+ .with('primary', () => 'brand' as const)
303
+ .with('secondary', () => 'alt' as const)
304
+ .with('ghost', () => 'alt' as const)
305
+ .exhaustive()
306
+ }
@@ -0,0 +1,26 @@
1
+ import type React from 'react'
2
+
3
+ interface CustomSplitVisualProps {
4
+ readonly code: string
5
+ readonly language?: string
6
+ }
7
+
8
+ /**
9
+ * Custom Split visual — renders a user-supplied code snippet inside the
10
+ * same `<pre>` shell as the default `ConfigPreview`. Language hint is
11
+ * surfaced as a `data-language` attribute so themes can hook into it
12
+ * for syntax-highlighter overlays. No client-side highlighting is
13
+ * performed in the framework (keeps the bundle slim); the rendered
14
+ * `<pre>` matches the default surface so themes that style the
15
+ * default block also style the custom one.
16
+ *
17
+ * @param props - Code snippet + optional language hint
18
+ * @returns Pre-formatted code block
19
+ */
20
+ export function CustomSplitVisual(props: CustomSplitVisualProps): React.ReactElement {
21
+ return (
22
+ <pre data-language={props.language ?? 'ts'} className="cp-split__code">
23
+ {props.code}
24
+ </pre>
25
+ )
26
+ }
@@ -1,4 +1,5 @@
1
- import type { HomeGridConfig } from '@ciderpress/config'
1
+ import type { HomeShowcaseConfig } from '@ciderpress/config'
2
+ import { useFrontmatter } from '@rspress/core/runtime'
2
3
  import { match, P } from 'massaman/match'
3
4
  import type React from 'react'
4
5
 
@@ -7,15 +8,29 @@ import type { WorkspaceGroupData } from '../../hooks/use-ciderpress'
7
8
  import { WorkspaceCard } from '../workspaces/card'
8
9
  import { WorkspaceGrid } from '../workspaces/grid'
9
10
 
11
+ interface FrontmatterWorkspacesHeading {
12
+ readonly label?: string
13
+ readonly title?: string
14
+ readonly subtitle?: string
15
+ }
16
+
10
17
  /**
11
18
  * Smart orchestrator that reads workspace data from themeConfig
12
19
  * and renders workspace groups with the correct card component per type.
13
20
  *
21
+ * Optionally renders a top-level heading above the workspace groups
22
+ * when `home.showcase.heading` is configured. The sync engine writes
23
+ * it into frontmatter as `workspacesHeading`.
24
+ *
14
25
  * @returns React element with workspace groups or null
15
26
  */
16
27
  export function HomeWorkspaces(): React.ReactElement | null {
17
28
  const { workspaces, home } = useCiderpress()
18
- const gridConfig = home && home.workspaces
29
+ const gridConfig = home && home.showcase
30
+ const { frontmatter } = useFrontmatter()
31
+ const heading = (frontmatter as Record<string, unknown>).workspacesHeading as
32
+ | FrontmatterWorkspacesHeading
33
+ | undefined
19
34
 
20
35
  return match(workspaces)
21
36
  .with(
@@ -23,6 +38,27 @@ export function HomeWorkspaces(): React.ReactElement | null {
23
38
  (groups) => (
24
39
  <div className="cp-workspace-section">
25
40
  <hr className="cp-divider" />
41
+ {match(heading)
42
+ .with(undefined, () => null)
43
+ .otherwise((h) => (
44
+ <div className="cp-feature-section-head">
45
+ {match(h.label)
46
+ .with(undefined, () => null)
47
+ .otherwise((e) => (
48
+ <div className="cp-feature-section-head__eyebrow">{e}</div>
49
+ ))}
50
+ {match(h.title)
51
+ .with(undefined, () => null)
52
+ .otherwise((t) => (
53
+ <h2 className="cp-feature-section-head__title">{t}</h2>
54
+ ))}
55
+ {match(h.subtitle)
56
+ .with(undefined, () => null)
57
+ .otherwise((s) => (
58
+ <p className="cp-feature-section-head__sub">{s}</p>
59
+ ))}
60
+ </div>
61
+ ))}
26
62
  {groups.map((group) => renderGroup(group, gridConfig))}
27
63
  </div>
28
64
  )
@@ -40,7 +76,7 @@ export function HomeWorkspaces(): React.ReactElement | null {
40
76
  */
41
77
  function renderGroup(
42
78
  group: WorkspaceGroupData,
43
- gridConfig: HomeGridConfig | undefined
79
+ gridConfig: HomeShowcaseConfig | undefined
44
80
  ): React.ReactElement {
45
81
  const titleLines = gridConfig && gridConfig.truncate && gridConfig.truncate.title
46
82
  const descLines = gridConfig && gridConfig.truncate && gridConfig.truncate.description
@@ -1,7 +1,7 @@
1
- import type { SiteConfig } from '@ciderpress/config'
2
1
  import { match, P } from 'massaman/match'
3
2
  import type React from 'react'
4
3
 
4
+ import type { CiderpressSiteBlock } from '../../hooks/use-ciderpress'
5
5
  import { useCiderpress } from '../../hooks/use-ciderpress'
6
6
 
7
7
  import './branch-tag.css'
@@ -12,7 +12,7 @@ declare const __CIDERPRESS_GIT_BRANCH__: string | undefined
12
12
  /**
13
13
  * Git branch tag — pill-shaped badge rendered via the `beforeNavMenu`
14
14
  * layout slot. Hidden when on default branches (`main` or `master`) or
15
- * when no `site.edit.repo` is configured. Uses the pixelarticons:git-branch
15
+ * when no `editLink.repo` is configured. Uses the pixelarticons:git-branch
16
16
  * icon.
17
17
  *
18
18
  * @returns React element, or `null` when on a default branch / unconfigured.
@@ -61,7 +61,7 @@ function resolveBranch(): string {
61
61
  }
62
62
 
63
63
  /**
64
- * Build a GitHub `/tree/<branch>` URL from the configured `site.edit.repo`.
64
+ * Build a GitHub `/tree/<branch>` URL from the configured `editLink.repo`.
65
65
  *
66
66
  * Accepts either a full URL (used as-is) or an `org/repo` slug
67
67
  * (prefixed with `https://github.com/`). Returns `null` when `repo` is
@@ -83,14 +83,14 @@ function buildBranchHref(params: {
83
83
  }
84
84
 
85
85
  /**
86
- * Pull the configured edit-repo slug off the site config, expressed
87
- * with explicit null checks rather than optional chaining.
86
+ * Pull the configured edit-repo slug off the serialised site block,
87
+ * expressed with explicit null checks rather than optional chaining.
88
88
  *
89
89
  * @private
90
- * @param site - The ciderpress site config (may be undefined).
90
+ * @param site - The runtime site block (may be undefined).
91
91
  * @returns The configured `edit.repo` value or `undefined`.
92
92
  */
93
- function resolveRepo(site: SiteConfig | undefined): string | undefined {
93
+ function resolveRepo(site: CiderpressSiteBlock | undefined): string | undefined {
94
94
  if (site === undefined) {
95
95
  return undefined
96
96
  }
@@ -76,7 +76,8 @@
76
76
  text-decoration: none;
77
77
  }
78
78
 
79
- .cp-header-logo > svg {
79
+ .cp-header-logo > svg,
80
+ .cp-header-logo > img {
80
81
  height: var(--cp-header-logo-height, 28px);
81
82
  width: auto;
82
83
  }
@@ -2,19 +2,29 @@ import { clsx } from 'clsx'
2
2
  import type React from 'react'
3
3
 
4
4
  import { RouteLink } from '../../lib/route-link.tsx'
5
- import { CiderpressLogo } from '../shared/ciderpress-logo'
6
5
  import { CiderpressNavHamburger } from './ciderpress-nav-hamburger'
7
6
  import { CiderpressNavMenu } from './ciderpress-nav-menu'
8
7
  import type { CiderpressNavMenuItem } from './ciderpress-nav-menu'
9
8
  import { CiderpressNavSearch } from './ciderpress-nav-search'
10
9
  import { CiderpressNavSocialLinks } from './ciderpress-nav-social-links'
11
10
  import type { CiderpressSocialLink } from './ciderpress-nav-social-links'
11
+ import { HeaderIcon } from './header-icon'
12
+ import { HeaderLogo } from './header-logo'
12
13
  import { NavDivider } from './nav-divider'
13
14
  import { TopbarCTA } from './topbar-cta'
14
15
  import { VariantToggle } from './variant-toggle'
15
16
 
16
17
  import './ciderpress-header.css'
17
18
 
19
+ /**
20
+ * Build-time flag: `true` when the active theme declares more than one
21
+ * variant AND `data-cp-variants` won't collapse the `<VariantToggle />`
22
+ * to `display: none`. Drives whether the trailing-cluster divider is
23
+ * rendered — when the toggle is hidden and neither socials nor CTA
24
+ * follow, the divider has nothing to separate and is omitted.
25
+ */
26
+ declare const __CIDERPRESS_HAS_VARIANT_TOGGLE__: boolean
27
+
18
28
  /**
19
29
  * Props for the ciderpress site header.
20
30
  */
@@ -57,6 +67,18 @@ export interface CiderpressHeaderProps {
57
67
  * @returns Sticky header element
58
68
  */
59
69
  export function CiderpressHeader(props: CiderpressHeaderProps): React.ReactElement {
70
+ // Dividers only render when they actually separate two visible
71
+ // clusters — orphan dividers (nothing on the right) are visually
72
+ // noisy. `<VariantToggle />` is hidden by CSS for single-variant
73
+ // themes; the build-time flag captures that decision so the React
74
+ // tree matches the painted output. The search/menu inner divider is
75
+ // unconditional — both clusters always render some chrome, and
76
+ // gating on `navItems.length` would flip after hydration (Rspress's
77
+ // nav data isn't populated at SSG) and produce a hydration warning.
78
+ const hasSocials = props.socialLinks.length > 0
79
+ const hasCta = props.topbarCta !== undefined
80
+ const hasTrailingCluster = __CIDERPRESS_HAS_VARIANT_TOGGLE__ || hasSocials || hasCta
81
+
60
82
  // Landing / home pages live inside a constrained max-width container;
61
83
  // doc pages stretch full-width (sidebar + article). The header mirrors
62
84
  // the content shell so it never floats outside the page rhythm.
@@ -78,7 +100,8 @@ export function CiderpressHeader(props: CiderpressHeaderProps): React.ReactEleme
78
100
  topbarCta={props.topbarCta}
79
101
  />
80
102
  <RouteLink href="/" className="cp-header-logo" aria-label="Home">
81
- <CiderpressLogo />
103
+ <HeaderIcon />
104
+ <HeaderLogo />
82
105
  </RouteLink>
83
106
 
84
107
  {/* Right cluster wrapper. Grows from a zero basis so its width
@@ -96,7 +119,7 @@ export function CiderpressHeader(props: CiderpressHeaderProps): React.ReactEleme
96
119
  <CiderpressNavMenu items={props.navItems} />
97
120
  </div>
98
121
 
99
- <NavDivider />
122
+ {hasTrailingCluster && <NavDivider />}
100
123
  <VariantToggle />
101
124
  <CiderpressNavSocialLinks links={props.socialLinks} />
102
125
  {props.topbarCta !== undefined && (
@@ -6,13 +6,15 @@ import { Icon } from '../shared/icon.tsx'
6
6
  import './ciderpress-nav-social-links.css'
7
7
 
8
8
  /**
9
- * Single social-link entry — matches the shape of `socialLinks` in
10
- * `ciderpress.config.ts`.
9
+ * Single social-link entry — matches the serialised `socials` shape
10
+ * written into `themeConfig.socialLinks` by `packages/ui/src/config.ts`.
11
+ *
12
+ * The Rspress `mode` / `content` discriminator was killed in the
13
+ * `rc.4` API overhaul; every link is now a plain anchor.
11
14
  */
12
15
  export interface CiderpressSocialLink {
13
16
  readonly icon: string
14
- readonly mode: string
15
- readonly content: string
17
+ readonly url: string
16
18
  readonly label?: string
17
19
  }
18
20
 
@@ -39,7 +41,7 @@ const ICON_MAP: Readonly<Record<string, string>> = Object.freeze({
39
41
  * Renders the configured social links as a cluster of icon buttons in
40
42
  * the topbar. Returns `null` when no links are configured.
41
43
  *
42
- * @param props - List of social links from `site.socialLinks`
44
+ * @param props - List of social links from `site.socials`
43
45
  * @returns Cluster of icon links, or `null`
44
46
  */
45
47
  export function CiderpressNavSocialLinks(
@@ -50,7 +52,7 @@ export function CiderpressNavSocialLinks(
50
52
  }
51
53
 
52
54
  const safeLinks = props.links
53
- .map((link) => ({ link, href: safeUrl(link.content) }))
55
+ .map((link) => ({ link, href: safeUrl(link.url) }))
54
56
  .filter((entry): entry is { readonly link: CiderpressSocialLink; readonly href: string } => {
55
57
  if (entry.href === null) {
56
58
  return false
@@ -66,7 +68,7 @@ export function CiderpressNavSocialLinks(
66
68
  <div className="cp-nav-social">
67
69
  {safeLinks.map(({ link, href }) => (
68
70
  <a
69
- key={link.content}
71
+ key={link.url}
70
72
  href={href}
71
73
  target="_blank"
72
74
  rel="noopener noreferrer"
@@ -1,7 +1,7 @@
1
- import type { SiteConfig } from '@ciderpress/config'
2
1
  import { match, P } from 'massaman/match'
3
2
  import type React from 'react'
4
3
 
4
+ import type { CiderpressSiteBlock } from '../../hooks/use-ciderpress'
5
5
  import { useCiderpress } from '../../hooks/use-ciderpress'
6
6
 
7
7
  import './floating-branch-indicator.css'
@@ -15,7 +15,7 @@ declare const __CIDERPRESS_GIT_BRANCH__: string | undefined
15
15
  * the branch name. Click anywhere on the pill opens the branch on GitHub.
16
16
  *
17
17
  * Hidden when on default branches (`main` / `master`), when the
18
- * build-time branch global is undefined, or when `site.edit.repo` is
18
+ * build-time branch global is undefined, or when `editLink.repo` is
19
19
  * not configured.
20
20
  *
21
21
  * @returns React element or null when hidden
@@ -69,7 +69,7 @@ function resolveBranch(): string {
69
69
  }
70
70
 
71
71
  /**
72
- * Build a GitHub `/tree/<branch>` URL from the configured `site.edit.repo`.
72
+ * Build a GitHub `/tree/<branch>` URL from the configured `editLink.repo`.
73
73
  *
74
74
  * Accepts either a full URL (used as-is) or an `org/repo` slug
75
75
  * (prefixed with `https://github.com/`). Returns `null` when `repo` is
@@ -91,14 +91,14 @@ function buildBranchHref(params: {
91
91
  }
92
92
 
93
93
  /**
94
- * Pull the configured edit-repo slug off the site config, expressed
95
- * with explicit null checks rather than optional chaining.
94
+ * Pull the configured edit-repo slug off the serialised site block,
95
+ * expressed with explicit null checks rather than optional chaining.
96
96
  *
97
97
  * @private
98
- * @param site - The ciderpress site config (may be undefined).
98
+ * @param site - The runtime site block (may be undefined).
99
99
  * @returns The configured `edit.repo` value or `undefined`.
100
100
  */
101
- function resolveRepo(site: SiteConfig | undefined): string | undefined {
101
+ function resolveRepo(site: CiderpressSiteBlock | undefined): string | undefined {
102
102
  if (site === undefined) {
103
103
  return undefined
104
104
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Small brand chip rendered immediately before `<HeaderLogo />` inside
3
+ * `cp-header-logo`. Sized to match `--cp-header-icon-size` (default
4
+ * 28px — same as the logo height token) so the two slots line up
5
+ * vertically by default. Themes can override either token to break
6
+ * the lockstep.
7
+ */
8
+
9
+ .cp-header-icon {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ flex-shrink: 0;
13
+ height: var(--cp-header-icon-size, var(--cp-header-logo-height, 28px));
14
+ width: var(--cp-header-icon-size, var(--cp-header-logo-height, 28px));
15
+ margin-right: var(--cp-header-icon-gap, 8px);
16
+ color: var(--cp-header-icon-color, currentColor);
17
+ }
18
+
19
+ .cp-header-icon > svg,
20
+ .cp-header-icon__img {
21
+ height: 100%;
22
+ width: 100%;
23
+ display: block;
24
+ }