design: implement 3-column horizontal side-by-side layout picker with minimal cards (illustrations only, no description/radio text)

This commit is contained in:
2026-06-08 11:47:01 -07:00
parent f82a0fa0ff
commit 43678aafec
2 changed files with 111 additions and 69 deletions

View File

@@ -1394,7 +1394,8 @@ function EntrepStyle({ productType, value, onChange }) {
}))}
value={value}
onChange={onChange}
columns={styles.length === 3 ? 1 : 2}
columns={styles.length === 3 ? 3 : 2}
minimal
/>
</>
);
@@ -1583,7 +1584,9 @@ export function EntrepreneurPath({
current={step + 2}
total={5}
/>
<WizardBody width={step === 2 || step === 3 ? "wide" : null}>
<WizardBody
width={step === 1 ? "xwide" : step === 2 || step === 3 ? "wide" : null}
>
{body}
<WizardFooter
onNext={next}

View File

@@ -338,6 +338,7 @@ export function PresetGroup({
value,
onChange,
columns = 1,
minimal = false,
}: {
options: Array<{
id: string;
@@ -345,10 +346,12 @@ export function PresetGroup({
desc?: React.ReactNode;
icon?: React.ReactNode;
fullWidth?: boolean;
illustration?: React.ReactNode;
}>;
value?: string;
onChange?: (id: string) => void;
columns?: number;
minimal?: boolean;
}) {
return (
<div
@@ -361,6 +364,8 @@ export function PresetGroup({
>
{options.map((opt) => {
const active = value === opt.id;
const hasIllustration = !!opt.illustration;
return (
<button
key={opt.id}
@@ -368,8 +373,8 @@ export function PresetGroup({
onClick={() => onChange(opt.id)}
style={{
textAlign: "left",
padding: "12px 14px",
borderRadius: 10,
padding: hasIllustration ? "0 0 14px" : "12px 14px",
borderRadius: 14,
border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`,
background: active
? "oklch(0.20 0.04 35 / 0.4)"
@@ -380,93 +385,127 @@ export function PresetGroup({
transition: "border-color .15s, background .15s",
color: "var(--fg)",
display: "flex",
alignItems: "flex-start",
gap: 12,
flexDirection: "column",
alignItems: "stretch",
gridColumn: opt.fullWidth ? "1 / -1" : "auto",
overflow: "hidden",
}}
>
{opt.icon && (
<span
{hasIllustration && (
<div
style={{
width: 28,
height: 28,
flexShrink: 0,
borderRadius: 8,
background: active
? "oklch(0.74 0.175 35 / 0.18)"
: "oklch(0.22 0.011 60)",
border: "1px solid var(--hairline)",
color: active ? "var(--accent)" : "var(--fg-mute)",
width: "100%",
height: 104,
background: "oklch(0.14 0.008 60 / 0.7)",
borderBottom: "1px solid var(--hairline)",
display: "grid",
placeItems: "center",
fontSize: 14,
marginTop: 1,
overflow: "hidden",
position: "relative",
}}
>
{opt.icon}
</span>
{opt.illustration}
</div>
)}
<span
<div
style={{
display: "flex",
flexDirection: "column",
gap: 2,
alignItems: "center",
justifyContent: minimal ? "center" : "flex-start",
gap: 12,
padding: hasIllustration ? "14px 14px 0" : "0",
width: "100%",
flex: 1,
minWidth: 0,
}}
>
<span
style={{
fontSize: 14,
fontWeight: 500,
letterSpacing: "-0.005em",
}}
>
{opt.label}
</span>
{opt.desc && (
{opt.icon && !minimal && (
<span
style={{
fontSize: 12.5,
color: "var(--fg-mute)",
lineHeight: 1.45,
width: 28,
height: 28,
flexShrink: 0,
borderRadius: 8,
background: active
? "oklch(0.74 0.175 35 / 0.18)"
: "oklch(0.22 0.011 60)",
border: "1px solid var(--hairline)",
color: active ? "var(--accent)" : "var(--fg-mute)",
display: "grid",
placeItems: "center",
fontSize: 14,
marginTop: 1,
}}
>
{opt.desc}
{opt.icon}
</span>
)}
</span>
<span
style={{
width: 16,
height: 16,
borderRadius: "50%",
background: active ? "var(--accent)" : "transparent",
border: `1.5px solid ${active ? "var(--accent)" : "var(--hairline-2)"}`,
display: "grid",
placeItems: "center",
color: "var(--accent-fg)",
flexShrink: 0,
marginTop: 6,
transition: "border-color .15s, background .15s",
}}
>
{active && (
<svg
width="9"
height="9"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
<span
style={{
display: "flex",
flexDirection: "column",
gap: 2,
flex: 1,
minWidth: 0,
textAlign: minimal ? "center" : "left",
}}
>
<span
style={{
fontSize: 13.5,
fontWeight: 500,
letterSpacing: "-0.005em",
color: active ? "var(--fg)" : "var(--fg-dim)",
}}
>
<path d="m3 8.5 3.2 3.2L13 5" />
</svg>
{opt.label}
</span>
{opt.desc && !minimal && (
<span
style={{
fontSize: 12.5,
color: "var(--fg-mute)",
lineHeight: 1.45,
}}
>
{opt.desc}
</span>
)}
</span>
{!minimal && (
<span
style={{
width: 16,
height: 16,
borderRadius: "50%",
background: active ? "var(--accent)" : "transparent",
border: `1.5px solid ${active ? "var(--accent)" : "var(--hairline-2)"}`,
display: "grid",
placeItems: "center",
color: "var(--accent-fg)",
flexShrink: 0,
marginTop: 2,
transition: "border-color .15s, background .15s",
}}
>
{active && (
<svg
width="9"
height="9"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m3 8.5 3.2 3.2L13 5" />
</svg>
)}
</span>
)}
</span>
</div>
</button>
);
})}