@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.10

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 (106) 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 +335 -51
  5. package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
  6. package/dist/theme/components/footer/site-footer.tsx +68 -9
  7. package/dist/theme/components/home/feature-card.tsx +5 -8
  8. package/dist/theme/components/home/feature.tsx +31 -11
  9. package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
  10. package/dist/theme/components/home/hero-demo.css +27 -0
  11. package/dist/theme/components/home/layout.tsx +149 -26
  12. package/dist/theme/components/home/split-visual-custom.tsx +26 -0
  13. package/dist/theme/components/home/workspaces.tsx +39 -3
  14. package/dist/theme/components/nav/branch-tag.tsx +7 -7
  15. package/dist/theme/components/nav/ciderpress-docs-bar.css +13 -1
  16. package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  17. package/dist/theme/components/nav/ciderpress-header.css +2 -1
  18. package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
  19. package/dist/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  20. package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  21. package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
  22. package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  23. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
  24. package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
  25. package/dist/theme/components/nav/header-icon.css +24 -0
  26. package/dist/theme/components/nav/header-icon.tsx +119 -0
  27. package/dist/theme/components/nav/header-logo.css +16 -0
  28. package/dist/theme/components/nav/header-logo.tsx +200 -0
  29. package/dist/theme/components/nav/layout.tsx +117 -16
  30. package/dist/theme/components/nav/nav-logo.tsx +16 -19
  31. package/dist/theme/components/openapi/index.ts +0 -2
  32. package/dist/theme/components/openapi/openapi.css +0 -14
  33. package/dist/theme/components/shared/card-icon.tsx +37 -0
  34. package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
  35. package/dist/theme/components/shared/icon.tsx +120 -35
  36. package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
  37. package/dist/theme/components/shared/section-card.tsx +37 -6
  38. package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
  39. package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
  40. package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
  41. package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
  42. package/dist/theme/components/theme-provider.tsx +27 -11
  43. package/dist/theme/components/workspaces/card.tsx +7 -9
  44. package/dist/theme/hooks/use-ciderpress.ts +65 -5
  45. package/dist/theme/hooks/use-nav-items.ts +82 -11
  46. package/dist/theme/index.tsx +5 -3
  47. package/dist/theme/lib/read-social-links.ts +4 -3
  48. package/dist/theme/lib/theme-favicon.ts +1 -1
  49. package/dist/theme/lib/with-mount-base.ts +31 -0
  50. package/dist/theme/styles/overrides/footnotes.css +54 -0
  51. package/dist/theme/styles/overrides/lists.css +83 -0
  52. package/dist/theme/styles/overrides/rail.css +27 -10
  53. package/dist/theme/styles/overrides/section-card.css +9 -0
  54. package/dist/theme/styles/overrides/sidebar.css +22 -0
  55. package/package.json +18 -16
  56. package/src/theme/components/footer/site-footer.tsx +68 -9
  57. package/src/theme/components/home/feature-card.tsx +5 -8
  58. package/src/theme/components/home/feature.tsx +31 -11
  59. package/src/theme/components/home/hero-demo-custom.tsx +116 -0
  60. package/src/theme/components/home/hero-demo.css +27 -0
  61. package/src/theme/components/home/layout.tsx +149 -26
  62. package/src/theme/components/home/split-visual-custom.tsx +26 -0
  63. package/src/theme/components/home/workspaces.tsx +39 -3
  64. package/src/theme/components/nav/branch-tag.tsx +7 -7
  65. package/src/theme/components/nav/ciderpress-docs-bar.css +13 -1
  66. package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  67. package/src/theme/components/nav/ciderpress-header.css +2 -1
  68. package/src/theme/components/nav/ciderpress-header.tsx +26 -3
  69. package/src/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  70. package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  71. package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
  72. package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  73. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
  74. package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
  75. package/src/theme/components/nav/header-icon.css +24 -0
  76. package/src/theme/components/nav/header-icon.tsx +119 -0
  77. package/src/theme/components/nav/header-logo.css +16 -0
  78. package/src/theme/components/nav/header-logo.tsx +200 -0
  79. package/src/theme/components/nav/layout.tsx +117 -16
  80. package/src/theme/components/nav/nav-logo.tsx +16 -19
  81. package/src/theme/components/openapi/index.ts +0 -2
  82. package/src/theme/components/openapi/openapi.css +0 -14
  83. package/src/theme/components/shared/card-icon.tsx +37 -0
  84. package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
  85. package/src/theme/components/shared/icon.tsx +120 -35
  86. package/src/theme/components/shared/resolve-card-icon.ts +53 -12
  87. package/src/theme/components/shared/section-card.tsx +37 -6
  88. package/src/theme/components/sidebar/sidebar-badge.css +50 -0
  89. package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
  90. package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
  91. package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
  92. package/src/theme/components/theme-provider.tsx +27 -11
  93. package/src/theme/components/workspaces/card.tsx +7 -9
  94. package/src/theme/hooks/use-ciderpress.ts +65 -5
  95. package/src/theme/hooks/use-nav-items.ts +82 -11
  96. package/src/theme/index.tsx +5 -3
  97. package/src/theme/lib/read-social-links.ts +4 -3
  98. package/src/theme/lib/theme-favicon.ts +1 -1
  99. package/src/theme/lib/with-mount-base.ts +31 -0
  100. package/src/theme/styles/overrides/footnotes.css +54 -0
  101. package/src/theme/styles/overrides/lists.css +83 -0
  102. package/src/theme/styles/overrides/rail.css +27 -10
  103. package/src/theme/styles/overrides/section-card.css +9 -0
  104. package/src/theme/styles/overrides/sidebar.css +22 -0
  105. package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
  106. package/src/theme/components/openapi/copy-markdown-button.tsx +0 -41
