Skip to content

Commit 04f9bf4

Browse files
author
Test
committed
feat(website): add v0.14.0 feature cards and clickable feature detail modals
Sync the marketing site with the 0.14.0 release (worktrees explorer, review-by-number, merged-branch cleanup, template preview, setup hooks, multi-remote, GitHub Enterprise, stash rescue) and make every Features card clickable, opening an accessible modal with extended copy, a reserved 16:9 video slot, and a placeholder until demo videos are recorded. Also removes roadmap entries that have since shipped.
1 parent 16af343 commit 04f9bf4

7 files changed

Lines changed: 916 additions & 165 deletions

File tree

website/src/components/FeatureCard/FeatureCard.module.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,25 @@
125125
text-overflow: ellipsis;
126126
min-width: 0;
127127
}
128+
129+
.clickable {
130+
position: relative;
131+
cursor: pointer;
132+
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
133+
}
134+
.clickable:hover {
135+
background: var(--bg-card-hover);
136+
border-color: var(--border);
137+
transform: translateY(-2px);
138+
}
139+
.cardButton { all: unset; font: inherit; color: inherit; cursor: pointer; }
140+
.cardButton::after { content: ''; position: absolute; inset: 0; }
141+
.cardButton:focus-visible::after {
142+
outline: 2px solid var(--accent);
143+
outline-offset: -2px;
144+
border-radius: var(--radius-md);
145+
}
146+
@media (prefers-reduced-motion: reduce) {
147+
.clickable { transition: none; }
148+
.clickable:hover { transform: none; }
149+
}

website/src/components/FeatureCard/FeatureCard.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface FeatureCardProps {
1414
tone?: FeatureCardTone;
1515
compact?: boolean;
1616
titleLevel?: 3 | 4;
17+
onClick?: () => void;
1718
}
1819

