@ciderpress/ui 1.0.0-rc.1 → 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.
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.mjs +316 -50
- package/dist/theme/components/footer/site-footer.tsx +68 -9
- package/dist/theme/components/home/feature-card.tsx +5 -8
- package/dist/theme/components/home/feature.tsx +31 -11
- package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
- package/dist/theme/components/home/hero-demo.css +27 -0
- package/dist/theme/components/home/layout.tsx +149 -26
- package/dist/theme/components/home/split-visual-custom.tsx +26 -0
- package/dist/theme/components/home/workspaces.tsx +39 -3
- package/dist/theme/components/nav/branch-tag.tsx +7 -7
- package/dist/theme/components/nav/ciderpress-header.css +2 -1
- package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
- package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
- package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
- package/dist/theme/components/nav/header-icon.css +24 -0
- package/dist/theme/components/nav/header-icon.tsx +119 -0
- package/dist/theme/components/nav/header-logo.css +16 -0
- package/dist/theme/components/nav/header-logo.tsx +200 -0
- package/dist/theme/components/nav/layout.tsx +45 -7
- package/dist/theme/components/nav/nav-logo.tsx +16 -19
- package/dist/theme/components/shared/card-icon.tsx +37 -0
- package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
- package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
- package/dist/theme/components/shared/section-card.tsx +9 -6
- package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/dist/theme/components/theme-provider.tsx +27 -11
- package/dist/theme/components/workspaces/card.tsx +7 -9
- package/dist/theme/hooks/use-ciderpress.ts +60 -5
- package/dist/theme/index.tsx +0 -1
- package/dist/theme/lib/read-social-links.ts +4 -3
- package/dist/theme/lib/theme-favicon.ts +1 -1
- package/dist/theme/lib/with-mount-base.ts +31 -0
- package/package.json +14 -14
- package/src/theme/components/footer/site-footer.tsx +68 -9
- package/src/theme/components/home/feature-card.tsx +5 -8
- package/src/theme/components/home/feature.tsx +31 -11
- package/src/theme/components/home/hero-demo-custom.tsx +116 -0
- package/src/theme/components/home/hero-demo.css +27 -0
- package/src/theme/components/home/layout.tsx +149 -26
- package/src/theme/components/home/split-visual-custom.tsx +26 -0
- package/src/theme/components/home/workspaces.tsx +39 -3
- package/src/theme/components/nav/branch-tag.tsx +7 -7
- package/src/theme/components/nav/ciderpress-header.css +2 -1
- package/src/theme/components/nav/ciderpress-header.tsx +26 -3
- package/src/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
- package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
- package/src/theme/components/nav/header-icon.css +24 -0
- package/src/theme/components/nav/header-icon.tsx +119 -0
- package/src/theme/components/nav/header-logo.css +16 -0
- package/src/theme/components/nav/header-logo.tsx +200 -0
- package/src/theme/components/nav/layout.tsx +45 -7
- package/src/theme/components/nav/nav-logo.tsx +16 -19
- package/src/theme/components/shared/card-icon.tsx +37 -0
- package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
- package/src/theme/components/shared/resolve-card-icon.ts +53 -12
- package/src/theme/components/shared/section-card.tsx +9 -6
- package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/src/theme/components/theme-provider.tsx +27 -11
- package/src/theme/components/workspaces/card.tsx +7 -9
- package/src/theme/hooks/use-ciderpress.ts +60 -5
- package/src/theme/index.tsx +0 -1
- package/src/theme/lib/read-social-links.ts +4 -3
- package/src/theme/lib/theme-favicon.ts +1 -1
- 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
|
|
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
|
|
28
|
-
readonly
|
|
36
|
+
readonly actions?: readonly ButtonConfig[]
|
|
37
|
+
readonly label?: string
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
interface
|
|
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
|
|
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
|
|
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.
|
|
97
|
+
eyebrow={h.label}
|
|
66
98
|
title={renderTitle(h.text ?? h.name ?? '')}
|
|
67
99
|
tagline={h.tagline}
|
|
68
|
-
actions={h.actions}
|
|
69
|
-
demo={
|
|
100
|
+
actions={mapButtonsToHeroActions(h.actions)}
|
|
101
|
+
demo={heroDemoEl}
|
|
70
102
|
/>
|
|
71
103
|
))
|
|
72
104
|
|
|
73
|
-
const
|
|
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(() =>
|
|
119
|
+
.otherwise(() => (
|
|
120
|
+
<CTA
|
|
121
|
+
title={c.title ?? ''}
|
|
122
|
+
subtitle={c.subtitle}
|
|
123
|
+
actions={mapButtonsToHeroActions(c.actions)}
|
|
124
|
+
/>
|
|
125
|
+
))
|
|
88
126
|
)
|
|
89
127
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
{'
|
|
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: {
|
|
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 {
|
|
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.
|
|
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:
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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:
|
|
93
|
+
function resolveRepo(site: CiderpressSiteBlock | undefined): string | undefined {
|
|
94
94
|
if (site === undefined) {
|
|
95
95
|
return undefined
|
|
96
96
|
}
|
|
@@ -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
|
-
<
|
|
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
|
|
10
|
-
* `
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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:
|
|
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
|
+
}
|