@@ -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
  }
@@ -90,11 +90,21 @@
90
90
  display: flex;
91
91
  align-items: center;
92
92
  gap: 6px;
93
- flex: 1 1 auto;
93
+ /* Shrink-to-content (not grow) so page badges sit snug to the right of
94
+ the trail; long trails still truncate via `overflow: hidden`. */
95
+ flex: 0 1 auto;
94
96
  min-width: 0;
95
97
  overflow: hidden;
96
98
  }
97
99
 
100
+ /* Page badges (ALPHA / WIP …) immediately right of the breadcrumb trail. */
101
+ .cp-docs-bar__badges {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 4px;
105
+ flex: 0 0 auto;
106
+ }
107
+
98
108
  .cp-docs-bar__crumb-wrap {
99
109
  display: inline-flex;
100
110
  align-items: center;
@@ -358,6 +368,8 @@ html[data-cp-outline-collapsed='true'] .cp-docs-bar__chev {
358
368
  `styles/overrides/rspress.css`. ──────────────────────────────── */
359
369
  .cp-docs-bar__llms {
360
370
  flex-shrink: 0;
371
+ /* Crumbs no longer grow, so push the LLMs cluster to the far right. */
372
+ margin-left: auto;
361
373
  }
362
374
 
363
375
  .cp-docs-bar__llms .rp-llms-container {
@@ -1,3 +1,4 @@
1
+ import type { BadgeConfig } from '@ciderpress/config'
1
2
  import { useFrontmatter, useLocation } from '@rspress/core/runtime'
2
3
  import {
3
4
  LlmsContainer,
@@ -13,8 +14,10 @@ import { match, P } from 'massaman/match'
13
14
  import { useCallback, useEffect, useRef, useState } from 'react'
14
15
  import type React from 'react'
15
16
 
17
+ import { useCiderpress } from '../../hooks/use-ciderpress'
16
18
  import { RouteLink } from '../../lib/route-link.tsx'
17
19
  import { Icon } from '../shared/icon.tsx'
20
+ import { BadgeChips } from '../sidebar/sidebar-badge'
18
21
  import { useCiderpressLayout } from './ciderpress-layout-context'
19
22
 
20
23
  import './ciderpress-docs-bar.css'
@@ -117,6 +120,9 @@ export function CiderpressDocsBar(): React.ReactElement | null {
117
120
 
118
121
  const pageTitle = readStringField(fmRecord.title)
119
122
 
123
+ const { pageBadges } = useCiderpress()
124
+ const badges = lookupPageBadges({ pageBadges, pathname })
125
+
120
126
  const breadcrumbs = buildBreadcrumbs(pathname, pageTitle)
121
127
  const outlineEnabled = match(fmRecord.outline)
122
128
  .with(false, () => false)
@@ -174,12 +180,12 @@ export function CiderpressDocsBar(): React.ReactElement | null {
174
180
  <span className="cp-docs-bar__sep">›</span>
175
181
  {match(crumb.current)
176
182
  .with(true, () => (
177
- <span className="cp-docs-bar__crumb" aria-current="page">
183
+ <span className="cp-docs-bar__crumb" aria-current="page" title={crumb.label}>
178
184
  {crumb.label}
179
185
  </span>
180
186
  ))
181
187
  .otherwise(() => (
182
- <RouteLink href={crumb.href} className="cp-docs-bar__crumb">
188
+ <RouteLink href={crumb.href} className="cp-docs-bar__crumb" title={crumb.label}>
183
189
  {crumb.label}
184
190
  </RouteLink>
185
191
  ))}
@@ -187,6 +193,11 @@ export function CiderpressDocsBar(): React.ReactElement | null {
187
193
  ))}
188
194
  </nav>
189
195
  )}
196
+ {badges.length > 0 && (
197
+ <span className="cp-docs-bar__badges">
198
+ <BadgeChips badges={badges} />
199
+ </span>
200
+ )}
190
201
  <div className="cp-docs-bar__llms">
191
202
  <LlmsContainer>
192
203
  <LlmsCopyButton />
@@ -254,6 +265,26 @@ interface Crumb {
254
265
  readonly current: boolean
255
266
  }
256
267
 
268
+ /**
269
+ * Look up the current page's badges from the route→badges map, tolerating
270
+ * a trailing slash on the pathname (map keys are stored without one).
271
+ *
272
+ * @private
273
+ * @param params - The route→badges map (if present) and current pathname.
274
+ * @returns The page's badges, or an empty array when none apply.
275
+ */
276
+ function lookupPageBadges(params: {
277
+ readonly pageBadges: Record<string, readonly BadgeConfig[]> | undefined
278
+ readonly pathname: string
279
+ }): readonly BadgeConfig[] {
280
+ const { pageBadges, pathname } = params
281
+ if (pageBadges === undefined) {
282
+ return []
283
+ }
284
+ const stripped = pathname.replace(/\/$/, '')
285
+ return pageBadges[pathname] ?? pageBadges[stripped] ?? []
286
+ }
287
+
257
288
  /**
258
289
  * Build a 1- or 2-level breadcrumb trail from the current path and the
259
290
  * page's frontmatter title. The section label comes from titlecasing
@@ -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 && (
@@ -141,6 +141,56 @@
141
141
  text-decoration: none;
142
142
  }
143
143
 
144
+ .cp-nav-drawer__group {
145
+ display: flex;
146
+ flex-direction: column;
147
+ }
148
+
149
+ .cp-nav-drawer__group-toggle {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ width: 100%;
154
+ padding: 12px 16px;
155
+ background: transparent;
156
+ border: none;
157
+ border-radius: 8px;
158
+ color: var(--cp-c-text-1);
159
+ font-family: inherit;
160
+ font-size: 15px;
161
+ font-weight: 500;
162
+ text-align: left;
163
+ cursor: pointer;
164
+ transition:
165
+ background 0.12s ease,
166
+ color 0.12s ease;
167
+ }
168
+
169
+ .cp-nav-drawer__group-toggle:hover {
170
+ background: var(--cp-c-bg-soft);
171
+ color: var(--cp-c-brand-1);
172
+ }
173
+
174
+ .cp-nav-drawer__group-chevron {
175
+ flex-shrink: 0;
176
+ color: var(--cp-c-text-2);
177
+ transition: transform 0.18s ease;
178
+ }
179
+
180
+ .cp-nav-drawer__group-toggle[aria-expanded='true'] .cp-nav-drawer__group-chevron {
181
+ transform: rotate(180deg);
182
+ }
183
+
184
+ .cp-nav-drawer__group-items {
185
+ display: flex;
186
+ flex-direction: column;
187
+ }
188
+
189
+ .cp-nav-drawer__link--nested {
190
+ padding-left: 28px;
191
+ font-size: 14px;
192
+ }
193
+
144
194
  .cp-nav-drawer__divider {
145
195
  height: 1px;
146
196
  margin: 4px 20px;