@ciderpress/ui 1.0.0-rc.12 → 1.0.0-rc.14
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/themes/honeycrisp.css +1 -1
- package/dist/node.mjs +17 -13
- package/dist/plugins/mermaid/MermaidRenderer.tsx +2 -2
- package/dist/theme/components/home/hero-banner.tsx +225 -0
- package/dist/theme/components/home/hero-demo.css +492 -61
- package/dist/theme/components/home/hero-demo.test.tsx +159 -0
- package/dist/theme/components/home/hero-demo.tsx +335 -32
- package/dist/theme/components/home/hero.css +142 -9
- package/dist/theme/components/home/hero.test.tsx +105 -0
- package/dist/theme/components/home/hero.tsx +201 -4
- package/dist/theme/components/home/home-visual.css +98 -0
- package/dist/theme/components/home/home-visual.test.tsx +27 -0
- package/dist/theme/components/home/home-visual.tsx +20 -13
- package/dist/theme/components/home/layout.tsx +125 -1
- package/dist/theme/components/home/split.css +2 -2
- package/dist/theme/components/home/tabs.css +1 -1
- package/dist/theme/components/home/trust-strip.css +12 -0
- package/dist/theme/components/home/trust-strip.test.tsx +33 -0
- package/dist/theme/components/home/trust-strip.tsx +76 -15
- package/dist/theme/components/nav/layout.tsx +10 -14
- package/dist/theme/components/nav/nav-logo.tsx +2 -3
- package/dist/theme/components/sidebar/sidebar-filter.test.ts +49 -9
- package/dist/theme/components/sidebar/sidebar-filter.ts +24 -4
- package/dist/theme/index.tsx +8 -1
- package/dist/theme/lib/rich-text-parse.ts +0 -2
- package/dist/theme/lib/theme-favicon.ts +0 -3
- package/dist/theme/styles/themes/honeycrisp.css +20 -20
- package/package.json +20 -19
- package/src/theme/components/home/hero-banner.tsx +225 -0
- package/src/theme/components/home/hero-demo.css +492 -61
- package/src/theme/components/home/hero-demo.test.tsx +159 -0
- package/src/theme/components/home/hero-demo.tsx +335 -32
- package/src/theme/components/home/hero.css +142 -9
- package/src/theme/components/home/hero.test.tsx +105 -0
- package/src/theme/components/home/hero.tsx +201 -4
- package/src/theme/components/home/home-visual.css +98 -0
- package/src/theme/components/home/home-visual.test.tsx +27 -0
- package/src/theme/components/home/home-visual.tsx +20 -13
- package/src/theme/components/home/layout.tsx +125 -1
- package/src/theme/components/home/split.css +2 -2
- package/src/theme/components/home/tabs.css +1 -1
- package/src/theme/components/home/trust-strip.css +12 -0
- package/src/theme/components/home/trust-strip.test.tsx +33 -0
- package/src/theme/components/home/trust-strip.tsx +76 -15
- package/src/theme/components/nav/layout.tsx +10 -14
- package/src/theme/components/nav/nav-logo.tsx +2 -3
- package/src/theme/components/sidebar/sidebar-filter.test.ts +49 -9
- package/src/theme/components/sidebar/sidebar-filter.ts +24 -4
- package/src/theme/index.tsx +8 -1
- package/src/theme/lib/rich-text-parse.ts +0 -2
- package/src/theme/lib/theme-favicon.ts +0 -3
- package/src/theme/styles/themes/honeycrisp.css +20 -20
|
@@ -1,10 +1,98 @@
|
|
|
1
1
|
.cp-hero {
|
|
2
2
|
position: relative;
|
|
3
|
+
overflow: hidden;
|
|
3
4
|
padding: 96px 0 72px;
|
|
4
5
|
text-align: center;
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
.cp-hero__background {
|
|
9
|
+
position: absolute;
|
|
10
|
+
z-index: 0;
|
|
11
|
+
inset: 0;
|
|
12
|
+
pointer-events: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.cp-hero__background > * {
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset: 0;
|
|
18
|
+
display: block;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
object-fit: cover;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.cp-hero--has-background {
|
|
25
|
+
background-color: var(--cp-c-home-bg);
|
|
26
|
+
background-image: var(--cp-hero-background-image);
|
|
27
|
+
background-position: var(--cp-hero-background-position, center);
|
|
28
|
+
background-size: var(--cp-hero-background-size, cover);
|
|
29
|
+
background-repeat: var(--cp-hero-background-repeat, no-repeat);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.cp-hero--has-background .cp-hero__inner {
|
|
33
|
+
position: relative;
|
|
34
|
+
z-index: 2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cp-hero--has-background::before {
|
|
38
|
+
content: '';
|
|
39
|
+
position: absolute;
|
|
40
|
+
inset: 0;
|
|
41
|
+
z-index: 1;
|
|
42
|
+
backdrop-filter: brightness(0.5);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:where([data-cp-variant='light']) .cp-hero--has-background::before {
|
|
46
|
+
backdrop-filter: brightness(0.8);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.cp-hero--background-dark,
|
|
50
|
+
.cp-hero--background-variants {
|
|
51
|
+
--cp-hero-text-1: #f5f5f5;
|
|
52
|
+
--cp-hero-text-2: #d4d4d4;
|
|
53
|
+
--cp-hero-border: rgb(255 255 255 / 18%);
|
|
54
|
+
--cp-hero-surface: rgb(10 10 10 / 72%);
|
|
55
|
+
--cp-hero-soft: rgb(255 255 255 / 10%);
|
|
56
|
+
|
|
57
|
+
background-color: #080506;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:where([data-cp-variant='light']) .cp-hero--background-dark::before,
|
|
61
|
+
.cp-hero--background-dark::before {
|
|
62
|
+
backdrop-filter: brightness(0.5);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.cp-hero--background-light,
|
|
66
|
+
:where([data-cp-variant='light']) .cp-hero--background-variants {
|
|
67
|
+
--cp-hero-text-1: #1a1a1a;
|
|
68
|
+
--cp-hero-text-2: #4a4a4a;
|
|
69
|
+
--cp-hero-border: rgb(0 0 0 / 18%);
|
|
70
|
+
--cp-hero-surface: rgb(255 255 255 / 76%);
|
|
71
|
+
--cp-hero-soft: rgb(0 0 0 / 8%);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.cp-hero--background-light::before,
|
|
75
|
+
:where([data-cp-variant='light']) .cp-hero--background-variants::before {
|
|
76
|
+
backdrop-filter: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.cp-hero--has-background .cp-hero__title code,
|
|
80
|
+
.cp-hero--has-background .cp-hero__tagline code {
|
|
81
|
+
color: var(--cp-hero-text-1);
|
|
82
|
+
background: var(--cp-hero-soft);
|
|
83
|
+
border-color: var(--cp-hero-border);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:where([data-cp-variant='light']) .cp-hero--background-variants {
|
|
87
|
+
background-image: var(--cp-hero-background-light-image);
|
|
88
|
+
background-position: var(--cp-hero-background-light-position, center);
|
|
89
|
+
background-size: var(--cp-hero-background-light-size, cover);
|
|
90
|
+
background-repeat: var(--cp-hero-background-light-repeat, no-repeat);
|
|
91
|
+
}
|
|
92
|
+
|
|
7
93
|
.cp-hero__inner {
|
|
94
|
+
position: relative;
|
|
95
|
+
z-index: 2;
|
|
8
96
|
max-width: 1100px;
|
|
9
97
|
margin: 0 auto;
|
|
10
98
|
padding: 0 32px;
|
|
@@ -15,12 +103,12 @@
|
|
|
15
103
|
align-items: center;
|
|
16
104
|
gap: 8px;
|
|
17
105
|
padding: 6px 12px;
|
|
18
|
-
border: 1px solid var(--cp-c-border);
|
|
106
|
+
border: 1px solid var(--cp-hero-border, var(--cp-c-border));
|
|
19
107
|
border-radius: var(--cp-radius-pill);
|
|
20
|
-
background: var(--cp-c-bg-elv);
|
|
108
|
+
background: var(--cp-hero-surface, var(--cp-c-bg-elv));
|
|
21
109
|
font-family: var(--cp-font-family-mono);
|
|
22
110
|
font-size: 11px;
|
|
23
|
-
color: var(--cp-c-text-2);
|
|
111
|
+
color: var(--cp-hero-text-2, var(--cp-c-text-2));
|
|
24
112
|
text-transform: uppercase;
|
|
25
113
|
letter-spacing: 0.06em;
|
|
26
114
|
margin-bottom: 28px;
|
|
@@ -32,7 +120,7 @@
|
|
|
32
120
|
letter-spacing: -0.04em;
|
|
33
121
|
line-height: 1;
|
|
34
122
|
margin: 0 0 24px;
|
|
35
|
-
color: var(--cp-c-text-1);
|
|
123
|
+
color: var(--cp-hero-text-1, var(--cp-c-text-1));
|
|
36
124
|
}
|
|
37
125
|
|
|
38
126
|
.cp-hero__title .cp-hero__grad {
|
|
@@ -44,7 +132,7 @@
|
|
|
44
132
|
margin: 0 auto 40px;
|
|
45
133
|
font-size: 18px;
|
|
46
134
|
line-height: 1.6;
|
|
47
|
-
color: var(--cp-c-text-2);
|
|
135
|
+
color: var(--cp-hero-text-2, var(--cp-c-text-2));
|
|
48
136
|
}
|
|
49
137
|
|
|
50
138
|
.cp-hero__cta {
|
|
@@ -63,9 +151,9 @@
|
|
|
63
151
|
font-size: 14px;
|
|
64
152
|
font-weight: 500;
|
|
65
153
|
border-radius: var(--cp-radius-pill);
|
|
66
|
-
border: 1px solid var(--cp-c-border);
|
|
154
|
+
border: 1px solid var(--cp-hero-border, var(--cp-c-border));
|
|
67
155
|
background: transparent;
|
|
68
|
-
color: var(--cp-c-text-1);
|
|
156
|
+
color: var(--cp-hero-text-1, var(--cp-c-text-1));
|
|
69
157
|
cursor: pointer;
|
|
70
158
|
transition: all 0.15s ease;
|
|
71
159
|
white-space: nowrap;
|
|
@@ -73,8 +161,8 @@
|
|
|
73
161
|
}
|
|
74
162
|
|
|
75
163
|
.cp-hero__btn:hover {
|
|
76
|
-
background: var(--cp-c-bg-soft);
|
|
77
|
-
border-color: var(--cp-c-border);
|
|
164
|
+
background: var(--cp-hero-soft, var(--cp-c-bg-soft));
|
|
165
|
+
border-color: var(--cp-hero-border, var(--cp-c-border));
|
|
78
166
|
}
|
|
79
167
|
|
|
80
168
|
.cp-hero__btn--primary {
|
|
@@ -94,3 +182,48 @@
|
|
|
94
182
|
margin: 0 auto;
|
|
95
183
|
text-align: left;
|
|
96
184
|
}
|
|
185
|
+
|
|
186
|
+
@media (max-width: 880px) {
|
|
187
|
+
.cp-hero--has-background {
|
|
188
|
+
background-image: var(--cp-hero-background-tablet-image, var(--cp-hero-background-image));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
:where([data-cp-variant='light']) .cp-hero--background-variants {
|
|
192
|
+
background-image: var(
|
|
193
|
+
--cp-hero-background-light-tablet-image,
|
|
194
|
+
var(--cp-hero-background-light-image)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@media (max-width: 640px) {
|
|
200
|
+
.cp-hero {
|
|
201
|
+
padding: 72px 0 48px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.cp-hero--has-background {
|
|
205
|
+
background-image: var(
|
|
206
|
+
--cp-hero-background-mobile-image,
|
|
207
|
+
var(--cp-hero-background-tablet-image, var(--cp-hero-background-image))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
:where([data-cp-variant='light']) .cp-hero--background-variants {
|
|
212
|
+
background-image: var(
|
|
213
|
+
--cp-hero-background-light-mobile-image,
|
|
214
|
+
var(--cp-hero-background-light-tablet-image, var(--cp-hero-background-light-image))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.cp-hero__inner {
|
|
219
|
+
padding: 0 20px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cp-hero__title .cp-hero__grad {
|
|
223
|
+
display: block;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cp-hero__tagline {
|
|
227
|
+
font-size: 16px;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { Hero } from './hero.tsx'
|
|
5
|
+
|
|
6
|
+
// `RouteLink` pulls in Rspress's router, which resolves the `virtual-routes`
|
|
7
|
+
// module only inside a real build. Stand in a plain anchor so link output is
|
|
8
|
+
// assertable.
|
|
9
|
+
vi.mock('@rspress/core/runtime', async () => {
|
|
10
|
+
const react = await import('react')
|
|
11
|
+
return {
|
|
12
|
+
Link: (props: { readonly to: string; readonly children?: React.ReactNode }) =>
|
|
13
|
+
react.createElement('a', { href: props.to }, props.children),
|
|
14
|
+
withBase: (v: string) => v,
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('Hero — background', () => {
|
|
19
|
+
it('should render React content in the background layer', () => {
|
|
20
|
+
const out = renderToStaticMarkup(
|
|
21
|
+
<Hero title="Docs" backgroundContent={<canvas data-testid="field" />} />
|
|
22
|
+
)
|
|
23
|
+
expect(out).toContain('<div class="cp-hero__background" aria-hidden="true"><canvas')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should add the modifier class when a background is configured', () => {
|
|
27
|
+
const out = renderToStaticMarkup(<Hero title="Docs" background={{ src: '/hero.jpg' }} />)
|
|
28
|
+
expect(out).toContain('cp-hero cp-hero--has-background')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should paint the image src into the CSS custom property', () => {
|
|
32
|
+
const out = renderToStaticMarkup(<Hero title="Docs" background={{ src: '/hero.jpg' }} />)
|
|
33
|
+
expect(out).toContain('--cp-hero-background-image:url("/hero.jpg")')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should use CSS defaults when sizing fields are omitted', () => {
|
|
37
|
+
const out = renderToStaticMarkup(<Hero title="Docs" background={{ src: '/hero.jpg' }} />)
|
|
38
|
+
expect(out).toContain('--cp-hero-background-position:center')
|
|
39
|
+
expect(out).toContain('--cp-hero-background-size:cover')
|
|
40
|
+
expect(out).toContain('--cp-hero-background-repeat:no-repeat')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should respect custom sizing fields', () => {
|
|
44
|
+
const out = renderToStaticMarkup(
|
|
45
|
+
<Hero title="Docs" background={{ src: '/hero.jpg', position: '50% 25%', size: 'contain' }} />
|
|
46
|
+
)
|
|
47
|
+
expect(out).toContain('--cp-hero-background-position:50% 25%')
|
|
48
|
+
expect(out).toContain('--cp-hero-background-size:contain')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should add the configured background mode class', () => {
|
|
52
|
+
const out = renderToStaticMarkup(
|
|
53
|
+
<Hero title="Docs" background={{ src: '/hero.jpg', mode: 'dark' }} />
|
|
54
|
+
)
|
|
55
|
+
expect(out).toContain('cp-hero--background-dark')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should paint responsive sources into CSS custom properties', () => {
|
|
59
|
+
const out = renderToStaticMarkup(
|
|
60
|
+
<Hero
|
|
61
|
+
title="Docs"
|
|
62
|
+
background={{
|
|
63
|
+
src: '/hero.jpg',
|
|
64
|
+
sources: { tablet: '/hero-tablet.jpg', mobile: '/hero-mobile.jpg' },
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
expect(out).toContain('--cp-hero-background-tablet-image:url("/hero-tablet.jpg")')
|
|
69
|
+
expect(out).toContain('--cp-hero-background-mobile-image:url("/hero-mobile.jpg")')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should paint dark and light variant sources into CSS custom properties', () => {
|
|
73
|
+
const out = renderToStaticMarkup(
|
|
74
|
+
<Hero
|
|
75
|
+
title="Docs"
|
|
76
|
+
background={{
|
|
77
|
+
dark: { src: '/hero-dark.jpg', sources: { mobile: '/hero-dark-mobile.jpg' } },
|
|
78
|
+
light: { src: '/hero-light.jpg', sources: { mobile: '/hero-light-mobile.jpg' } },
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
expect(out).toContain('cp-hero--background-variants')
|
|
83
|
+
expect(out).toContain('--cp-hero-background-image:url("/hero-dark.jpg")')
|
|
84
|
+
expect(out).toContain('--cp-hero-background-light-image:url("/hero-light.jpg")')
|
|
85
|
+
expect(out).toContain(
|
|
86
|
+
'--cp-hero-background-light-mobile-image:url("/hero-light-mobile.jpg")'
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should not add the modifier when background is absent', () => {
|
|
91
|
+
const out = renderToStaticMarkup(<Hero title="Docs" />)
|
|
92
|
+
expect(out).not.toContain('cp-hero--has-background')
|
|
93
|
+
expect(out).not.toContain('--cp-hero-background-image')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should not render an empty demo wrapper when the demo is suppressed', () => {
|
|
97
|
+
const out = renderToStaticMarkup(<Hero title="Docs" demo={null} />)
|
|
98
|
+
expect(out).not.toContain('cp-hero__demo')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should not paint a background when src is an empty string', () => {
|
|
102
|
+
const out = renderToStaticMarkup(<Hero title="Docs" background={{ src: '' }} />)
|
|
103
|
+
expect(out).not.toContain('cp-hero--has-background')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -3,6 +3,7 @@ import type React from 'react'
|
|
|
3
3
|
|
|
4
4
|
import { RouteLink } from '../../lib/route-link.tsx'
|
|
5
5
|
import { safeUrl } from '../../lib/safe-url.ts'
|
|
6
|
+
import { withMountBase } from '../../lib/with-mount-base.ts'
|
|
6
7
|
|
|
7
8
|
import './hero.css'
|
|
8
9
|
|
|
@@ -21,6 +22,55 @@ export interface HeroAction {
|
|
|
21
22
|
readonly theme?: 'brand' | 'alt'
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** Background image source and responsive rendering controls. */
|
|
26
|
+
export interface HeroBackgroundSource {
|
|
27
|
+
/**
|
|
28
|
+
* Image URL or path. Runs through {@link withMountBase} so paths on a
|
|
29
|
+
* mounted `base` still resolve.
|
|
30
|
+
*/
|
|
31
|
+
readonly src: string
|
|
32
|
+
/**
|
|
33
|
+
* Optional image overrides for the 880px tablet and 640px mobile
|
|
34
|
+
* breakpoints.
|
|
35
|
+
*/
|
|
36
|
+
readonly sources?: {
|
|
37
|
+
readonly tablet?: string
|
|
38
|
+
readonly mobile?: string
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* CSS `background-position` value (e.g. `'center'`). Default `'center'`.
|
|
42
|
+
*/
|
|
43
|
+
readonly position?: string
|
|
44
|
+
/**
|
|
45
|
+
* CSS `background-size` value. Default `'cover'`.
|
|
46
|
+
*/
|
|
47
|
+
readonly size?: string
|
|
48
|
+
/**
|
|
49
|
+
* CSS `background-repeat` value. Default `'no-repeat'`.
|
|
50
|
+
*/
|
|
51
|
+
readonly repeat?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A single hero background image with fixed foreground contrast. */
|
|
55
|
+
export interface HeroBackgroundImage extends HeroBackgroundSource {
|
|
56
|
+
/**
|
|
57
|
+
* Color mode used for the hero foreground and scrim. When omitted, the
|
|
58
|
+
* active site variant is used.
|
|
59
|
+
*/
|
|
60
|
+
readonly mode?: 'dark' | 'light'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Hero background sources selected from the active site variant. */
|
|
64
|
+
export interface HeroBackgroundVariants {
|
|
65
|
+
/** Artwork displayed in dark mode. */
|
|
66
|
+
readonly dark: HeroBackgroundSource
|
|
67
|
+
/** Artwork displayed in light mode. */
|
|
68
|
+
readonly light: HeroBackgroundSource
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Background image painted behind the hero copy. */
|
|
72
|
+
export type HeroBackground = HeroBackgroundImage | HeroBackgroundVariants
|
|
73
|
+
|
|
24
74
|
export interface HeroProps {
|
|
25
75
|
/**
|
|
26
76
|
* Optional eyebrow chip (e.g. "★ 2.1k stars · v0.4.2").
|
|
@@ -43,22 +93,39 @@ export interface HeroProps {
|
|
|
43
93
|
* Optional visual block beneath the CTAs (terminal demo, screenshot, etc).
|
|
44
94
|
*/
|
|
45
95
|
readonly demo?: React.ReactNode
|
|
96
|
+
/**
|
|
97
|
+
* Optional background image painted behind the hero copy.
|
|
98
|
+
*/
|
|
99
|
+
readonly background?: HeroBackground
|
|
100
|
+
/**
|
|
101
|
+
* Optional React content painted behind the hero copy. Intended for
|
|
102
|
+
* procedural canvas, WebGL, or inline SVG backgrounds.
|
|
103
|
+
*/
|
|
104
|
+
readonly backgroundContent?: React.ReactNode
|
|
46
105
|
}
|
|
47
106
|
|
|
48
107
|
/**
|
|
49
108
|
* Hero — landing-page hero with eyebrow chip, gradient-friendly headline,
|
|
50
|
-
* tagline, CTAs,
|
|
51
|
-
* grid backdrop are baked in via CSS and
|
|
109
|
+
* tagline, CTAs, an optional demo slot, and an optional background image.
|
|
110
|
+
* The radial accent glow + dotted grid backdrop are baked in via CSS and
|
|
111
|
+
* adapt to the active theme.
|
|
52
112
|
*
|
|
53
113
|
* @param props - Hero configuration.
|
|
54
114
|
* @returns React element.
|
|
55
115
|
*/
|
|
56
116
|
export function Hero(props: HeroProps): React.ReactElement {
|
|
57
|
-
const { eyebrow, title, tagline, actions, demo } = props
|
|
117
|
+
const { eyebrow, title, tagline, actions, demo, background, backgroundContent } = props
|
|
58
118
|
const list = actions ?? []
|
|
59
119
|
|
|
60
120
|
return (
|
|
61
|
-
<section className=
|
|
121
|
+
<section className={heroClassName(background)} style={heroBackgroundStyle(background)}>
|
|
122
|
+
{match(backgroundContent)
|
|
123
|
+
.with(undefined, () => null)
|
|
124
|
+
.otherwise((content) => (
|
|
125
|
+
<div className="cp-hero__background" aria-hidden="true">
|
|
126
|
+
{content}
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
62
129
|
<div className="cp-hero__inner">
|
|
63
130
|
{match(eyebrow)
|
|
64
131
|
.with(undefined, () => null)
|
|
@@ -77,6 +144,7 @@ export function Hero(props: HeroProps): React.ReactElement {
|
|
|
77
144
|
<div className="cp-hero__cta">{list.map(renderAction)}</div>
|
|
78
145
|
))}
|
|
79
146
|
{match(demo)
|
|
147
|
+
.with(null, () => null)
|
|
80
148
|
.with(undefined, () => null)
|
|
81
149
|
.otherwise((d) => (
|
|
82
150
|
<div className="cp-hero__demo">{d}</div>
|
|
@@ -86,6 +154,135 @@ export function Hero(props: HeroProps): React.ReactElement {
|
|
|
86
154
|
)
|
|
87
155
|
}
|
|
88
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Compute the `<section>` className. The modifier class carries the
|
|
159
|
+
* background-image CSS variables' consumers; the base class alone when no
|
|
160
|
+
* background is configured.
|
|
161
|
+
*
|
|
162
|
+
* @private
|
|
163
|
+
* @param background - Hero background config (may be `undefined`)
|
|
164
|
+
* @returns ClassName string
|
|
165
|
+
*/
|
|
166
|
+
function heroClassName(background: HeroBackground | undefined): string {
|
|
167
|
+
if (background === undefined) {
|
|
168
|
+
return 'cp-hero'
|
|
169
|
+
}
|
|
170
|
+
if (isVariantBackground(background)) {
|
|
171
|
+
return 'cp-hero cp-hero--has-background cp-hero--background-variants'
|
|
172
|
+
}
|
|
173
|
+
if (background.src.length === 0) {
|
|
174
|
+
return 'cp-hero'
|
|
175
|
+
}
|
|
176
|
+
if (background.mode === undefined) {
|
|
177
|
+
return 'cp-hero cp-hero--has-background'
|
|
178
|
+
}
|
|
179
|
+
return `cp-hero cp-hero--has-background cp-hero--background-${background.mode}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build the inline CSS custom properties for the hero background. Returns
|
|
184
|
+
* `undefined` when no background is configured, so the `<section>` carries
|
|
185
|
+
* no `style` attr at all in the default case.
|
|
186
|
+
*
|
|
187
|
+
* @private
|
|
188
|
+
* @param background - Hero background config (may be `undefined`)
|
|
189
|
+
* @returns CSS properties keyed as custom properties, or `undefined`
|
|
190
|
+
*/
|
|
191
|
+
function heroBackgroundStyle(
|
|
192
|
+
background: HeroBackground | undefined
|
|
193
|
+
): React.CSSProperties | undefined {
|
|
194
|
+
if (background === undefined) {
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
if (isVariantBackground(background)) {
|
|
198
|
+
const dark = heroBackgroundSourceStyle(background.dark)
|
|
199
|
+
const light = heroBackgroundSourceStyle(background.light)
|
|
200
|
+
return {
|
|
201
|
+
'--cp-hero-background-image': dark.image,
|
|
202
|
+
'--cp-hero-background-position': dark.position,
|
|
203
|
+
'--cp-hero-background-size': dark.size,
|
|
204
|
+
'--cp-hero-background-repeat': dark.repeat,
|
|
205
|
+
'--cp-hero-background-tablet-image': dark.tabletImage,
|
|
206
|
+
'--cp-hero-background-mobile-image': dark.mobileImage,
|
|
207
|
+
'--cp-hero-background-light-image': light.image,
|
|
208
|
+
'--cp-hero-background-light-position': light.position,
|
|
209
|
+
'--cp-hero-background-light-size': light.size,
|
|
210
|
+
'--cp-hero-background-light-repeat': light.repeat,
|
|
211
|
+
'--cp-hero-background-light-tablet-image': light.tabletImage,
|
|
212
|
+
'--cp-hero-background-light-mobile-image': light.mobileImage,
|
|
213
|
+
} as React.CSSProperties
|
|
214
|
+
}
|
|
215
|
+
if (background.src.length === 0) {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
const style = heroBackgroundSourceStyle(background)
|
|
219
|
+
return {
|
|
220
|
+
'--cp-hero-background-image': style.image,
|
|
221
|
+
'--cp-hero-background-position': style.position,
|
|
222
|
+
'--cp-hero-background-size': style.size,
|
|
223
|
+
'--cp-hero-background-repeat': style.repeat,
|
|
224
|
+
'--cp-hero-background-tablet-image': style.tabletImage,
|
|
225
|
+
'--cp-hero-background-mobile-image': style.mobileImage,
|
|
226
|
+
} as React.CSSProperties
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface HeroBackgroundStyleValues {
|
|
230
|
+
readonly image: string
|
|
231
|
+
readonly position: string
|
|
232
|
+
readonly size: string
|
|
233
|
+
readonly repeat: string
|
|
234
|
+
readonly tabletImage: string | undefined
|
|
235
|
+
readonly mobileImage: string | undefined
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Build normalized CSS values for one background source.
|
|
240
|
+
*
|
|
241
|
+
* @private
|
|
242
|
+
* @param source - Background source and rendering controls.
|
|
243
|
+
* @returns Normalized CSS values.
|
|
244
|
+
*/
|
|
245
|
+
function heroBackgroundSourceStyle(source: HeroBackgroundSource): HeroBackgroundStyleValues {
|
|
246
|
+
const sources = source.sources
|
|
247
|
+
return {
|
|
248
|
+
image: `url("${withMountBase(source.src)}")`,
|
|
249
|
+
position: source.position ?? 'center',
|
|
250
|
+
size: source.size ?? 'cover',
|
|
251
|
+
repeat: source.repeat ?? 'no-repeat',
|
|
252
|
+
tabletImage: match(sources)
|
|
253
|
+
.with(undefined, () => undefined)
|
|
254
|
+
.otherwise((responsive) => heroSourceImage(responsive.tablet)),
|
|
255
|
+
mobileImage: match(sources)
|
|
256
|
+
.with(undefined, () => undefined)
|
|
257
|
+
.otherwise((responsive) => heroSourceImage(responsive.mobile)),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Test whether a background provides site-variant sources.
|
|
263
|
+
*
|
|
264
|
+
* @private
|
|
265
|
+
* @param background - Hero background configuration.
|
|
266
|
+
* @returns Whether the background contains dark and light sources.
|
|
267
|
+
*/
|
|
268
|
+
function isVariantBackground(background: HeroBackground): background is HeroBackgroundVariants {
|
|
269
|
+
return 'dark' in background
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Convert an optional responsive source into a base-aware CSS image value.
|
|
274
|
+
*
|
|
275
|
+
* @private
|
|
276
|
+
* @param src - Optional image URL or path.
|
|
277
|
+
* @returns CSS `url()` value, or `undefined` when no override is configured.
|
|
278
|
+
*/
|
|
279
|
+
function heroSourceImage(src: string | undefined): string | undefined {
|
|
280
|
+
if (src === undefined) {
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
return `url("${withMountBase(src)}")`
|
|
284
|
+
}
|
|
285
|
+
|
|
89
286
|
/**
|
|
90
287
|
* Render a single hero CTA action.
|
|
91
288
|
*
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
.cp-home-terminal {
|
|
2
|
+
width: 100%;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
background: var(--cp-c-term-bg);
|
|
6
|
+
color: var(--cp-c-term-text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.cp-home-terminal--framed {
|
|
10
|
+
width: min(920px, 100%);
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
border: 1px solid var(--cp-c-term-border);
|
|
13
|
+
border-radius: var(--cp-radius-lg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.cp-home-terminal__bar {
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-columns: 1fr auto 1fr;
|
|
19
|
+
align-items: center;
|
|
20
|
+
min-height: var(--cp-size-titlebar);
|
|
21
|
+
padding: var(--cp-s-8) var(--cp-s-12);
|
|
22
|
+
border-bottom: 1px solid var(--cp-c-term-border);
|
|
23
|
+
background: var(--cp-c-term-titlebar-bg);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.cp-home-terminal__dots {
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: var(--cp-s-6);
|
|
29
|
+
justify-self: start;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.cp-home-terminal__dot {
|
|
33
|
+
width: var(--cp-size-window-dot);
|
|
34
|
+
height: var(--cp-size-window-dot);
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.cp-home-terminal__dot--close {
|
|
39
|
+
background: var(--cp-c-window-dot-close);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cp-home-terminal__dot--minimize {
|
|
43
|
+
background: var(--cp-c-window-dot-minimize);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cp-home-terminal__dot--maximize {
|
|
47
|
+
background: var(--cp-c-window-dot-maximize);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.cp-home-terminal__title {
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
color: var(--cp-c-term-title);
|
|
53
|
+
font-family: var(--rp-font-family-mono);
|
|
54
|
+
font-size: var(--cp-fs-window-title);
|
|
55
|
+
text-overflow: ellipsis;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.cp-home-terminal__body {
|
|
60
|
+
min-height: 220px;
|
|
61
|
+
margin: 0;
|
|
62
|
+
overflow-x: auto;
|
|
63
|
+
padding: var(--cp-s-20) var(--cp-s-24);
|
|
64
|
+
color: var(--cp-c-term-text);
|
|
65
|
+
font-family: var(--rp-font-family-mono);
|
|
66
|
+
font-size: clamp(11px, 1.2vw, var(--cp-fs-term-body));
|
|
67
|
+
line-height: var(--cp-lh-relaxed);
|
|
68
|
+
overflow-wrap: anywhere;
|
|
69
|
+
white-space: pre-wrap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.cp-home-terminal__prompt {
|
|
73
|
+
color: var(--cp-c-term-prompt-prefix);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.cp-home-terminal__ok {
|
|
77
|
+
color: var(--cp-c-term-green);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cp-home-terminal__info {
|
|
81
|
+
color: var(--cp-c-term-cyan);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cp-home-terminal__cmt {
|
|
85
|
+
color: var(--cp-c-term-muted);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.cp-home-terminal__err {
|
|
89
|
+
color: var(--cp-c-term-red);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (max-width: 640px) {
|
|
93
|
+
.cp-home-terminal__body {
|
|
94
|
+
min-height: 190px;
|
|
95
|
+
padding: var(--cp-s-16);
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { HomeVisualView } from './home-visual.tsx'
|
|
5
|
+
|
|
6
|
+
vi.mock('@rspress/core/theme', () => ({ CodeBlockRuntime: (): null => null }))
|
|
7
|
+
vi.mock('@rspress/core/runtime', () => ({ withBase: (value: string): string => value }))
|
|
8
|
+
|
|
9
|
+
describe('home visual', () => {
|
|
10
|
+
it('should render terminal visuals with dedicated compact chrome', () => {
|
|
11
|
+
expect.assertions(3)
|
|
12
|
+
const output = renderToStaticMarkup(
|
|
13
|
+
<HomeVisualView
|
|
14
|
+
context="tabs"
|
|
15
|
+
visual={{
|
|
16
|
+
type: 'terminal',
|
|
17
|
+
windowTitle: 'ciderpress dev',
|
|
18
|
+
command: 'ciderpress dev',
|
|
19
|
+
lines: [{ kind: 'ok', text: 'ready' }],
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
expect(output).toContain('role="group" aria-label="Terminal output"')
|
|
24
|
+
expect(output).toContain('ciderpress dev')
|
|
25
|
+
expect(output).toContain('ready')
|
|
26
|
+
})
|
|
27
|
+
})
|