1920
export function FeatureCard({
@@ -25,13 +26,15 @@ export function FeatureCard({
2526
tone,
2627
compact = false,
2728
titleLevel = 3,
29+
onClick,
2830
}: FeatureCardProps) {
2931
const commandPaletteShortcut = command ? getCommandPaletteShortcut() : undefined;
3032
const Title = titleLevel === 4 ? 'h4' : 'h3';
3133
const className = [
3234
styles.card,
3335
compact ? styles.compact : '',
3436
tone ? styles[tone] : '',
37+
onClick ? styles.clickable : '',
3538
]
3639
.filter(Boolean)
3740
.join(' ');
@@ -41,7 +44,20 @@ export function FeatureCard({
4144
<div className={styles.cardIcon}>{icon}</div>
4245
<div className={styles.cardBody}>
4346
<div className={styles.cardTitleRow}>
44-
<Title className={styles.cardTitle}>{title}</Title>
47+
<Title className={styles.cardTitle}>
48+
{onClick ? (
49+
<button
50+
type="button"
51+
className={styles.cardButton}
52+
onClick={onClick}
53+
aria-haspopup="dialog"
54+
>
55+
{title}
56+
</button>
57+
) : (
58+
title
59+
)}
60+
</Title>
4561
{tag && <span className={styles.tag}>{tag}</span>}
4662
</div>
4763
<p className={styles.cardDesc}>{description}</p>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
.dialog {
2+
padding: 0;
3+
margin: auto;
4+
border: 1px solid var(--border);
5+
border-radius: var(--radius-lg);
6+
background: var(--bg-card);
7+
color: var(--text-primary);
8+
box-shadow: var(--shadow-lg);
9+
width: min(820px, calc(100vw - 48px));
10+
max-width: none;
11+
max-height: min(85dvh, 860px);
12+
}
13+
.dialog::backdrop {
14+
background: rgba(1, 4, 9, 0.7);
15+
}
16+
.inner {
17+
position: relative;
18+
display: flex;
19+
flex-direction: column;
20+
max-height: inherit;
21+
}
22+
.media {
23+
aspect-ratio: 16 / 9;
24+
flex-shrink: 0;
25+
overflow: hidden;
26+
background: var(--bg-primary);
27+
border-bottom: 1px solid var(--border-subtle);
28+
}
29+
.media video,
30+
.media iframe {
31+
display: block;
32+
width: 100%;
33+
height: 100%;
34+
border: 0;
35+
object-fit: contain;
36+
}
37+
.placeholder {
38+
width: 100%;
39+
height: 100%;
40+
display: flex;
41+
flex-direction: column;
42+
gap: 12px;
43+
align-items: center;
44+
justify-content: center;
45+
border: 1px dashed var(--border);
46+
color: var(--text-muted);
47+
font-size: 14px;
48+
}
49+
.body {
50+
overflow-y: auto;
51+
padding: 28px;
52+
display: grid;
53+
gap: 18px;
54+
}
55+
.close {
56+
position: absolute;
57+
top: 12px;
58+
right: 12px;
59+
z-index: 1;
60+
width: 32px;
61+
height: 32px;
62+
border-radius: 50%;
63+
background: rgba(13, 17, 23, 0.7);
64+
border: 1px solid var(--border);
65+
color: var(--text-secondary);
66+
display: grid;
67+
place-items: center;
68+
cursor: pointer;
69+
}
70+
.close:hover {
71+
color: var(--text-primary);
72+
background: var(--bg-card-hover);
73+
}
74+
@media (prefers-reduced-motion: no-preference) {
75+
.dialog[open] {
76+
animation: modal-in 0.18s ease-out;
77+
}
78+
.dialog[open]::backdrop {
79+
animation: backdrop-in 0.18s ease-out;
80+
}
81+
}
82+
@keyframes modal-in {
83+
from {
84+
opacity: 0;
85+
transform: translateY(10px) scale(0.98);
86+
}
87+
}
88+
@keyframes backdrop-in {
89+
from {
90+
opacity: 0;
91+
}
92+
}
93+
@media (max-width: 768px) {
94+
.dialog {
95+
width: 100vw;
96+
max-height: none;
97+
height: 100dvh;
98+
margin: 0;
99+
border: none;
100+
border-radius: 0;
101+
}
102+
.body {
103+
padding: 24px 16px;
104+
}
105+
}
106+
@media (max-width: 480px) {
107+
.body {
108+
padding: 20px 12px;
109+
}
110+
}
111+
112+
.titleRow {
113+
display: flex;
114+
align-items: center;
115+
gap: 10px;
116+
}
117+
.icon {
118+
font-size: 24px;
119+
line-height: 1;
120+
}
121+
.titleRow h3 {
122+
font-size: 20px;
123+
font-weight: 700;
124+
color: var(--text-primary);
125+
margin: 0;
126+
}
127+
.tag {
128+
padding: 2px 8px;
129+
border-radius: 100px;
130+
background: var(--orange-subtle);
131+
color: var(--orange);
132+
font-size: 11px;
133+
font-weight: 600;
134+
letter-spacing: 0.04em;
135+
flex-shrink: 0;
136+
}
137+
.longDesc {
138+
font-size: 14px;
139+
line-height: 1.6;
140+
color: var(--text-secondary);
141+
}
142+
.body section h4 {
143+
font-size: 12px;
144+
text-transform: uppercase;
145+
letter-spacing: 0.06em;
146+
color: var(--text-muted);
147+
margin: 0 0 10px;
148+
}
149+
.body section ul {
150+
display: grid;
151+
gap: 6px;
152+
margin: 0;
153+
padding-left: 18px;
154+
font-size: 14px;
155+
line-height: 1.6;
156+
color: var(--text-secondary);
157+
}
158+
.body section li::marker {
159+
color: var(--accent);
160+
}
161+
.row {
162+
display: grid;
163+
padding: 4px 10px;
164+
border-radius: var(--radius-sm);
165+
background: var(--bg-secondary);
166+
border: 1px solid var(--border-subtle);
167+
margin-bottom: 6px;
168+
overflow: hidden;
169+
}
170+
.row:last-child {
171+
margin-bottom: 0;
172+
}
173+
.row code {
174+
font-family: var(--font-mono);
175+
font-size: 12px;
176+
color: var(--text-secondary);
177+
white-space: nowrap;
178+
overflow: hidden;
179+
text-overflow: ellipsis;
180+
}
181+
.settingCode {
182+
color: var(--purple);
183+
}
184+
.docsLink {
185+
font-size: 14px;
186+
font-weight: 600;
187+
color: var(--accent);
188+
}
189+
.docsLink:hover {
190+
color: var(--accent-hover);
191+
}
192+
.playCircle {
193+
color: var(--text-muted);
194+
display: grid;
195+
place-items: center;
196+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useEffect, useRef, type MouseEvent } from 'react';
2+
3+
import type { Feature } from '../Features/featuresData';
4+
import styles from './FeatureModal.module.css';
5+
6+
interface FeatureModalProps {
7+
feature: Feature;
8+
onClose: () => void;
9+
}
10+
11+
export function FeatureModal({ feature, onClose }: FeatureModalProps) {
12+
const ref = useRef<HTMLDialogElement>(null);
13+
const { details } = feature;
14+
const { media } = details;
15+
16+
useEffect(() => {
17+
ref.current?.showModal();
18+
document.body.style.overflow = 'hidden';
19+
return () => {
20+
document.body.style.overflow = '';
21+
};
22+
}, []);
23+
24+
const handleBackdropClick = (e: MouseEvent<HTMLDialogElement>) => {
25+
if (e.target === e.currentTarget) {
26+
ref.current?.close();
27+
}
28+
};
29+
30+
return (
31+
<dialog
32+
ref={ref}
33+
className={styles.dialog}
34+
onClose={onClose}
35+
onClick={handleBackdropClick}
36+
aria-labelledby="feature-modal-title"
37+
>
38+
<div className={styles.inner}>
39+
<div className={styles.media}>
40+
{media?.kind === 'video' ? (
41+
<video src={media.src} poster={media.poster} controls playsInline preload="metadata" />
42+
) : media?.kind === 'youtube' ? (
43+
<iframe
44+
src={`https://www.youtube-nocookie.com/embed/${media.src}`}
45+
title={`${feature.title} demo`}
46+
allow="accelerometer; encrypted-media; picture-in-picture; fullscreen"
47+
allowFullScreen
48+
loading="lazy"
49+
/>
50+
) : (
51+
<div className={styles.placeholder}>
52+
<span className={styles.playCircle} aria-hidden="true">
53+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none">
54+
<circle cx="12" cy="12" r="11" stroke="currentColor" strokeWidth="1.5" />
55+
<path d="M10 8.5L16 12L10 15.5V8.5Z" fill="currentColor" />
56+
</svg>
57+
</span>
58+
<span>Demo video coming soon</span>
59+
</div>
60+
)}
61+
</div>
62+
<button
63+
type="button"
64+
className={styles.close}
65+
onClick={() => ref.current?.close()}
66+
aria-label="Close dialog"
67+
>
68+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
69+
<path
70+
d="M6 6L18 18M6 18L18 6"
71+
stroke="currentColor"
72+
strokeWidth="2"
73+
strokeLinecap="round"
74+
/>
75+
</svg>
76+
</button>
77+
<div className={styles.body}>
78+
<header className={styles.titleRow}>
79+
<span className={styles.icon} aria-hidden="true">
80+
{feature.icon}
81+
</span>
82+
<h3 id="feature-modal-title">{feature.title}</h3>
83+
{feature.tag && <span className={styles.tag}>{feature.tag}</span>}
84+
</header>
85+
<div className={styles.longDesc}>{details.longDescription}</div>
86+
<section>
87+
<h4>Highlights</h4>
88+
<ul>
89+
{details.highlights.map((highlight) => (
90+
<li key={highlight}>{highlight}</li>
91+
))}
92+
</ul>
93+
</section>
94+
{details.commands && (
95+
<section>
96+
<h4>Commands</h4>
97+
{details.commands.map((command) => (
98+
<div key={command} className={styles.row}>
99+
<code>{command}</code>
100+
</div>
101+
))}
102+
</section>
103+
)}
104+
{details.settings && (
105+
<section>
106+
<h4>Settings</h4>
107+
{details.settings.map((setting) => (
108+
<div key={setting} className={styles.row}>
109+
<code className={styles.settingCode}>{setting}</code>
110+
</div>
111+
))}
112+
</section>
113+
)}
114+
{details.docsUrl && (
115+
<a href={details.docsUrl} target="_blank" rel="noreferrer" className={styles.docsLink}>
116+
Read the docs →
117+
</a>
118+
)}
119+
</div>
120+
</div>
121+
</dialog>
122+
);
123+
}

0 commit comments

Comments
 (0)