@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
|
@@ -4,19 +4,46 @@
|
|
|
4
4
|
// workspace dependencies here — they will cause "factory is undefined" runtime
|
|
5
5
|
// errors. See packages/ui/AGENTS.md for details.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// MERMAID v11: works fine here — no pin needed. The earlier v10 pin blamed
|
|
8
|
+
// langium/lazy-loading, but mermaid.render() actually resolves correctly. The
|
|
9
|
+
// real bug was local: `config` defaults to a fresh {} each render, so keying
|
|
10
|
+
// renderMermaid on it re-created the callback every render and re-fired the
|
|
11
|
+
// render effect in a loop. Each loop iteration called mermaid.render() into the
|
|
12
|
+
// same element id, clobbering the just-injected SVG — the diagram went blank
|
|
13
|
+
// until an unrelated re-render happened to run last. Fixed by keying on a stable
|
|
14
|
+
// serialized config value (see configKey below).
|
|
11
15
|
|
|
12
16
|
// oxlint-disable no-ternary
|
|
13
17
|
|
|
18
|
+
import elkLayouts from '@mermaid-js/layout-elk'
|
|
14
19
|
import mermaid from 'mermaid'
|
|
15
20
|
import type { MermaidConfig } from 'mermaid'
|
|
16
21
|
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
17
22
|
|
|
18
23
|
import './mermaid.css'
|
|
19
24
|
|
|
25
|
+
// ELK layout support. Diagrams that opt in with `layout: elk` (via frontmatter or
|
|
26
|
+
// `%%{init: {'layout': 'elk'}}%%`) need the ELK loaders registered before render,
|
|
27
|
+
// or mermaid.render() throws and the diagram silently blanks. `elkLayouts` is an
|
|
28
|
+
// array of lazy loaders (`() => import(...)`), so elkjs — and its web worker —
|
|
29
|
+
// only load client-side when a diagram actually uses ELK, never at build/SSR time.
|
|
30
|
+
// Registration itself is a cheap, edge-safe registry push. We guard on a module
|
|
31
|
+
// flag so React re-mounts / StrictMode double-invokes register exactly once.
|
|
32
|
+
const elkState = { registered: false }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register the ELK layout loaders with mermaid, exactly once per module load.
|
|
36
|
+
*
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
function ensureElkRegistered(): void {
|
|
40
|
+
if (elkState.registered) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
elkState.registered = true
|
|
44
|
+
mermaid.registerLayoutLoaders(elkLayouts)
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
interface MermaidRendererProps {
|
|
21
48
|
readonly code: string
|
|
22
49
|
readonly config?: MermaidConfig
|
|
@@ -214,6 +241,11 @@ function MermaidRenderer(props: MermaidRendererProps): React.ReactElement | null
|
|
|
214
241
|
|
|
215
242
|
const [svg, setSvg] = useState('')
|
|
216
243
|
const [renderError, setRenderError] = useState(false)
|
|
244
|
+
// Monotonic counter for per-render element ids. mermaid.render() renders into a
|
|
245
|
+
// node keyed by the id we pass; reusing one id across calls (React StrictMode
|
|
246
|
+
// double-invokes the mount effect) lets a later call clobber the SVG an earlier
|
|
247
|
+
// call already injected. A fresh id per call makes every render self-contained.
|
|
248
|
+
const renderSeq = useRef(0)
|
|
217
249
|
const [transform, setTransform] = useState<Transform>(INITIAL_TRANSFORM)
|
|
218
250
|
const transformRef = useRef<Transform>(INITIAL_TRANSFORM)
|
|
219
251
|
const [fullscreen, setFullscreen] = useState(false)
|
|
@@ -221,11 +253,21 @@ function MermaidRenderer(props: MermaidRendererProps): React.ReactElement | null
|
|
|
221
253
|
|
|
222
254
|
const highlightedCode = useMemo(() => highlightMermaid(code), [code])
|
|
223
255
|
|
|
256
|
+
// Stable value-key for the config object. `config` defaults to a fresh {} on
|
|
257
|
+
// every render, so depending on it directly re-creates renderMermaid each
|
|
258
|
+
// render, which re-fires the render effect in a loop. Each re-render calls
|
|
259
|
+
// mermaid.render() into the same element id — which clobbers the SVG we just
|
|
260
|
+
// injected, leaving the diagram blank until an unrelated re-render happens to
|
|
261
|
+
// land last. Keying on the serialized value keeps renderMermaid stable.
|
|
262
|
+
const configKey = JSON.stringify(config)
|
|
263
|
+
|
|
224
264
|
transformRef.current = transform
|
|
225
265
|
|
|
226
266
|
const isPreview = tab === 'preview'
|
|
227
267
|
|
|
228
268
|
const renderMermaid = useCallback(async () => {
|
|
269
|
+
ensureElkRegistered()
|
|
270
|
+
|
|
229
271
|
const hasDarkClass = document.documentElement.classList.contains('dark')
|
|
230
272
|
|
|
231
273
|
const mermaidConfig: MermaidConfig = {
|
|
@@ -235,14 +277,15 @@ function MermaidRenderer(props: MermaidRendererProps): React.ReactElement | null
|
|
|
235
277
|
...config,
|
|
236
278
|
}
|
|
237
279
|
|
|
280
|
+
const renderId = `${id.replaceAll(':', '')}-${(renderSeq.current += 1)}`
|
|
238
281
|
try {
|
|
239
282
|
mermaid.initialize(mermaidConfig)
|
|
240
|
-
const result = await mermaid.render(
|
|
283
|
+
const result = await mermaid.render(renderId, code as string)
|
|
241
284
|
setSvg(result.svg)
|
|
242
285
|
} catch {
|
|
243
286
|
setRenderError(true)
|
|
244
287
|
}
|
|
245
|
-
}, [code,
|
|
288
|
+
}, [code, configKey, id])
|
|
246
289
|
|
|
247
290
|
useEffect(() => {
|
|
248
291
|
renderMermaid()
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import type { CopyrightConfig, FooterConfig } from '@ciderpress/config'
|
|
1
2
|
import { useSite } from '@rspress/core/runtime'
|
|
2
|
-
import { match } from 'massaman/match'
|
|
3
|
+
import { match, P } from 'massaman/match'
|
|
3
4
|
import type React from 'react'
|
|
4
5
|
|
|
5
6
|
import { useCiderpress } from '../../hooks/use-ciderpress'
|
|
6
7
|
import { readSocialLinks } from '../../lib/read-social-links'
|
|
7
8
|
import { RouteLink } from '../../lib/route-link.tsx'
|
|
8
9
|
import { safeUrl } from '../../lib/safe-url.ts'
|
|
10
|
+
import { withMountBase } from '../../lib/with-mount-base.ts'
|
|
9
11
|
import { CiderpressNavSocialLinks } from '../nav/ciderpress-nav-social-links'
|
|
10
12
|
import { ThemeSwitcher } from '../nav/theme-switcher'
|
|
11
|
-
import { CiderpressMark } from '../shared/ciderpress-mark'
|
|
12
13
|
|
|
13
14
|
import './site-footer.css'
|
|
14
15
|
|
|
@@ -35,14 +36,17 @@ export function SiteFooter(): React.ReactElement | null {
|
|
|
35
36
|
const { message, copyright, socials } = ciderpressFooter ?? {}
|
|
36
37
|
const { columns, tagline, brandMark } = siteFooter ?? {}
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
+
const copyrightText = resolveCopyright(copyright, rspressSite.title)
|
|
40
|
+
const shouldRenderSocials = socials === true && socialLinks.length > 0
|
|
41
|
+
|
|
42
|
+
const hasFooterContent = message !== undefined || copyrightText !== null || shouldRenderSocials
|
|
39
43
|
const hasSiteContent = columns !== undefined || tagline !== undefined
|
|
40
44
|
// The footer also hosts the theme switcher when it's enabled — so a
|
|
41
|
-
// config with `theme.
|
|
45
|
+
// config with `theme.themeSwitcher: true` but no other footer content
|
|
42
46
|
// still needs the footer shell rendered.
|
|
43
47
|
const hasSwitcher = __CIDERPRESS_THEME_SWITCHER__
|
|
44
48
|
|
|
45
|
-
if (!
|
|
49
|
+
if (!hasFooterContent && !hasSiteContent && !hasSwitcher) {
|
|
46
50
|
return null
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -56,7 +60,16 @@ export function SiteFooter(): React.ReactElement | null {
|
|
|
56
60
|
<div className="cp-site-footer__brand-mark">
|
|
57
61
|
{match(brandMark)
|
|
58
62
|
.with(undefined, () => (
|
|
59
|
-
|
|
63
|
+
// Default to the project's `/icon.svg` (auto-generated
|
|
64
|
+
// from `config.title` at sync time, or overridden by
|
|
65
|
+
// shipping a `public/icon.svg`). This was previously
|
|
66
|
+
// the hardcoded `<CiderpressMark />` apple, which
|
|
67
|
+
// leaked ciderpress branding into every footer.
|
|
68
|
+
<img
|
|
69
|
+
src={withMountBase('/icon.svg')}
|
|
70
|
+
alt=""
|
|
71
|
+
className="cp-site-footer__brand-icon"
|
|
72
|
+
/>
|
|
60
73
|
))
|
|
61
74
|
.otherwise((mark) => mark)}
|
|
62
75
|
</div>
|
|
@@ -65,7 +78,7 @@ export function SiteFooter(): React.ReactElement | null {
|
|
|
65
78
|
.otherwise((msg) => (
|
|
66
79
|
<p className="cp-site-footer__message">{msg}</p>
|
|
67
80
|
))}
|
|
68
|
-
{match(
|
|
81
|
+
{match(shouldRenderSocials)
|
|
69
82
|
.with(true, () => (
|
|
70
83
|
<div className="cp-site-footer__socials">
|
|
71
84
|
<CiderpressNavSocialLinks links={socialLinks} />
|
|
@@ -95,8 +108,8 @@ export function SiteFooter(): React.ReactElement | null {
|
|
|
95
108
|
</div>
|
|
96
109
|
<div className="cp-site-footer__bottom">
|
|
97
110
|
<div className="cp-site-footer__bottom-inner">
|
|
98
|
-
{match(
|
|
99
|
-
.with(
|
|
111
|
+
{match(copyrightText)
|
|
112
|
+
.with(null, () => null)
|
|
100
113
|
.otherwise((cr) => (
|
|
101
114
|
<span className="cp-site-footer__copyright">{cr}</span>
|
|
102
115
|
))}
|
|
@@ -113,3 +126,49 @@ export function SiteFooter(): React.ReactElement | null {
|
|
|
113
126
|
</footer>
|
|
114
127
|
)
|
|
115
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the `footer.copyright` config union into a string for rendering.
|
|
132
|
+
*
|
|
133
|
+
* Accepts the new public shape: `true` for auto-generation from the site
|
|
134
|
+
* title + current year, a verbatim string, or a `CopyrightConfig` object
|
|
135
|
+
* with `company` / `dba` / `year`. `undefined` returns `null` so the
|
|
136
|
+
* caller can skip the render.
|
|
137
|
+
*
|
|
138
|
+
* @private
|
|
139
|
+
* @param copyright - The footer copyright config value
|
|
140
|
+
* @param siteTitle - The Rspress site title to use as fallback owner name
|
|
141
|
+
* @returns The formatted copyright string, or `null` when nothing should render
|
|
142
|
+
*/
|
|
143
|
+
function resolveCopyright(
|
|
144
|
+
copyright: FooterConfig['copyright'],
|
|
145
|
+
siteTitle: string | undefined
|
|
146
|
+
): string | null {
|
|
147
|
+
return match(copyright)
|
|
148
|
+
.with(undefined, () => null)
|
|
149
|
+
.with(true, () => formatCopyright({}, siteTitle))
|
|
150
|
+
.with(P.string, (s) => s)
|
|
151
|
+
.otherwise((cfg) => formatCopyright(cfg, siteTitle))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a copyright string from a partial `CopyrightConfig`. Falls back to
|
|
156
|
+
* the site title for the owner name and the current year for the date.
|
|
157
|
+
*
|
|
158
|
+
* @private
|
|
159
|
+
* @param cfg - Copyright config object (may be empty)
|
|
160
|
+
* @param siteTitle - Fallback owner name
|
|
161
|
+
* @returns Formatted copyright string
|
|
162
|
+
*/
|
|
163
|
+
function formatCopyright(cfg: CopyrightConfig, siteTitle: string | undefined): string {
|
|
164
|
+
const currentYear = new Date().getFullYear()
|
|
165
|
+
const yearPart = match(cfg.year)
|
|
166
|
+
.with(undefined, () => String(currentYear))
|
|
167
|
+
.with(P.number, (n) => String(n))
|
|
168
|
+
.otherwise(({ from }) => `${from}–${currentYear}`)
|
|
169
|
+
const owner = cfg.dba ?? cfg.company ?? siteTitle ?? ''
|
|
170
|
+
const ownerPart = match(owner.length === 0)
|
|
171
|
+
.with(true, () => '')
|
|
172
|
+
.otherwise(() => ` ${owner}`)
|
|
173
|
+
return `© ${yearPart}${ownerPart}`
|
|
174
|
+
}
|
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
padding: 0 32px;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
.cp-cta__eyebrow {
|
|
13
|
+
font-family: var(--cp-font-family-mono);
|
|
14
|
+
font-size: var(--cp-fs-eyebrow);
|
|
15
|
+
text-transform: uppercase;
|
|
16
|
+
letter-spacing: var(--cp-letter-eyebrow);
|
|
17
|
+
/* Same shade pair as `.cp-accent` — `--cp-c-brand-1` is a fill colour and
|
|
18
|
+
misses 4.5:1 at this size on several themes. See styles/rich-text.css. */
|
|
19
|
+
color: var(--cp-c-brand-2, var(--cp-c-brand-1));
|
|
20
|
+
margin-bottom: var(--cp-s-12);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
html[data-cp-variant='dark'] .cp-cta__eyebrow {
|
|
24
|
+
color: var(--cp-c-brand-light, var(--cp-c-brand-1));
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
.cp-cta__title {
|
|
13
28
|
font-size: clamp(32px, 5vw, 56px);
|
|
14
29
|
font-weight: 600;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { match } from 'massaman/match'
|
|
1
|
+
import { match, P } from 'massaman/match'
|
|
2
2
|
import type React from 'react'
|
|
3
3
|
|
|
4
|
+
import { renderRichText } from '../../lib/rich-text.tsx'
|
|
4
5
|
import { RouteLink } from '../../lib/route-link.tsx'
|
|
5
6
|
import { safeUrl } from '../../lib/safe-url.ts'
|
|
6
7
|
import type { HeroAction } from './hero'
|
|
@@ -8,6 +9,10 @@ import type { HeroAction } from './hero'
|
|
|
8
9
|
import './cta.css'
|
|
9
10
|
|
|
10
11
|
export interface CTAProps {
|
|
12
|
+
/**
|
|
13
|
+
* Small uppercase kicker rendered above the headline.
|
|
14
|
+
*/
|
|
15
|
+
readonly eyebrow?: string
|
|
11
16
|
/**
|
|
12
17
|
* Headline.
|
|
13
18
|
*/
|
|
@@ -30,12 +35,15 @@ export interface CTAProps {
|
|
|
30
35
|
* @returns React element.
|
|
31
36
|
*/
|
|
32
37
|
export function CTA(props: CTAProps): React.ReactElement {
|
|
33
|
-
const { title, subtitle, actions } = props
|
|
38
|
+
const { eyebrow, title, subtitle, actions } = props
|
|
34
39
|
const list = actions ?? []
|
|
35
40
|
|
|
36
41
|
return (
|
|
37
42
|
<section className="cp-cta">
|
|
38
43
|
<div className="cp-cta__inner">
|
|
44
|
+
{match(eyebrow)
|
|
45
|
+
.with(P.string, (e) => <div className="cp-cta__eyebrow">{renderRichText(e)}</div>)
|
|
46
|
+
.otherwise(() => null)}
|
|
39
47
|
<h2 className="cp-cta__title">{title}</h2>
|
|
40
48
|
{match(subtitle)
|
|
41
49
|
.with(undefined, () => null)
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.cp-feature-card__title {
|
|
21
|
-
font-family:
|
|
21
|
+
font-family: var(--cp-ff-display);
|
|
22
22
|
font-size: var(--cp-fs-feature-title);
|
|
23
23
|
font-weight: var(--cp-fw-bold);
|
|
24
24
|
color: var(--rp-c-text-1, var(--cp-c-text-1));
|
|
@@ -2,15 +2,17 @@ import { match, P } from 'massaman/match'
|
|
|
2
2
|
import type React from 'react'
|
|
3
3
|
|
|
4
4
|
import './feature-card.css'
|
|
5
|
+
import { renderRichText } from '../../lib/rich-text.tsx'
|
|
5
6
|
import { Card } from '../shared/card'
|
|
6
|
-
import {
|
|
7
|
+
import { CardIcon } from '../shared/card-icon'
|
|
8
|
+
import type { CardIconInput } from '../shared/resolve-card-icon'
|
|
7
9
|
import { resolveCardIcon } from '../shared/resolve-card-icon'
|
|
8
10
|
|
|
9
11
|
export interface FeatureCardProps {
|
|
10
12
|
readonly title: string
|
|
11
13
|
readonly description: string
|
|
12
14
|
readonly href?: string
|
|
13
|
-
readonly icon?:
|
|
15
|
+
readonly icon?: CardIconInput
|
|
14
16
|
readonly span?: 2 | 3 | 4 | 6
|
|
15
17
|
readonly titleLines?: number
|
|
16
18
|
readonly descriptionLines?: number
|
|
@@ -23,7 +25,7 @@ export interface FeatureItem {
|
|
|
23
25
|
readonly title: string
|
|
24
26
|
readonly details: string
|
|
25
27
|
readonly link?: string
|
|
26
|
-
readonly icon?:
|
|
28
|
+
readonly icon?: CardIconInput
|
|
27
29
|
readonly span?: 2 | 3 | 4 | 6
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -46,11 +48,7 @@ export function FeatureCard({
|
|
|
46
48
|
const resolved = resolveCardIcon(icon)
|
|
47
49
|
|
|
48
50
|
const iconEl = match(resolved)
|
|
49
|
-
.with(P.nonNullable, (r) =>
|
|
50
|
-
<span className={`cp-card__icon cp-card__icon--${r.color}`}>
|
|
51
|
-
<Icon icon={r.id} />
|
|
52
|
-
</span>
|
|
53
|
-
))
|
|
51
|
+
.with(P.nonNullable, (r) => <CardIcon resolved={r} className="cp-card__icon" />)
|
|
54
52
|
.otherwise(() => null)
|
|
55
53
|
|
|
56
54
|
const linkTail = match(href)
|
|
@@ -67,14 +65,14 @@ export function FeatureCard({
|
|
|
67
65
|
className={clampClass('cp-feature-card__title', titleLines)}
|
|
68
66
|
style={clampStyle(titleLines)}
|
|
69
67
|
>
|
|
70
|
-
{title}
|
|
68
|
+
{renderRichText(title)}
|
|
71
69
|
</span>
|
|
72
70
|
</div>
|
|
73
71
|
<span
|
|
74
72
|
className={clampClass('cp-feature-card__desc', descriptionLines)}
|
|
75
73
|
style={clampStyle(descriptionLines)}
|
|
76
74
|
>
|
|
77
|
-
{description}
|
|
75
|
+
{renderRichText(description)}
|
|
78
76
|
</span>
|
|
79
77
|
{linkTail}
|
|
80
78
|
</Card>
|
|
@@ -1,45 +1,68 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { useFrontmatter } from '@rspress/core/runtime'
|
|
1
|
+
import type { TruncateConfig } from '@ciderpress/config'
|
|
3
2
|
import { match, P } from 'massaman/match'
|
|
4
3
|
import type React from 'react'
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { renderRichText } from '../../lib/rich-text.tsx'
|
|
7
6
|
import { FeatureCard } from './feature-card'
|
|
8
7
|
import type { FeatureItem } from './feature-card'
|
|
9
8
|
|
|
9
|
+
interface FrontmatterFeaturesHeading {
|
|
10
|
+
readonly label?: string
|
|
11
|
+
readonly title?: string
|
|
12
|
+
readonly body?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface HomeFeatureProps {
|
|
16
|
+
readonly items?: readonly FeatureItem[]
|
|
17
|
+
readonly heading?: FrontmatterFeaturesHeading
|
|
18
|
+
readonly columns?: 1 | 2 | 3 | 4
|
|
19
|
+
readonly truncate?: TruncateConfig
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_HEADING_EYEBROW = 'Features'
|
|
23
|
+
const DEFAULT_HEADING_TITLE = 'Built for the way you ship.'
|
|
24
|
+
const DEFAULT_HEADING_SUBTITLE =
|
|
25
|
+
"Everything you need, nothing you don't. Configured in TypeScript, validated at boot."
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
28
|
+
* Features grid block. Renders the supplied feature cards under a heading,
|
|
29
|
+
* or nothing when no cards are present.
|
|
13
30
|
*
|
|
14
|
-
* @
|
|
31
|
+
* @param props - Resolved feature cards, optional heading, and truncation limits.
|
|
32
|
+
* @returns React element with the feature grid, or null.
|
|
15
33
|
*/
|
|
16
|
-
export function HomeFeature(): React.ReactElement | null {
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
export function HomeFeature(props: HomeFeatureProps): React.ReactElement | null {
|
|
35
|
+
const { items, heading, columns, truncate } = props
|
|
36
|
+
// Frontmatter is unvalidated user content — a `heading.title: {}`
|
|
37
|
+
// would otherwise render as `[object Object]` in the H2. Treat any
|
|
38
|
+
// non-string value as missing and fall through to the framework default.
|
|
39
|
+
const headingEyebrow = match(heading && heading.label)
|
|
40
|
+
.with(P.string, (s) => s)
|
|
41
|
+
.otherwise(() => DEFAULT_HEADING_EYEBROW)
|
|
42
|
+
const headingTitle = match(heading && heading.title)
|
|
43
|
+
.with(P.string, (s) => s)
|
|
44
|
+
.otherwise(() => DEFAULT_HEADING_TITLE)
|
|
45
|
+
const headingSubtitle = match(heading && heading.body)
|
|
46
|
+
.with(P.string, (s) => s)
|
|
47
|
+
.otherwise(() => DEFAULT_HEADING_SUBTITLE)
|
|
48
|
+
// Column count rides on a custom property so the grid keeps its CSS
|
|
49
|
+
// media queries — a narrow viewport still collapses to one column.
|
|
50
|
+
const gridStyle = match(columns)
|
|
51
|
+
.with(P.number, (c) => ({ '--cp-feature-cols': String(c) }) as React.CSSProperties)
|
|
52
|
+
.otherwise(() => undefined)
|
|
27
53
|
|
|
28
|
-
return match(
|
|
54
|
+
return match(items)
|
|
29
55
|
.with(
|
|
30
56
|
P.when((f): f is readonly FeatureItem[] => Array.isArray(f) && f.length > 0),
|
|
31
|
-
(
|
|
57
|
+
(list) => (
|
|
32
58
|
<div className="cp-feature-section">
|
|
33
59
|
<div className="cp-feature-section-head">
|
|
34
|
-
<div className="cp-feature-section-head__eyebrow">
|
|
35
|
-
<h2 className="cp-feature-section-head__title">
|
|
36
|
-
<p className="cp-feature-section-head__sub">
|
|
37
|
-
Everything you need, nothing you don't. Configured in TypeScript, validated at
|
|
38
|
-
boot.
|
|
39
|
-
</p>
|
|
60
|
+
<div className="cp-feature-section-head__eyebrow">{renderRichText(headingEyebrow)}</div>
|
|
61
|
+
<h2 className="cp-feature-section-head__title">{renderRichText(headingTitle)}</h2>
|
|
62
|
+
<p className="cp-feature-section-head__sub">{renderRichText(headingSubtitle)}</p>
|
|
40
63
|
</div>
|
|
41
|
-
<div className="cp-feature-grid">
|
|
42
|
-
{
|
|
64
|
+
<div className="cp-feature-grid" style={gridStyle}>
|
|
65
|
+
{list.map((f, i) => renderFeature(f, i, truncate))}
|
|
43
66
|
</div>
|
|
44
67
|
</div>
|
|
45
68
|
)
|
|
@@ -54,16 +77,16 @@ export function HomeFeature(): React.ReactElement | null {
|
|
|
54
77
|
* @private
|
|
55
78
|
* @param feature - Feature item data
|
|
56
79
|
* @param index - Array index for key generation
|
|
57
|
-
* @param
|
|
80
|
+
* @param truncate - Optional line-clamp limits for card text
|
|
58
81
|
* @returns Feature card element
|
|
59
82
|
*/
|
|
60
83
|
function renderFeature(
|
|
61
84
|
feature: FeatureItem,
|
|
62
85
|
index: number,
|
|
63
|
-
|
|
86
|
+
truncate: TruncateConfig | undefined
|
|
64
87
|
): React.ReactElement {
|
|
65
|
-
const titleLines =
|
|
66
|
-
const descLines =
|
|
88
|
+
const titleLines = truncate && truncate.title
|
|
89
|
+
const descLines = truncate && truncate.description
|
|
67
90
|
|
|
68
91
|
return (
|
|
69
92
|
<FeatureCard
|
|
@@ -64,3 +64,41 @@
|
|
|
64
64
|
.cp-hero-demo__file {
|
|
65
65
|
color: var(--cp-c-tint-amber-bright-fg);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/* Image-form hero demo — the frame chrome (rounded corners + brand-soft
|
|
69
|
+
glow shadow) stays; the `<img>` simply fills the surface. The hero
|
|
70
|
+
slot already constrains aspect ratio. */
|
|
71
|
+
.cp-hero-demo--image {
|
|
72
|
+
padding: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Code-form hero demo — the frame owns the chrome, so strip the native
|
|
76
|
+
Rspress code block's own margin and rounded corners. */
|
|
77
|
+
.cp-hero-demo--code .rp-codeblock {
|
|
78
|
+
margin: 0;
|
|
79
|
+
border-radius: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.cp-hero-demo--code .rp-codeblock pre {
|
|
83
|
+
overflow-x: auto;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.cp-hero-demo__img {
|
|
87
|
+
display: block;
|
|
88
|
+
width: 100%;
|
|
89
|
+
height: auto;
|
|
90
|
+
border-radius: inherit;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Custom Split visual `<pre>` shares the `.cp-split__code` surface with
|
|
94
|
+
the default `ConfigPreview` block — same padding, font, scrolling. */
|
|
95
|
+
.cp-split__code {
|
|
96
|
+
margin: 0;
|
|
97
|
+
padding: var(--cp-s-20) var(--cp-s-24);
|
|
98
|
+
font-family: var(--cp-font-family-mono);
|
|
99
|
+
font-size: var(--cp-fs-code);
|
|
100
|
+
line-height: var(--cp-lh-code);
|
|
101
|
+
white-space: pre;
|
|
102
|
+
overflow-x: auto;
|
|
103
|
+
color: var(--cp-c-text-1);
|
|
104
|
+
}
|