@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.mjs +335 -51
- package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
- 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-docs-bar.css +13 -1
- package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
- 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-hamburger.css +50 -0
- package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
- package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
- package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
- package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
- 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 +117 -16
- package/dist/theme/components/nav/nav-logo.tsx +16 -19
- package/dist/theme/components/openapi/index.ts +0 -2
- package/dist/theme/components/openapi/openapi.css +0 -14
- 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/icon.tsx +120 -35
- package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
- package/dist/theme/components/shared/section-card.tsx +37 -6
- package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
- package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
- package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
- 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 +65 -5
- package/dist/theme/hooks/use-nav-items.ts +82 -11
- package/dist/theme/index.tsx +5 -3
- 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/dist/theme/styles/overrides/footnotes.css +54 -0
- package/dist/theme/styles/overrides/lists.css +83 -0
- package/dist/theme/styles/overrides/rail.css +27 -10
- package/dist/theme/styles/overrides/section-card.css +9 -0
- package/dist/theme/styles/overrides/sidebar.css +22 -0
- package/package.json +18 -16
- 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-docs-bar.css +13 -1
- package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
- 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-hamburger.css +50 -0
- package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
- package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
- package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
- package/src/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
- 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 +117 -16
- package/src/theme/components/nav/nav-logo.tsx +16 -19
- package/src/theme/components/openapi/index.ts +0 -2
- package/src/theme/components/openapi/openapi.css +0 -14
- 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/icon.tsx +120 -35
- package/src/theme/components/shared/resolve-card-icon.ts +53 -12
- package/src/theme/components/shared/section-card.tsx +37 -6
- package/src/theme/components/sidebar/sidebar-badge.css +50 -0
- package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
- package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
- 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 +65 -5
- package/src/theme/hooks/use-nav-items.ts +82 -11
- package/src/theme/index.tsx +5 -3
- 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
- package/src/theme/styles/overrides/footnotes.css +54 -0
- package/src/theme/styles/overrides/lists.css +83 -0
- package/src/theme/styles/overrides/rail.css +27 -10
- package/src/theme/styles/overrides/section-card.css +9 -0
- package/src/theme/styles/overrides/sidebar.css +22 -0
- package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
- 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
|
|
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
|
}
|
|
@@ -90,11 +90,21 @@
|
|
|
90
90
|
display: flex;
|
|
91
91
|
align-items: center;
|
|
92
92
|
gap: 6px;
|
|
93
|
-
|
|
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
|
|
@@ -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 && (
|
|
@@ -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;
|