@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.11
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/head/css/loader-apple.css +1 -1
- package/dist/head/css/loader-dots.css +1 -1
- package/dist/head/css/themes/amber.css +1 -1
- package/dist/head/css/themes/arcade.css +1 -1
- package/dist/head/css/themes/grannysmith.css +1 -1
- package/dist/head/css/themes/honeycrisp.css +1 -1
- package/dist/head/css/themes/midnight.css +1 -1
- package/dist/head/css/themes/mulled.css +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.mjs +732 -55
- package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
- package/dist/theme/components/footer/site-footer.tsx +68 -9
- package/dist/theme/components/home/cta.css +15 -0
- package/dist/theme/components/home/cta.tsx +10 -2
- package/dist/theme/components/home/feature-card.css +1 -1
- package/dist/theme/components/home/feature-card.tsx +8 -10
- package/dist/theme/components/home/feature.tsx +54 -31
- package/dist/theme/components/home/hero-demo.css +38 -0
- package/dist/theme/components/home/home-visual.tsx +226 -0
- package/dist/theme/components/home/layout.tsx +410 -108
- package/dist/theme/components/home/split.css +46 -19
- package/dist/theme/components/home/split.tsx +28 -8
- package/dist/theme/components/home/tabs.css +299 -0
- package/dist/theme/components/home/tabs.tsx +351 -0
- package/dist/theme/components/home/trust-strip.css +41 -0
- package/dist/theme/components/home/trust-strip.tsx +168 -11
- package/dist/theme/components/home/workspaces.tsx +176 -38
- 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.css +4 -0
- package/dist/theme/components/workspaces/card.tsx +10 -12
- package/dist/theme/components/workspaces/grid.tsx +18 -6
- package/dist/theme/hooks/use-ciderpress.ts +65 -5
- package/dist/theme/hooks/use-nav-items.ts +82 -11
- package/dist/theme/index.tsx +10 -3
- package/dist/theme/lib/read-social-links.ts +4 -3
- package/dist/theme/lib/rich-text-parse.ts +740 -0
- package/dist/theme/lib/rich-text.test.tsx +342 -0
- package/dist/theme/lib/rich-text.tsx +86 -0
- package/dist/theme/lib/theme-favicon.ts +1 -1
- package/dist/theme/lib/with-mount-base.ts +31 -0
- package/dist/theme/styles/overrides/fonts.css +7 -4
- package/dist/theme/styles/overrides/footnotes.css +54 -0
- package/dist/theme/styles/overrides/home.css +11 -2
- package/dist/theme/styles/overrides/lists.css +83 -0
- package/dist/theme/styles/overrides/rail.css +27 -10
- package/dist/theme/styles/overrides/rspress.css +10 -5
- package/dist/theme/styles/overrides/section-card.css +9 -0
- package/dist/theme/styles/overrides/sidebar.css +22 -0
- package/dist/theme/styles/overrides/tokens.css +41 -23
- package/dist/theme/styles/rich-text.css +106 -0
- package/dist/theme/styles/themes/amber.css +32 -2
- package/dist/theme/styles/themes/arcade.css +16 -1
- package/dist/theme/styles/themes/grannysmith.css +32 -2
- package/dist/theme/styles/themes/honeycrisp.css +37 -7
- package/dist/theme/styles/themes/midnight.css +16 -1
- package/dist/theme/styles/themes/mulled.css +54 -9
- package/package.json +18 -16
- package/src/theme/components/footer/site-footer.tsx +68 -9
- package/src/theme/components/home/cta.css +15 -0
- package/src/theme/components/home/cta.tsx +10 -2
- package/src/theme/components/home/feature-card.css +1 -1
- package/src/theme/components/home/feature-card.tsx +8 -10
- package/src/theme/components/home/feature.tsx +54 -31
- package/src/theme/components/home/hero-demo.css +38 -0
- package/src/theme/components/home/home-visual.tsx +226 -0
- package/src/theme/components/home/layout.tsx +410 -108
- package/src/theme/components/home/split.css +46 -19
- package/src/theme/components/home/split.tsx +28 -8
- package/src/theme/components/home/tabs.css +299 -0
- package/src/theme/components/home/tabs.tsx +351 -0
- package/src/theme/components/home/trust-strip.css +41 -0
- package/src/theme/components/home/trust-strip.tsx +168 -11
- package/src/theme/components/home/workspaces.tsx +176 -38
- 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.css +4 -0
- package/src/theme/components/workspaces/card.tsx +10 -12
- package/src/theme/components/workspaces/grid.tsx +18 -6
- package/src/theme/hooks/use-ciderpress.ts +65 -5
- package/src/theme/hooks/use-nav-items.ts +82 -11
- package/src/theme/index.tsx +10 -3
- package/src/theme/lib/read-social-links.ts +4 -3
- package/src/theme/lib/rich-text-parse.ts +740 -0
- package/src/theme/lib/rich-text.test.tsx +342 -0
- package/src/theme/lib/rich-text.tsx +86 -0
- package/src/theme/lib/theme-favicon.ts +1 -1
- package/src/theme/lib/with-mount-base.ts +31 -0
- package/src/theme/styles/overrides/fonts.css +7 -4
- package/src/theme/styles/overrides/footnotes.css +54 -0
- package/src/theme/styles/overrides/home.css +11 -2
- package/src/theme/styles/overrides/lists.css +83 -0
- package/src/theme/styles/overrides/rail.css +27 -10
- package/src/theme/styles/overrides/rspress.css +10 -5
- package/src/theme/styles/overrides/section-card.css +9 -0
- package/src/theme/styles/overrides/sidebar.css +22 -0
- package/src/theme/styles/overrides/tokens.css +41 -23
- package/src/theme/styles/rich-text.css +106 -0
- package/src/theme/styles/themes/amber.css +32 -2
- package/src/theme/styles/themes/arcade.css +16 -1
- package/src/theme/styles/themes/grannysmith.css +32 -2
- package/src/theme/styles/themes/honeycrisp.css +37 -7
- package/src/theme/styles/themes/midnight.css +16 -1
- package/src/theme/styles/themes/mulled.css +54 -9
- package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
- package/src/theme/components/openapi/copy-markdown-button.tsx +0 -41
|
@@ -1,183 +1,485 @@
|
|
|
1
|
+
import type { ButtonConfig, HomeVisual, TruncateConfig } from '@ciderpress/config'
|
|
1
2
|
import { useFrontmatter } from '@rspress/core/runtime'
|
|
2
|
-
import { match } from 'massaman/match'
|
|
3
|
-
import
|
|
3
|
+
import { match, P } from 'massaman/match'
|
|
4
|
+
import React from 'react'
|
|
4
5
|
|
|
6
|
+
import { hasAccentMarker, isPlainText, renderRichText } from '../../lib/rich-text.tsx'
|
|
5
7
|
import { SiteFooter } from '../footer/site-footer'
|
|
6
8
|
import { CTA } from './cta'
|
|
7
9
|
import { HomeFeature } from './feature'
|
|
10
|
+
import type { FeatureItem } from './feature-card'
|
|
8
11
|
import { Hero } from './hero'
|
|
9
12
|
import type { HeroAction } from './hero'
|
|
10
13
|
import { HeroDemo } from './hero-demo'
|
|
14
|
+
import { HomeVisualView } from './home-visual'
|
|
11
15
|
import { PageRail } from './page-rail'
|
|
12
16
|
import { HomeSplit } from './split'
|
|
17
|
+
import { HomeTabs } from './tabs'
|
|
18
|
+
import type { HomeTabEntry } from './tabs'
|
|
13
19
|
import { TrustStrip } from './trust-strip'
|
|
20
|
+
import type { TrustItem } from './trust-strip'
|
|
14
21
|
import { HomeWorkspaces } from './workspaces'
|
|
22
|
+
import type { ShowcaseCard } from './workspaces'
|
|
15
23
|
|
|
16
24
|
interface HomeLayoutProps {
|
|
17
25
|
readonly beforeHero?: React.ReactNode
|
|
18
26
|
readonly afterHero?: React.ReactNode
|
|
19
|
-
readonly beforeFeatures?: React.ReactNode
|
|
20
|
-
readonly afterFeatures?: React.ReactNode
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
interface FrontmatterHero {
|
|
24
30
|
readonly name?: string
|
|
25
31
|
readonly text?: string
|
|
26
32
|
readonly tagline?: string
|
|
27
|
-
readonly actions?: readonly
|
|
33
|
+
readonly actions?: readonly ButtonConfig[]
|
|
34
|
+
/**
|
|
35
|
+
* Eyebrow chip copy. The sync engine reads `hero.label` from the config
|
|
36
|
+
* and emits it under this key (see `emitHero` in
|
|
37
|
+
* `@ciderpress/cli/lib/sync/home.ts`), so the frontmatter name is
|
|
38
|
+
* `eyebrow`, not `label`.
|
|
39
|
+
*/
|
|
28
40
|
readonly eyebrow?: string
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Flat heading trio carried by every copy-bearing block.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
interface FrontmatterHeading {
|
|
49
|
+
readonly label?: string
|
|
50
|
+
readonly title?: string
|
|
51
|
+
readonly body?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface FrontmatterProofBlock {
|
|
55
|
+
readonly type: 'proof'
|
|
32
56
|
readonly lead?: string
|
|
33
|
-
readonly names?: readonly
|
|
57
|
+
readonly names?: readonly TrustItem[]
|
|
34
58
|
}
|
|
35
59
|
|
|
36
|
-
interface
|
|
60
|
+
interface FrontmatterFeaturesBlock extends FrontmatterHeading {
|
|
61
|
+
readonly type: 'features'
|
|
62
|
+
readonly items?: readonly FeatureItem[]
|
|
63
|
+
readonly columns?: 1 | 2 | 3 | 4
|
|
64
|
+
readonly truncate?: TruncateConfig
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface FrontmatterShowcaseBlock extends FrontmatterHeading {
|
|
68
|
+
readonly type: 'showcase'
|
|
69
|
+
readonly cards?: readonly ShowcaseCard[]
|
|
70
|
+
readonly columns?: 1 | 2 | 3 | 4
|
|
71
|
+
readonly truncate?: TruncateConfig
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface FrontmatterSplitBlock {
|
|
75
|
+
readonly type: 'split'
|
|
76
|
+
readonly label?: string
|
|
77
|
+
readonly title: string
|
|
78
|
+
readonly body?: string
|
|
79
|
+
readonly bullets?: readonly string[]
|
|
80
|
+
readonly cta?: ButtonConfig
|
|
81
|
+
readonly visual?: HomeVisual
|
|
82
|
+
readonly reverse?: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tab entry as it arrives from frontmatter — the CTA is still a
|
|
87
|
+
* `ButtonConfig`, not the component's `link`/`theme` shape.
|
|
88
|
+
*
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
interface FrontmatterTabItem {
|
|
92
|
+
readonly label: string
|
|
93
|
+
readonly icon?: HomeTabEntry['icon']
|
|
37
94
|
readonly title?: string
|
|
38
|
-
readonly
|
|
39
|
-
readonly
|
|
95
|
+
readonly body?: string
|
|
96
|
+
readonly bullets?: readonly string[]
|
|
97
|
+
readonly cta?: ButtonConfig
|
|
98
|
+
readonly visual?: HomeVisual
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface FrontmatterTabsBlock extends FrontmatterHeading {
|
|
102
|
+
readonly type: 'tabs'
|
|
103
|
+
readonly items?: readonly FrontmatterTabItem[]
|
|
104
|
+
readonly orientation?: 'vertical' | 'horizontal'
|
|
105
|
+
readonly reverse?: boolean
|
|
40
106
|
}
|
|
41
107
|
|
|
108
|
+
interface FrontmatterCtaBlock extends FrontmatterHeading {
|
|
109
|
+
readonly type: 'cta'
|
|
110
|
+
readonly actions?: readonly ButtonConfig[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type FrontmatterBlock =
|
|
114
|
+
| FrontmatterProofBlock
|
|
115
|
+
| FrontmatterFeaturesBlock
|
|
116
|
+
| FrontmatterShowcaseBlock
|
|
117
|
+
| FrontmatterSplitBlock
|
|
118
|
+
| FrontmatterTabsBlock
|
|
119
|
+
| FrontmatterCtaBlock
|
|
120
|
+
|
|
42
121
|
/**
|
|
43
122
|
* Custom HomeLayout for ciderpress.
|
|
44
123
|
*
|
|
45
|
-
* Renders the
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
124
|
+
* Renders the hero header followed by an ordered `blocks` array (proof,
|
|
125
|
+
* features, showcase, split, tabs, cta) inside a continuous PageRail. Block
|
|
126
|
+
* order is the array order and any block type may repeat — the whole deck
|
|
127
|
+
* is driven by `home.blocks`, compiled into frontmatter by the sync engine.
|
|
49
128
|
*
|
|
50
|
-
* @param props -
|
|
129
|
+
* @param props - Hero slot props (kept for API compatibility with Rspress's HomeLayout).
|
|
51
130
|
* @returns React element with the home page.
|
|
52
131
|
*/
|
|
53
132
|
export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
|
|
54
133
|
const { frontmatter } = useFrontmatter()
|
|
134
|
+
|
|
135
|
+
// SSG-MD short-circuit: the React tree is walked by
|
|
136
|
+
// `react-render-to-markdown` to produce `.md` files served to
|
|
137
|
+
// `<LlmsCopyButton />`. Rendering the home shell would dump hero
|
|
138
|
+
// text, demo blocks, and footer columns into every copied page.
|
|
139
|
+
// Return an empty fragment for SSG-MD — the home page has no
|
|
140
|
+
// canonical markdown body. The `useFrontmatter` hook above runs in
|
|
141
|
+
// both passes so hook order stays consistent.
|
|
142
|
+
if (import.meta.env.SSG_MD) {
|
|
143
|
+
return <></>
|
|
144
|
+
}
|
|
145
|
+
|
|
55
146
|
const fm = frontmatter as Record<string, unknown>
|
|
56
147
|
|
|
57
148
|
const hero = fm.hero as FrontmatterHero | undefined
|
|
58
|
-
|
|
59
|
-
|
|
149
|
+
// heroDemo frontmatter:
|
|
150
|
+
// undefined → render the framework default
|
|
151
|
+
// false → suppress entirely
|
|
152
|
+
// object → render the user-supplied custom variant
|
|
153
|
+
const heroDemoFm = fm.heroDemo as false | HomeVisual | undefined
|
|
154
|
+
const blocks = (fm.blocks as readonly FrontmatterBlock[] | undefined) ?? []
|
|
155
|
+
|
|
156
|
+
// `P.nullish`, not `undefined`: a blank `heroDemo:` in a hand-authored
|
|
157
|
+
// index.md parses to null, which misses an `undefined` arm and would
|
|
158
|
+
// reach HomeVisualView with nothing to render.
|
|
159
|
+
const heroDemoEl = match(heroDemoFm)
|
|
160
|
+
.with(false, () => null)
|
|
161
|
+
.with(P.nullish, () => <HeroDemo />)
|
|
162
|
+
.otherwise((d) => <HomeVisualView visual={d} context="hero" />)
|
|
60
163
|
|
|
61
164
|
const heroSection = match(hero)
|
|
62
165
|
.with(undefined, () => null)
|
|
63
166
|
.otherwise((h) => (
|
|
64
167
|
<Hero
|
|
65
|
-
eyebrow={h.eyebrow}
|
|
168
|
+
eyebrow={renderOptionalRichText(h.eyebrow)}
|
|
66
169
|
title={renderTitle(h.text ?? h.name ?? '')}
|
|
67
|
-
tagline={h.tagline}
|
|
68
|
-
actions={h.actions}
|
|
69
|
-
demo={
|
|
170
|
+
tagline={renderOptionalRichText(h.tagline)}
|
|
171
|
+
actions={mapButtonsToHeroActions(h.actions)}
|
|
172
|
+
demo={heroDemoEl}
|
|
70
173
|
/>
|
|
71
174
|
))
|
|
72
175
|
|
|
73
|
-
const trustSection = match(trust)
|
|
74
|
-
.with(undefined, () => null)
|
|
75
|
-
.otherwise((t) => {
|
|
76
|
-
const names = t.names ?? []
|
|
77
|
-
return match(names.length === 0)
|
|
78
|
-
.with(true, () => null)
|
|
79
|
-
.otherwise(() => <TrustStrip lead={t.lead} names={names} />)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const ctaSection = match(cta)
|
|
83
|
-
.with(undefined, () => null)
|
|
84
|
-
.otherwise((c) =>
|
|
85
|
-
match(c.title === undefined)
|
|
86
|
-
.with(true, () => null)
|
|
87
|
-
.otherwise(() => <CTA title={c.title ?? ''} subtitle={c.subtitle} actions={c.actions} />)
|
|
88
|
-
)
|
|
89
|
-
|
|
90
176
|
return (
|
|
91
177
|
<PageRail>
|
|
92
178
|
{props.beforeHero}
|
|
93
179
|
{heroSection}
|
|
94
180
|
{props.afterHero}
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{props.afterFeatures}
|
|
99
|
-
<HomeSplit
|
|
100
|
-
eyebrow="Configuration"
|
|
101
|
-
title="One file. Validated. Type-safe."
|
|
102
|
-
body="Define your docs site in ciderpress.config.ts. Zod validates at boot — no surprises in prod."
|
|
103
|
-
bullets={[
|
|
104
|
-
'Type-safe config with full IntelliSense',
|
|
105
|
-
'Hot-reloads on every save',
|
|
106
|
-
'Composable presets for OpenAPI, blog, changelog',
|
|
107
|
-
'First-class i18n out of the box',
|
|
108
|
-
]}
|
|
109
|
-
action={{ theme: 'brand', text: 'Read the docs', link: '/getting-started/quick-start' }}
|
|
110
|
-
visual={<ConfigPreview />}
|
|
111
|
-
/>
|
|
112
|
-
<HomeWorkspaces />
|
|
113
|
-
{ctaSection}
|
|
181
|
+
{blocks.map((block, index) => (
|
|
182
|
+
<React.Fragment key={`${block.type}-${index}`}>{renderBlock(block)}</React.Fragment>
|
|
183
|
+
))}
|
|
114
184
|
<SiteFooter />
|
|
115
185
|
</PageRail>
|
|
116
186
|
)
|
|
117
187
|
}
|
|
118
188
|
|
|
119
189
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
190
|
+
* Render a single home block by its discriminated `type`.
|
|
191
|
+
*
|
|
192
|
+
* Terminates in `.otherwise`, not `.exhaustive`: `blocks` is read from
|
|
193
|
+
* frontmatter with a cast, and a hand-authored `index.md` short-circuits
|
|
194
|
+
* the sync engine's block compilation entirely, so an unrecognized `type`
|
|
195
|
+
* is reachable user input. `.exhaustive()` would throw `NonExhaustiveError`
|
|
196
|
+
* mid-render and take down the whole page — and the SSG build with it —
|
|
197
|
+
* over one typo. An unknown block renders as nothing instead.
|
|
123
198
|
*
|
|
124
|
-
* @
|
|
199
|
+
* @private
|
|
200
|
+
* @param block - Frontmatter block compiled from `home.blocks`.
|
|
201
|
+
* @returns The block's React element, or null when it has no renderable data.
|
|
125
202
|
*/
|
|
126
|
-
function
|
|
127
|
-
return (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
203
|
+
function renderBlock(block: FrontmatterBlock): React.ReactNode {
|
|
204
|
+
return match(block)
|
|
205
|
+
.with({ type: 'proof' }, (b) => {
|
|
206
|
+
const names = b.names ?? []
|
|
207
|
+
return match(names.length === 0)
|
|
208
|
+
.with(true, () => null)
|
|
209
|
+
.otherwise(() => <TrustStrip lead={b.lead} names={names} />)
|
|
210
|
+
})
|
|
211
|
+
.with({ type: 'features' }, (b) => (
|
|
212
|
+
<HomeFeature
|
|
213
|
+
items={b.items}
|
|
214
|
+
heading={toHeading(b)}
|
|
215
|
+
columns={b.columns}
|
|
216
|
+
truncate={b.truncate}
|
|
217
|
+
/>
|
|
218
|
+
))
|
|
219
|
+
.with({ type: 'showcase' }, (b) => (
|
|
220
|
+
<HomeWorkspaces
|
|
221
|
+
heading={toHeading(b)}
|
|
222
|
+
cards={b.cards}
|
|
223
|
+
columns={b.columns}
|
|
224
|
+
truncate={b.truncate}
|
|
225
|
+
/>
|
|
226
|
+
))
|
|
227
|
+
.with({ type: 'split' }, (b) => (
|
|
228
|
+
<HomeSplit
|
|
229
|
+
eyebrow={b.label}
|
|
230
|
+
title={renderRichText(b.title)}
|
|
231
|
+
body={renderOptionalRichText(b.body)}
|
|
232
|
+
bullets={b.bullets ?? []}
|
|
233
|
+
action={mapButtonToAction(b.cta)}
|
|
234
|
+
reverse={b.reverse}
|
|
235
|
+
visual={match(b.visual)
|
|
236
|
+
.with(P.nullish, () => null)
|
|
237
|
+
.otherwise((v) => (
|
|
238
|
+
<HomeVisualView visual={v} context="split" />
|
|
239
|
+
))}
|
|
240
|
+
/>
|
|
241
|
+
))
|
|
242
|
+
.with({ type: 'tabs' }, (b) => (
|
|
243
|
+
<HomeTabs
|
|
244
|
+
eyebrow={b.label}
|
|
245
|
+
title={b.title}
|
|
246
|
+
body={b.body}
|
|
247
|
+
items={mapTabItems(b.items)}
|
|
248
|
+
orientation={b.orientation}
|
|
249
|
+
reverse={b.reverse}
|
|
250
|
+
/>
|
|
251
|
+
))
|
|
252
|
+
.with({ type: 'cta' }, (b) =>
|
|
253
|
+
// Blank/absent title, not just `undefined` — a hand-authored `title:`
|
|
254
|
+
// parses to null and `title: ''` is empty, and either would paint the
|
|
255
|
+
// full band (eyebrow, glow, buttons) around an empty headline.
|
|
256
|
+
match(renderOptionalRichText(b.title))
|
|
257
|
+
.with(P.nullish, () => null)
|
|
258
|
+
.otherwise((title) => (
|
|
259
|
+
<CTA
|
|
260
|
+
eyebrow={b.label}
|
|
261
|
+
title={title}
|
|
262
|
+
subtitle={renderOptionalRichText(b.body)}
|
|
263
|
+
actions={mapButtonsToHeroActions(b.actions)}
|
|
264
|
+
/>
|
|
265
|
+
))
|
|
266
|
+
)
|
|
267
|
+
.otherwise(() => null)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Map a single `ButtonConfig` into the `link`/`theme` action shape the
|
|
272
|
+
* split and tab components take, preserving the configured variant.
|
|
273
|
+
*
|
|
274
|
+
* @private
|
|
275
|
+
* @param button - Optional button config from frontmatter
|
|
276
|
+
* @returns Action object, or undefined when no button is configured
|
|
277
|
+
*/
|
|
278
|
+
function mapButtonToAction(
|
|
279
|
+
button: ButtonConfig | undefined
|
|
280
|
+
): { readonly text: string; readonly link: string; readonly theme?: 'brand' | 'alt' } | undefined {
|
|
281
|
+
if (!isButtonConfig(button)) {
|
|
282
|
+
return undefined
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
text: button.text,
|
|
286
|
+
link: button.href,
|
|
287
|
+
theme: mapButtonVariantToHeroTheme(button.variant),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Convert frontmatter tab entries into the component's entry shape. Only
|
|
293
|
+
* the CTA needs work — its `ButtonConfig` becomes a `link`/`theme`
|
|
294
|
+
* action; every other field passes through.
|
|
295
|
+
*
|
|
296
|
+
* Malformed entries are dropped rather than rendered: a YAML sequence
|
|
297
|
+
* with a blank item yields `null`, which would throw on the first
|
|
298
|
+
* property read.
|
|
299
|
+
*
|
|
300
|
+
* @private
|
|
301
|
+
* @param items - Tab entries from frontmatter, possibly absent or malformed
|
|
302
|
+
* @returns Component-ready tab entries
|
|
303
|
+
*/
|
|
304
|
+
function mapTabItems(items: readonly FrontmatterTabItem[] | undefined): readonly HomeTabEntry[] {
|
|
305
|
+
if (!Array.isArray(items)) {
|
|
306
|
+
return []
|
|
307
|
+
}
|
|
308
|
+
return items.filter(isTabItem).map(toTabEntry)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Project one validated frontmatter tab entry onto the component's entry
|
|
313
|
+
* shape — only the CTA changes form.
|
|
314
|
+
*
|
|
315
|
+
* @private
|
|
316
|
+
* @param item - Validated tab entry from frontmatter
|
|
317
|
+
* @returns Component-ready tab entry
|
|
318
|
+
*/
|
|
319
|
+
function toTabEntry(item: FrontmatterTabItem): HomeTabEntry {
|
|
320
|
+
return {
|
|
321
|
+
label: item.label,
|
|
322
|
+
icon: item.icon,
|
|
323
|
+
title: item.title,
|
|
324
|
+
body: item.body,
|
|
325
|
+
bullets: item.bullets,
|
|
326
|
+
cta: mapButtonToAction(item.cta),
|
|
327
|
+
visual: item.visual,
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Whether a frontmatter value is a usable tab entry. `label` is the one
|
|
333
|
+
* required field — it is the clickable text and the render key.
|
|
334
|
+
*
|
|
335
|
+
* @private
|
|
336
|
+
* @param value - Unvalidated entry from frontmatter
|
|
337
|
+
* @returns True when the entry can be rendered
|
|
338
|
+
*/
|
|
339
|
+
function isTabItem(value: unknown): value is FrontmatterTabItem {
|
|
340
|
+
if (typeof value !== 'object' || value === null) {
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
const item = value as { label?: unknown }
|
|
344
|
+
return typeof item.label === 'string'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Whether a frontmatter value is a usable button config. Both `text` and
|
|
349
|
+
* `href` must be strings — `safeUrl` would throw on a missing `href`.
|
|
350
|
+
*
|
|
351
|
+
* @private
|
|
352
|
+
* @param value - Unvalidated button from frontmatter
|
|
353
|
+
* @returns True when the button can be rendered
|
|
354
|
+
*/
|
|
355
|
+
function isButtonConfig(value: unknown): value is ButtonConfig {
|
|
356
|
+
if (typeof value !== 'object' || value === null) {
|
|
357
|
+
return false
|
|
358
|
+
}
|
|
359
|
+
const button = value as { text?: unknown; href?: unknown }
|
|
360
|
+
return typeof button.text === 'string' && typeof button.href === 'string'
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Collect a block's flat `label` / `title` / `body` keys into the heading
|
|
365
|
+
* object the grid components take.
|
|
366
|
+
*
|
|
367
|
+
* @private
|
|
368
|
+
* @param block - Any block carrying the flat heading trio
|
|
369
|
+
* @returns Heading object for the grid components
|
|
370
|
+
*/
|
|
371
|
+
function toHeading(block: FrontmatterHeading): FrontmatterHeading {
|
|
372
|
+
return { label: block.label, title: block.title, body: block.body }
|
|
150
373
|
}
|
|
151
374
|
|
|
152
375
|
/**
|
|
153
|
-
* Render
|
|
376
|
+
* Render optional rich-text copy, collapsing absent or blank values to
|
|
377
|
+
* `undefined` rather than an empty node.
|
|
154
378
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
379
|
+
* Callers treat `undefined` as "omit this element entirely". Passing
|
|
380
|
+
* `renderRichText('')` instead returns an empty-but-defined node, which
|
|
381
|
+
* slips past those checks and paints a styled shell with no content in
|
|
382
|
+
* it: an empty eyebrow chip renders as a stray pill above the headline.
|
|
383
|
+
*
|
|
384
|
+
* @private
|
|
385
|
+
* @param raw - Optional raw copy from frontmatter.
|
|
386
|
+
* @returns Rendered fragment, or undefined when there is nothing to show.
|
|
387
|
+
*/
|
|
388
|
+
function renderOptionalRichText(raw: string | undefined): React.ReactNode {
|
|
389
|
+
const value = raw ?? ''
|
|
390
|
+
return match(value.trim() === '')
|
|
391
|
+
.with(true, () => undefined)
|
|
392
|
+
.otherwise(() => renderRichText(value))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Render a hero title, accenting part of it in the brand colour.
|
|
397
|
+
*
|
|
398
|
+
* Two modes, so the default is good and the override is exact:
|
|
399
|
+
*
|
|
400
|
+
* - **explicit** — a title carrying `**emphasis**` accents precisely
|
|
401
|
+
* what the author marked, and nothing else
|
|
402
|
+
* - **automatic** — a title of unmarked text accents the trailing half
|
|
403
|
+
* of its words, the long-standing behaviour
|
|
404
|
+
*
|
|
405
|
+
* A title carrying any other markup renders as written: the automatic
|
|
406
|
+
* pass works on the raw string, so it would cut links and code spans in
|
|
407
|
+
* half.
|
|
408
|
+
*
|
|
409
|
+
* Either way the copy runs through the inline renderer, so code and
|
|
410
|
+
* links work in a title regardless of which mode applies.
|
|
158
411
|
*
|
|
159
412
|
* @private
|
|
160
413
|
* @param raw - The raw title string from frontmatter.
|
|
161
|
-
* @returns Title fragment
|
|
414
|
+
* @returns Title fragment.
|
|
162
415
|
*/
|
|
163
416
|
function renderTitle(raw: string): React.ReactNode {
|
|
164
417
|
const trimmed = raw.trim()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
418
|
+
if (trimmed.length === 0) {
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
// The automatic accent slices the raw string, so it can only run on
|
|
422
|
+
// copy with no markup in it — splitting `[a b](/c)` or `` `a b` ``
|
|
423
|
+
// down the middle would produce two broken fragments.
|
|
424
|
+
if (hasAccentMarker(trimmed) || !isPlainText(trimmed)) {
|
|
425
|
+
return renderRichText(trimmed)
|
|
426
|
+
}
|
|
427
|
+
const words = trimmed.split(/\s+/)
|
|
428
|
+
if (words.length <= 1) {
|
|
429
|
+
return renderRichText(trimmed)
|
|
430
|
+
}
|
|
431
|
+
const tailCount = Math.max(1, Math.ceil(words.length / 2))
|
|
432
|
+
const headWords = words.slice(0, words.length - tailCount).join(' ')
|
|
433
|
+
const tailWords = words.slice(words.length - tailCount).join(' ')
|
|
434
|
+
return (
|
|
435
|
+
<>
|
|
436
|
+
{renderRichText(headWords)}
|
|
437
|
+
<span className="cp-hero__grad"> {renderRichText(tailWords)}</span>
|
|
438
|
+
</>
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Map the unified `ButtonConfig[]` shape (used by the `home.hero`
|
|
444
|
+
* / cta configs) into the legacy `HeroAction[]` shape still consumed by
|
|
445
|
+
* `<Hero />` and `<CTA />`. `'primary'` → `'brand'`,
|
|
446
|
+
* `'secondary' | 'ghost'` → `'alt'`, `undefined` → `undefined`.
|
|
447
|
+
*
|
|
448
|
+
* @private
|
|
449
|
+
* @param actions - Optional list of unified button configs from frontmatter
|
|
450
|
+
* @returns Hero-action array consumed by the existing component API
|
|
451
|
+
*/
|
|
452
|
+
function mapButtonsToHeroActions(
|
|
453
|
+
actions: readonly ButtonConfig[] | undefined
|
|
454
|
+
): readonly HeroAction[] | undefined {
|
|
455
|
+
if (!Array.isArray(actions)) {
|
|
456
|
+
return undefined
|
|
457
|
+
}
|
|
458
|
+
return actions.filter(isButtonConfig).map((action) => ({
|
|
459
|
+
text: action.text,
|
|
460
|
+
link: action.href,
|
|
461
|
+
theme: mapButtonVariantToHeroTheme(action.variant),
|
|
462
|
+
}))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Project the `'primary' | 'secondary' | 'ghost'` variant token back into
|
|
467
|
+
* the legacy `'brand' | 'alt'` token that `<Hero />` accepts.
|
|
468
|
+
*
|
|
469
|
+
* Terminates in `.otherwise`: `isButtonConfig` only checks `text` and
|
|
470
|
+
* `href`, so an unrecognized `variant` on a hand-authored button reaches
|
|
471
|
+
* here. This runs for hero, split, and tab CTAs alike, and `.exhaustive()`
|
|
472
|
+
* threw on all three. An unknown variant falls back to the default style.
|
|
473
|
+
*
|
|
474
|
+
* @private
|
|
475
|
+
* @param variant - Optional variant from the unified button config
|
|
476
|
+
* @returns `'brand'`, `'alt'`, or `undefined`
|
|
477
|
+
*/
|
|
478
|
+
function mapButtonVariantToHeroTheme(
|
|
479
|
+
variant: ButtonConfig['variant']
|
|
480
|
+
): 'brand' | 'alt' | undefined {
|
|
481
|
+
return match(variant)
|
|
482
|
+
.with('primary', () => 'brand' as const)
|
|
483
|
+
.with(P.union('secondary', 'ghost'), () => 'alt' as const)
|
|
484
|
+
.otherwise(() => undefined)
|
|
183
485
|
}
|