Number Card and Form Component Updates #37
+221
-599
@@ -1,29 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Tooltip from "../components/Tooltip";
|
import TextInput from "../components/TextInput";
|
||||||
import Alert from "../components/Alert";
|
import Checkbox from "../components/Checkbox";
|
||||||
import Button from "../components/Button";
|
import CheckboxGroup from "../components/CheckboxGroup";
|
||||||
import Stepper from "../components/Stepper";
|
import RadioGroup from "../components/RadioGroup";
|
||||||
import Progress from "../components/Progress";
|
|
||||||
import Create from "../components/Create";
|
|
||||||
import Input from "../components/Input";
|
|
||||||
import InputWithCounter from "../components/InputWithCounter";
|
|
||||||
import IconCard from "../components/IconCard";
|
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
|
||||||
|
|
||||||
export default function ComponentsPreview() {
|
export default function ComponentsPreview() {
|
||||||
const [alertVisible, setAlertVisible] = useState({
|
const [defaultInputValue, setDefaultInputValue] = useState("");
|
||||||
default: true,
|
const [activeInputValue, setActiveInputValue] = useState("");
|
||||||
positive: true,
|
const [errorInputValue, setErrorInputValue] = useState("");
|
||||||
warning: true,
|
const [standardCheckbox, setStandardCheckbox] = useState(false);
|
||||||
danger: true,
|
const [inverseCheckbox, setInverseCheckbox] = useState(false);
|
||||||
banner: true,
|
const [checkboxGroupValues, setCheckboxGroupValues] = useState<string[]>([]);
|
||||||
});
|
const [radioValue, setRadioValue] = useState("");
|
||||||
|
const [inverseRadioValue, setInverseRadioValue] = useState("");
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
|
||||||
const [createStep, setCreateStep] = useState(1);
|
|
||||||
const [policyName, setPolicyName] = useState("");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
|
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
|
||||||
@@ -37,600 +28,231 @@ export default function ComponentsPreview() {
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Button Section */}
|
{/* Text Input Section */}
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
Button Component
|
Text Input Component
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
<div className="space-y-[var(--spacing-scale-016)]">
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
All Variants
|
States
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)]">
|
|
||||||
<Button variant="filled" size="medium">
|
|
||||||
Filled
|
|
||||||
</Button>
|
|
||||||
<Button variant="filled-inverse" size="medium">
|
|
||||||
Filled Inverse
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="medium">
|
|
||||||
Outline
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline-inverse" size="medium">
|
|
||||||
Outline Inverse
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="medium">
|
|
||||||
Ghost
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost-inverse" size="medium">
|
|
||||||
Ghost Inverse
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger" size="medium">
|
|
||||||
Danger
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger-inverse" size="medium">
|
|
||||||
Danger Inverse
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
All Sizes - Danger Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)] items-center">
|
|
||||||
<Button variant="danger" size="xsmall">
|
|
||||||
XSmall
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger" size="small">
|
|
||||||
Small
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger" size="medium">
|
|
||||||
Medium
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger" size="large">
|
|
||||||
Large
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger" size="xlarge">
|
|
||||||
XLarge
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
All Sizes - Danger Inverse Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)] items-center">
|
|
||||||
<Button variant="danger-inverse" size="xsmall">
|
|
||||||
XSmall
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger-inverse" size="small">
|
|
||||||
Small
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger-inverse" size="medium">
|
|
||||||
Medium
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger-inverse" size="large">
|
|
||||||
Large
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger-inverse" size="xlarge">
|
|
||||||
XLarge
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
All Sizes - Ghost Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)] items-center">
|
|
||||||
<Button variant="ghost" size="xsmall">
|
|
||||||
XSmall
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="small">
|
|
||||||
Small
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="medium">
|
|
||||||
Medium
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="large">
|
|
||||||
Large
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="xlarge">
|
|
||||||
XLarge
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
All Sizes - Ghost Inverse Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)] items-center">
|
|
||||||
<Button variant="ghost-inverse" size="xsmall">
|
|
||||||
XSmall
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost-inverse" size="small">
|
|
||||||
Small
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost-inverse" size="medium">
|
|
||||||
Medium
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost-inverse" size="large">
|
|
||||||
Large
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost-inverse" size="xlarge">
|
|
||||||
XLarge
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
States - Danger Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)]">
|
|
||||||
<Button variant="danger" size="medium">
|
|
||||||
Normal
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger" size="medium" disabled>
|
|
||||||
Disabled
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
States - Danger Inverse Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)]">
|
|
||||||
<Button variant="danger-inverse" size="medium">
|
|
||||||
Normal
|
|
||||||
</Button>
|
|
||||||
<Button variant="danger-inverse" size="medium" disabled>
|
|
||||||
Disabled
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
States - Ghost Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)]">
|
|
||||||
<Button variant="ghost" size="medium">
|
|
||||||
Normal
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="medium" disabled>
|
|
||||||
Disabled
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
|
||||||
States - Ghost Inverse Variant
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-012)]">
|
|
||||||
<Button variant="ghost-inverse" size="medium">
|
|
||||||
Normal
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost-inverse" size="medium" disabled>
|
|
||||||
Disabled
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Tooltip Section */}
|
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
|
||||||
Tooltip Component
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-024)] items-center">
|
|
||||||
<Tooltip text="Tooltip positioned at top" position="top">
|
|
||||||
<Button variant="filled" size="medium">
|
|
||||||
Hover me (Top)
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip text="Tooltip positioned at bottom" position="bottom">
|
|
||||||
<Button variant="filled-inverse" size="medium">
|
|
||||||
Hover me (Bottom)
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip text="Disabled tooltip" disabled>
|
|
||||||
<Button variant="ghost" size="medium">
|
|
||||||
Disabled Tooltip
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip text="Tooltip with icon button" position="top">
|
|
||||||
<button className="p-[var(--spacing-scale-012)] rounded-full hover:bg-[var(--color-surface-default-tertiary)] transition-colors">
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10 9V11M10 15H10.01M19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Alert Section */}
|
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
|
||||||
Alert Component
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
{/* Toast Alerts */}
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-016)]">
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
|
||||||
Toast Alerts
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{alertVisible.default && (
|
|
||||||
<Alert
|
|
||||||
title="Short alert banner message goes here"
|
|
||||||
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
|
||||||
status="default"
|
|
||||||
type="toast"
|
|
||||||
onClose={() =>
|
|
||||||
setAlertVisible({ ...alertVisible, default: false })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{alertVisible.positive && (
|
|
||||||
<Alert
|
|
||||||
title="Short alert banner message goes here"
|
|
||||||
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
|
||||||
status="positive"
|
|
||||||
type="toast"
|
|
||||||
onClose={() =>
|
|
||||||
setAlertVisible({ ...alertVisible, positive: false })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{alertVisible.warning && (
|
|
||||||
<Alert
|
|
||||||
title="Short alert banner message goes here"
|
|
||||||
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
|
||||||
status="warning"
|
|
||||||
type="toast"
|
|
||||||
onClose={() =>
|
|
||||||
setAlertVisible({ ...alertVisible, warning: false })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{alertVisible.danger && (
|
|
||||||
<Alert
|
|
||||||
title="Short alert banner message goes here"
|
|
||||||
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
|
||||||
status="danger"
|
|
||||||
type="toast"
|
|
||||||
onClose={() =>
|
|
||||||
setAlertVisible({ ...alertVisible, danger: false })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Banner Alerts */}
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-016)]">
|
|
||||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
|
||||||
Banner Alerts
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{alertVisible.banner && (
|
|
||||||
<Alert
|
|
||||||
title="Short alert banner message goes here"
|
|
||||||
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
|
||||||
status="default"
|
|
||||||
type="banner"
|
|
||||||
onClose={() =>
|
|
||||||
setAlertVisible({ ...alertVisible, banner: false })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
title="Positive banner alert"
|
|
||||||
description="This is a positive banner message"
|
|
||||||
status="positive"
|
|
||||||
type="banner"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
title="Warning banner alert"
|
|
||||||
description="This is a warning banner message"
|
|
||||||
status="warning"
|
|
||||||
type="banner"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
title="Danger banner alert"
|
|
||||||
description="This is a danger banner message"
|
|
||||||
status="danger"
|
|
||||||
type="banner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Stepper Section */}
|
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
|
||||||
Stepper Component
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
|
||||||
<div className="space-y-[var(--spacing-scale-016)]">
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Step 1 of 5
|
|
||||||
</p>
|
|
||||||
<Stepper active={1} totalSteps={5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Step 2 of 5
|
|
||||||
</p>
|
|
||||||
<Stepper active={2} totalSteps={5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Step 3 of 5
|
|
||||||
</p>
|
|
||||||
<Stepper active={3} totalSteps={5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Step 4 of 5
|
|
||||||
</p>
|
|
||||||
<Stepper active={4} totalSteps={5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Step 5 of 5
|
|
||||||
</p>
|
|
||||||
<Stepper active={5} totalSteps={5} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Progress Section */}
|
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
|
||||||
Progress Component
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
|
||||||
<div className="space-y-[var(--spacing-scale-016)]">
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 1-0
|
|
||||||
</p>
|
|
||||||
<Progress progress="1-0" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 1-1
|
|
||||||
</p>
|
|
||||||
<Progress progress="1-1" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 1-2
|
|
||||||
</p>
|
|
||||||
<Progress progress="1-2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 1-3
|
|
||||||
</p>
|
|
||||||
<Progress progress="1-3" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 1-4
|
|
||||||
</p>
|
|
||||||
<Progress progress="1-4" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 1-5
|
|
||||||
</p>
|
|
||||||
<Progress progress="1-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 2-0
|
|
||||||
</p>
|
|
||||||
<Progress progress="2-0" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 2-1
|
|
||||||
</p>
|
|
||||||
<Progress progress="2-1" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 2-2
|
|
||||||
</p>
|
|
||||||
<Progress progress="2-2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 3-0
|
|
||||||
</p>
|
|
||||||
<Progress progress="3-0" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 3-1
|
|
||||||
</p>
|
|
||||||
<Progress progress="3-1" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
|
||||||
Progress: 3-2
|
|
||||||
</p>
|
|
||||||
<Progress progress="3-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Create Component Section */}
|
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
|
||||||
Create Component
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
|
||||||
<div className="space-y-[var(--spacing-scale-016)]">
|
|
||||||
<Button
|
|
||||||
variant="filled-inverse"
|
|
||||||
size="medium"
|
|
||||||
onClick={() => setCreateOpen(true)}
|
|
||||||
>
|
|
||||||
Open Create Dialog
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="space-y-[var(--spacing-scale-008)]">
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
|
|
||||||
Step {createStep} of 3
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setCreateStep((prev) => Math.max(1, prev - 1))}
|
|
||||||
disabled={createStep === 1}
|
|
||||||
>
|
|
||||||
Previous Step
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setCreateStep((prev) => Math.min(3, prev + 1))}
|
|
||||||
disabled={createStep === 3}
|
|
||||||
>
|
|
||||||
Next Step
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Create
|
|
||||||
isOpen={createOpen}
|
|
||||||
onClose={() => setCreateOpen(false)}
|
|
||||||
title={
|
|
||||||
createStep === 1
|
|
||||||
? "What do you call your group's new policy?"
|
|
||||||
: createStep === 2
|
|
||||||
? "How should conflicts be resolved?"
|
|
||||||
: "Review your policy"
|
|
||||||
}
|
|
||||||
description="You can also combine or add new approaches to the list"
|
|
||||||
showBackButton={true}
|
|
||||||
showNextButton={true}
|
|
||||||
onBack={() => setCreateStep((prev) => Math.max(1, prev - 1))}
|
|
||||||
onNext={() => setCreateStep((prev) => Math.min(3, prev + 1))}
|
|
||||||
backButtonText="Back"
|
|
||||||
nextButtonText={createStep === 3 ? "Finish" : "Next"}
|
|
||||||
nextButtonDisabled={createStep === 1 && !policyName.trim()}
|
|
||||||
currentStep={createStep}
|
|
||||||
totalSteps={3}
|
|
||||||
>
|
|
||||||
<div className="space-y-[var(--spacing-scale-024)]">
|
|
||||||
{createStep === 1 && (
|
|
||||||
<InputWithCounter
|
|
||||||
label="Label"
|
|
||||||
placeholder="Policy name"
|
|
||||||
value={policyName}
|
|
||||||
onChange={setPolicyName}
|
|
||||||
maxLength={48}
|
|
||||||
showHelpIcon
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{createStep === 2 && (
|
|
||||||
<div className="space-y-[var(--spacing-scale-008)]">
|
|
||||||
<Input
|
|
||||||
label="Conflict Resolution Method"
|
|
||||||
placeholder="Enter method"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-primary)]">
|
|
||||||
Select how conflicts should be resolved in your group.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{createStep === 3 && (
|
|
||||||
<div className="space-y-[var(--spacing-scale-016)]">
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
<p className="font-inter text-[16px] leading-[24px] text-[var(--color-content-default-primary)]">
|
<TextInput
|
||||||
Review your policy configuration before finalizing.
|
label="Default Text Input"
|
||||||
</p>
|
placeholder="Enter text"
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-200,8px)] p-[var(--spacing-scale-016)]">
|
value={defaultInputValue}
|
||||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
|
onChange={(e) => setDefaultInputValue(e.target.value)}
|
||||||
Policy details will appear here
|
/>
|
||||||
</p>
|
<TextInput
|
||||||
|
label="Interactive Text Input (click = active, tab = focus)"
|
||||||
|
placeholder="Enter text"
|
||||||
|
value={activeInputValue}
|
||||||
|
onChange={(e) => setActiveInputValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Disabled Text Input"
|
||||||
|
placeholder="Enter text"
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Error Text Input"
|
||||||
|
placeholder="Enter text"
|
||||||
|
value={errorInputValue}
|
||||||
|
onChange={(e) => setErrorInputValue(e.target.value)}
|
||||||
|
error
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Checkbox Section */}
|
||||||
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Checkbox Component
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
|
Standard Mode
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<Checkbox
|
||||||
|
label="Standard Checkbox"
|
||||||
|
checked={standardCheckbox}
|
||||||
|
mode="standard"
|
||||||
|
onChange={({ checked }) => setStandardCheckbox(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
|
Inverse Mode
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<Checkbox
|
||||||
|
label="Inverse Checkbox"
|
||||||
|
checked={inverseCheckbox}
|
||||||
|
mode="inverse"
|
||||||
|
onChange={({ checked }) => setInverseCheckbox(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Checkbox Group Section */}
|
||||||
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Checkbox Group Component
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
|
Standard Mode
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<CheckboxGroup
|
||||||
|
name="standard-checkbox-group"
|
||||||
|
value={checkboxGroupValues}
|
||||||
|
onChange={({ value }) => setCheckboxGroupValues(value)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{
|
||||||
|
value: "option2",
|
||||||
|
label: "Checkbox label",
|
||||||
|
subtext: "Nunc sed hendrerit consequat.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
|
Inverse Mode
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<CheckboxGroup
|
||||||
|
name="inverse-checkbox-group"
|
||||||
|
value={checkboxGroupValues}
|
||||||
|
onChange={({ value }) => setCheckboxGroupValues(value)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option3", label: "Checkbox label" },
|
||||||
|
{
|
||||||
|
value: "option4",
|
||||||
|
label: "Checkbox label",
|
||||||
|
subtext: "Nunc sed hendrerit consequat.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</Create>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* IconCard Component Section */}
|
{/* Radio Group Section */}
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
IconCard Component
|
Radio Group Component
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-024)]">
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
<IconCard
|
<div>
|
||||||
icon={
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
<img
|
Standard Mode
|
||||||
src={getAssetPath("assets/Vector_WorkerCoop.svg")}
|
</h3>
|
||||||
alt=""
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
className="w-[36px] h-[36px]"
|
<RadioGroup
|
||||||
width="36"
|
name="default-radio"
|
||||||
height="36"
|
value={radioValue}
|
||||||
|
onChange={({ value }) => setRadioValue(value)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
}
|
<RadioGroup
|
||||||
title="Worker's cooperatives"
|
name="interactive-radio"
|
||||||
description="Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations."
|
value={radioValue}
|
||||||
onClick={() => {
|
onChange={({ value }) => setRadioValue(value)}
|
||||||
// IconCard clicked handler
|
mode="standard"
|
||||||
}}
|
options={[
|
||||||
/>
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<RadioGroup
|
||||||
|
name="disabled-radio"
|
||||||
|
value=""
|
||||||
|
mode="standard"
|
||||||
|
disabled
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-012)]">
|
||||||
|
Inverse Mode
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<RadioGroup
|
||||||
|
name="inverse-default-radio"
|
||||||
|
value={inverseRadioValue}
|
||||||
|
onChange={({ value }) => setInverseRadioValue(value)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<RadioGroup
|
||||||
|
name="inverse-interactive-radio"
|
||||||
|
value={inverseRadioValue}
|
||||||
|
onChange={({ value }) => setInverseRadioValue(value)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<RadioGroup
|
||||||
|
name="inverse-disabled-radio"
|
||||||
|
value=""
|
||||||
|
mode="inverse"
|
||||||
|
disabled
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -21,49 +21,59 @@ const CheckboxContainer = memo<CheckboxProps>(
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const isInverse = mode === "inverse";
|
const isInverse = mode === "inverse";
|
||||||
|
const isStandard = mode === "standard";
|
||||||
|
|
||||||
// Base tokens (rough placeholders leveraging existing CSS variables)
|
// Generate unique ID for accessibility if not provided
|
||||||
const colorContent = isInverse
|
const { id: checkboxId, labelId } = useComponentId("checkbox", id);
|
||||||
? "var(--color-content-inverse-primary)"
|
|
||||||
: "var(--color-content-default-primary)";
|
|
||||||
|
|
||||||
// Visual container depending on state
|
// Base box styles per Figma
|
||||||
const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`;
|
const baseBox = `
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
shrink-0
|
||||||
|
w-[24px]
|
||||||
|
h-[24px]
|
||||||
|
rounded-[4px]
|
||||||
|
transition-all
|
||||||
|
duration-200
|
||||||
|
ease-in-out
|
||||||
|
`.trim().replace(/\s+/g, " ");
|
||||||
|
|
||||||
const stateStyles: Record<string, string> = {
|
// Get box styles based on state and checked status per Figma designs
|
||||||
default: "",
|
const getBoxStyles = (): string => {
|
||||||
hover: "",
|
// Standard mode styles
|
||||||
focus: "",
|
if (isStandard) {
|
||||||
|
// Default state: tertiary border, with hover and focus states via CSS
|
||||||
|
// Hover changes border to brand primary color
|
||||||
|
// Focus removes border and shows shadow (double ring: 2px white inner, 4px dark outer)
|
||||||
|
return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid border-[var(--color-border-default-tertiary,#464646)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] focus:border-transparent focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse mode styles per Figma
|
||||||
|
if (isInverse) {
|
||||||
|
// Inverse: transparent background, white border
|
||||||
|
// Hover changes border to brand primary color
|
||||||
|
// Focus shows shadow (2px dark inner, 4px white outer) - note: reversed from standard
|
||||||
|
return `${baseBox} bg-transparent border border-solid border-[var(--color-border-invert-primary,white)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseBox;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Background behavior:
|
const combinedBoxStyles = getBoxStyles();
|
||||||
// - Standard: background does not change on check; only checkmark appears
|
|
||||||
// - Inverse: transparent background, checkmark appears on check
|
// Checkmark color per Figma
|
||||||
const backgroundWhenChecked = isInverse
|
|
||||||
? "var(--color-surface-default-transparent)"
|
|
||||||
: "var(--color-surface-default-primary)";
|
|
||||||
const checkGlyphColor = checked
|
const checkGlyphColor = checked
|
||||||
? isInverse
|
? isStandard
|
||||||
? "var(--color-content-inverse-primary)"
|
? "var(--color-content-default-brand-primary, #fefcc9)" // Light yellow/cream for standard mode
|
||||||
: "var(--color-border-default-brand-primary)"
|
: "var(--color-content-inverse-primary, #000000)" // Black for inverse mode
|
||||||
: "transparent";
|
: "transparent";
|
||||||
const labelColor = colorContent;
|
|
||||||
|
|
||||||
const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`;
|
// Label color
|
||||||
|
const labelColor = isInverse
|
||||||
// Force visible outline for standard / default / unchecked
|
? "var(--color-content-inverse-primary)"
|
||||||
// Outline classes instead of inline styles so hover can override
|
: "var(--color-content-default-primary)";
|
||||||
const defaultOutlineClass = isInverse
|
|
||||||
? "outline outline-1 outline-[var(--color-border-inverse-primary)]"
|
|
||||||
: "outline outline-1 outline-[var(--color-border-default-tertiary)]";
|
|
||||||
|
|
||||||
// Apply brand outline only on actual :hover, and only when standard/unchecked
|
|
||||||
const conditionalHoverOutlineClass =
|
|
||||||
"hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]";
|
|
||||||
|
|
||||||
// Focus state for standard/unchecked with brand primary color and specific blur/spread
|
|
||||||
const conditionalFocusClass =
|
|
||||||
"focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]";
|
|
||||||
|
|
||||||
const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => {
|
const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
@@ -74,9 +84,6 @@ const CheckboxContainer = memo<CheckboxProps>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate unique ID for accessibility if not provided
|
|
||||||
const { id: checkboxId, labelId } = useComponentId("checkbox", id);
|
|
||||||
|
|
||||||
const accessibilityProps = {
|
const accessibilityProps = {
|
||||||
role: "checkbox" as const,
|
role: "checkbox" as const,
|
||||||
"aria-checked": checked,
|
"aria-checked": checked,
|
||||||
@@ -107,10 +114,6 @@ const CheckboxContainer = memo<CheckboxProps>(
|
|||||||
value={value}
|
value={value}
|
||||||
className={className}
|
className={className}
|
||||||
combinedBoxStyles={combinedBoxStyles}
|
combinedBoxStyles={combinedBoxStyles}
|
||||||
defaultOutlineClass={defaultOutlineClass}
|
|
||||||
conditionalHoverOutlineClass={conditionalHoverOutlineClass}
|
|
||||||
conditionalFocusClass={conditionalFocusClass}
|
|
||||||
backgroundWhenChecked={backgroundWhenChecked}
|
|
||||||
checkGlyphColor={checkGlyphColor}
|
checkGlyphColor={checkGlyphColor}
|
||||||
labelColor={labelColor}
|
labelColor={labelColor}
|
||||||
accessibilityProps={accessibilityProps}
|
accessibilityProps={accessibilityProps}
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ export interface CheckboxViewProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
className: string;
|
className: string;
|
||||||
combinedBoxStyles: string;
|
combinedBoxStyles: string;
|
||||||
defaultOutlineClass: string;
|
|
||||||
conditionalHoverOutlineClass: string;
|
|
||||||
conditionalFocusClass: string;
|
|
||||||
backgroundWhenChecked: string;
|
|
||||||
checkGlyphColor: string;
|
checkGlyphColor: string;
|
||||||
labelColor: string;
|
labelColor: string;
|
||||||
accessibilityProps: React.HTMLAttributes<HTMLSpanElement>;
|
accessibilityProps: React.HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ export function CheckboxView({
|
|||||||
value,
|
value,
|
||||||
className,
|
className,
|
||||||
combinedBoxStyles,
|
combinedBoxStyles,
|
||||||
defaultOutlineClass,
|
|
||||||
conditionalHoverOutlineClass,
|
|
||||||
conditionalFocusClass,
|
|
||||||
backgroundWhenChecked,
|
|
||||||
checkGlyphColor,
|
checkGlyphColor,
|
||||||
labelColor,
|
labelColor,
|
||||||
accessibilityProps,
|
accessibilityProps,
|
||||||
@@ -30,18 +26,16 @@ export function CheckboxView({
|
|||||||
{...accessibilityProps}
|
{...accessibilityProps}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`}
|
className={`${combinedBoxStyles} p-[4px] ${disabled ? "" : "cursor-pointer"}`}
|
||||||
style={{
|
|
||||||
backgroundColor: backgroundWhenChecked,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Simple check glyph */}
|
{/* Checkmark SVG per Figma - 16px size */}
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 12 12"
|
viewBox="0 0 12 12"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
className="block"
|
||||||
>
|
>
|
||||||
<polyline
|
<polyline
|
||||||
points="2.5 6 5 8.5 10 3.5"
|
points="2.5 6 5 8.5 10 3.5"
|
||||||
@@ -63,7 +57,7 @@ export function CheckboxView({
|
|||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* Hidden native input for form compatibility (optional for now) */}
|
{/* Hidden native input for form compatibility */}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={name}
|
name={name}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo, useCallback, useId, useState } from "react";
|
||||||
|
import { CheckboxGroupView } from "./CheckboxGroup.view";
|
||||||
|
import type { CheckboxGroupProps } from "./CheckboxGroup.types";
|
||||||
|
|
||||||
|
const CheckboxGroupContainer = ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
mode = "standard",
|
||||||
|
disabled = false,
|
||||||
|
options = [],
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: CheckboxGroupProps) => {
|
||||||
|
// Generate unique ID for accessibility if not provided
|
||||||
|
const generatedId = useId();
|
||||||
|
const groupId = name || `checkbox-group-${generatedId}`;
|
||||||
|
|
||||||
|
// Internal state to track checked values (only used if value prop is not provided)
|
||||||
|
const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Use controlled value if provided, otherwise use internal state
|
||||||
|
const checkedValues = value !== undefined ? value : internalCheckedValues;
|
||||||
|
|
||||||
|
const handleOptionChange = useCallback(
|
||||||
|
(optionValue: string, checked: boolean) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
const newCheckedValues = checked
|
||||||
|
? [...checkedValues, optionValue]
|
||||||
|
: checkedValues.filter((v) => v !== optionValue);
|
||||||
|
|
||||||
|
// Only update internal state if uncontrolled
|
||||||
|
if (value === undefined) {
|
||||||
|
setInternalCheckedValues(newCheckedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ value: newCheckedValues });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, checkedValues, onChange, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxGroupView
|
||||||
|
groupId={groupId}
|
||||||
|
value={checkedValues}
|
||||||
|
mode={mode}
|
||||||
|
disabled={disabled}
|
||||||
|
options={options}
|
||||||
|
className={className}
|
||||||
|
ariaLabel={props["aria-label"]}
|
||||||
|
onOptionChange={handleOptionChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CheckboxGroupContainer.displayName = "CheckboxGroup";
|
||||||
|
|
||||||
|
export default memo(CheckboxGroupContainer);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export interface CheckboxOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
subtext?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckboxGroupProps {
|
||||||
|
name?: string;
|
||||||
|
value?: string[];
|
||||||
|
onChange?: (_data: { value: string[] }) => void;
|
||||||
|
mode?: "standard" | "inverse";
|
||||||
|
disabled?: boolean;
|
||||||
|
options?: CheckboxOption[];
|
||||||
|
className?: string;
|
||||||
|
"aria-label"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckboxGroupViewProps {
|
||||||
|
groupId: string;
|
||||||
|
value: string[];
|
||||||
|
mode: "standard" | "inverse";
|
||||||
|
disabled: boolean;
|
||||||
|
options: CheckboxOption[];
|
||||||
|
className: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
onOptionChange: (_optionValue: string, _checked: boolean) => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import Checkbox from "../Checkbox";
|
||||||
|
import type { CheckboxGroupViewProps } from "./CheckboxGroup.types";
|
||||||
|
|
||||||
|
export function CheckboxGroupView({
|
||||||
|
groupId,
|
||||||
|
value,
|
||||||
|
mode,
|
||||||
|
disabled,
|
||||||
|
options,
|
||||||
|
className,
|
||||||
|
ariaLabel,
|
||||||
|
onOptionChange,
|
||||||
|
}: CheckboxGroupViewProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`space-y-[8px] ${className}`}
|
||||||
|
role="group"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{options.map((option) => {
|
||||||
|
const isChecked = value.includes(option.value);
|
||||||
|
|
||||||
|
// If there's subtext, render checkbox without label and handle layout separately
|
||||||
|
if (option.subtext) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className="flex gap-[8px] items-start"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={isChecked}
|
||||||
|
mode={mode}
|
||||||
|
disabled={disabled}
|
||||||
|
name={groupId}
|
||||||
|
value={option.value}
|
||||||
|
ariaLabel={option.ariaLabel || option.label}
|
||||||
|
onChange={({ checked }) => {
|
||||||
|
onOptionChange(option.value, checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-[4px] flex-1">
|
||||||
|
<span
|
||||||
|
className={`font-inter text-[14px] leading-[20px] ${
|
||||||
|
mode === "inverse"
|
||||||
|
? "text-[var(--color-content-inverse-primary)]"
|
||||||
|
: "text-[var(--color-content-default-primary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`font-inter text-[14px] leading-[20px] ${
|
||||||
|
mode === "inverse"
|
||||||
|
? "text-[var(--color-content-inverse-secondary,#1f1f1f)]"
|
||||||
|
: "text-[var(--color-content-default-tertiary,#b4b4b4)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.subtext}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no subtext, use Checkbox's built-in label
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
key={option.value}
|
||||||
|
checked={isChecked}
|
||||||
|
mode={mode}
|
||||||
|
disabled={disabled}
|
||||||
|
label={option.label}
|
||||||
|
name={groupId}
|
||||||
|
value={option.value}
|
||||||
|
ariaLabel={option.ariaLabel}
|
||||||
|
onChange={({ checked }) => {
|
||||||
|
onOptionChange(option.value, checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./CheckboxGroup.container";
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { memo, forwardRef } from "react";
|
|
||||||
import { useComponentId, useFormField } from "../../hooks";
|
|
||||||
import { InputView } from "./Input.view";
|
|
||||||
import type { InputProps } from "./Input.types";
|
|
||||||
|
|
||||||
const InputContainer = forwardRef<HTMLInputElement, InputProps>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
size = "medium",
|
|
||||||
labelVariant = "default",
|
|
||||||
state = "default",
|
|
||||||
disabled = false,
|
|
||||||
error = false,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
type = "text",
|
|
||||||
className = "",
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
// Generate unique ID for accessibility if not provided
|
|
||||||
const { id: inputId, labelId } = useComponentId("input", id);
|
|
||||||
|
|
||||||
// Size variants
|
|
||||||
const sizeStyles: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
input: string;
|
|
||||||
label: string;
|
|
||||||
container: string;
|
|
||||||
radius: string;
|
|
||||||
}
|
|
||||||
> = {
|
|
||||||
small: {
|
|
||||||
input:
|
|
||||||
labelVariant === "horizontal"
|
|
||||||
? "h-[30px] px-[12px] py-[8px] text-[10px]"
|
|
||||||
: "h-[32px] px-[12px] py-[8px] text-[10px]",
|
|
||||||
label: "text-[12px] leading-[14px] font-medium",
|
|
||||||
container: "gap-[4px]",
|
|
||||||
radius: "var(--measures-radius-small)",
|
|
||||||
},
|
|
||||||
medium: {
|
|
||||||
input: "h-[36px] px-[12px] py-[8px] text-[14px] leading-[20px]",
|
|
||||||
label: "text-[14px] leading-[16px] font-medium",
|
|
||||||
container: "gap-[8px]",
|
|
||||||
radius: "var(--measures-radius-medium)",
|
|
||||||
},
|
|
||||||
large: {
|
|
||||||
input: "h-[40px] px-[12px] py-[8px] text-[16px] leading-[24px]",
|
|
||||||
label: "text-[16px] leading-[20px] font-medium",
|
|
||||||
container: "gap-[12px]",
|
|
||||||
radius: "var(--measures-radius-large)",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// State styles
|
|
||||||
const getStateStyles = (): {
|
|
||||||
input: string;
|
|
||||||
label: string;
|
|
||||||
} => {
|
|
||||||
if (disabled) {
|
|
||||||
return {
|
|
||||||
input:
|
|
||||||
"bg-[var(--color-content-default-secondary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] cursor-not-allowed",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
input:
|
|
||||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-negative)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "active":
|
|
||||||
return {
|
|
||||||
input:
|
|
||||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
case "hover":
|
|
||||||
return {
|
|
||||||
input:
|
|
||||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
case "focus":
|
|
||||||
return {
|
|
||||||
input:
|
|
||||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
input:
|
|
||||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border border-[var(--color-border-default-tertiary)] hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stateStyles = getStateStyles();
|
|
||||||
const currentSize = sizeStyles[size];
|
|
||||||
|
|
||||||
// Container classes based on label variant
|
|
||||||
const containerClasses =
|
|
||||||
labelVariant === "horizontal"
|
|
||||||
? `flex items-center gap-[12px]`
|
|
||||||
: `flex flex-col ${currentSize.container}`;
|
|
||||||
|
|
||||||
const labelClasses =
|
|
||||||
labelVariant === "horizontal"
|
|
||||||
? `${currentSize.label} font-inter min-w-fit`
|
|
||||||
: `${currentSize.label} font-inter`;
|
|
||||||
|
|
||||||
const inputClasses = `
|
|
||||||
w-full border transition-all duration-200 ease-in-out
|
|
||||||
focus:outline-none focus:ring-0
|
|
||||||
${currentSize.input}
|
|
||||||
${stateStyles.input}
|
|
||||||
${className}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
// Form field handlers with disabled state handling
|
|
||||||
const { handleChange, handleFocus, handleBlur } =
|
|
||||||
useFormField<HTMLInputElement>(disabled, {
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputView
|
|
||||||
ref={ref}
|
|
||||||
inputId={inputId}
|
|
||||||
labelId={labelId}
|
|
||||||
size={size}
|
|
||||||
labelVariant={labelVariant}
|
|
||||||
state={state}
|
|
||||||
disabled={disabled}
|
|
||||||
error={error}
|
|
||||||
label={label}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
name={name}
|
|
||||||
type={type}
|
|
||||||
className={className}
|
|
||||||
containerClasses={containerClasses}
|
|
||||||
labelClasses={labelClasses}
|
|
||||||
inputClasses={inputClasses}
|
|
||||||
borderRadius={currentSize.radius}
|
|
||||||
handleChange={handleChange}
|
|
||||||
handleFocus={handleFocus}
|
|
||||||
handleBlur={handleBlur}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
InputContainer.displayName = "Input";
|
|
||||||
|
|
||||||
export default memo(InputContainer);
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { forwardRef } from "react";
|
|
||||||
import type { InputViewProps } from "./Input.types";
|
|
||||||
|
|
||||||
export const InputView = forwardRef<HTMLInputElement, InputViewProps>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
inputId,
|
|
||||||
labelId,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
value,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
size: _size,
|
|
||||||
labelVariant: _labelVariant,
|
|
||||||
state: _state,
|
|
||||||
error: _error,
|
|
||||||
className: _className,
|
|
||||||
containerClasses,
|
|
||||||
labelClasses,
|
|
||||||
inputClasses,
|
|
||||||
borderRadius,
|
|
||||||
handleChange,
|
|
||||||
handleFocus,
|
|
||||||
handleBlur,
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<div className={containerClasses}>
|
|
||||||
{label && (
|
|
||||||
<label
|
|
||||||
id={labelId}
|
|
||||||
htmlFor={inputId}
|
|
||||||
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<div className={disabled ? "opacity-40" : ""}>
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
id={inputId}
|
|
||||||
name={name}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={handleChange}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
disabled={disabled}
|
|
||||||
className={inputClasses}
|
|
||||||
style={{ borderRadius }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
InputView.displayName = "InputView";
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./Input.container";
|
|
||||||
export type { InputProps } from "./Input.types";
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import SectionNumber from "./SectionNumber";
|
||||||
|
|
||||||
|
interface NumberCardProps {
|
||||||
|
number: number;
|
||||||
|
text: string;
|
||||||
|
size?: "Small" | "Medium" | "Large" | "XLarge";
|
||||||
|
iconShape?: string;
|
||||||
|
iconColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberCard = memo<NumberCardProps>(({ number, text, size }) => {
|
||||||
|
// Base classes common to all sizes
|
||||||
|
const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
|
||||||
|
|
||||||
|
// If size prop is provided, use explicit size classes
|
||||||
|
// Otherwise, use responsive breakpoints for backward compatibility
|
||||||
|
if (size) {
|
||||||
|
// Size-specific classes
|
||||||
|
const sizeClasses = {
|
||||||
|
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
|
||||||
|
Medium: "flex flex-row items-center gap-8 p-8 relative",
|
||||||
|
Large: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||||
|
XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Text size classes
|
||||||
|
const textClasses = {
|
||||||
|
Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
|
||||||
|
Medium: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||||
|
Large: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||||
|
XLarge: "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Section number wrapper classes - Small doesn't need a wrapper
|
||||||
|
const sectionNumberWrapperClasses = {
|
||||||
|
Small: "relative shrink-0",
|
||||||
|
Medium: "flex justify-start flex-shrink-0",
|
||||||
|
Large: "absolute top-8 right-8",
|
||||||
|
XLarge: "absolute top-8 right-8",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Content container classes
|
||||||
|
const contentClasses = {
|
||||||
|
Small: "min-w-full relative shrink-0",
|
||||||
|
Medium: "flex-1",
|
||||||
|
Large: "absolute bottom-8 left-8 right-16",
|
||||||
|
XLarge: "absolute bottom-8 left-8 right-16",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small variant has section number as direct child, others need wrapper
|
||||||
|
if (size === "Small") {
|
||||||
|
return (
|
||||||
|
<div className={`${baseClasses} ${sizeClasses[size]}`}>
|
||||||
|
{/* Section Number - Direct child for Small */}
|
||||||
|
<SectionNumber number={number} />
|
||||||
|
|
||||||
|
{/* Card Content */}
|
||||||
|
<p className={textClasses[size]}>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${baseClasses} ${sizeClasses[size]}`}>
|
||||||
|
{/* Section Number */}
|
||||||
|
<div className={sectionNumberWrapperClasses[size]}>
|
||||||
|
<SectionNumber number={number} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card Content */}
|
||||||
|
<div className={contentClasses[size]}>
|
||||||
|
<p className={textClasses[size]}>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive breakpoints for backward compatibility (matches original behavior)
|
||||||
|
// Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl)
|
||||||
|
return (
|
||||||
|
<div className={`${baseClasses} flex flex-col gap-4 p-5 sm:flex-row sm:gap-8 sm:p-8 sm:items-center lg:flex-col lg:gap-[22px] lg:items-start lg:justify-end lg:p-8 lg:relative lg:h-[238px]`}>
|
||||||
|
{/* Section Number - Responsive positioning */}
|
||||||
|
<div className="flex justify-end items-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
|
||||||
|
<SectionNumber number={number} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card Content - Responsive positioning */}
|
||||||
|
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
|
||||||
|
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[#141414]">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NumberCard.displayName = "NumberCard";
|
||||||
|
|
||||||
|
export default NumberCard;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { memo } from "react";
|
|
||||||
import SectionNumber from "./SectionNumber";
|
|
||||||
|
|
||||||
interface NumberedCardProps {
|
|
||||||
number: number;
|
|
||||||
text: string;
|
|
||||||
iconShape?: string;
|
|
||||||
iconColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NumberedCard = memo<NumberedCardProps>(({ number, text }) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
|
|
||||||
{/* Section Number - Top right (lg breakpoint) */}
|
|
||||||
<div className="flex justify-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
|
|
||||||
<SectionNumber number={number} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Content - Bottom left (lg breakpoint) */}
|
|
||||||
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
|
|
||||||
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:font-normal sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[#141414]">
|
|
||||||
{text}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
NumberedCard.displayName = "NumberedCard";
|
|
||||||
|
|
||||||
export default NumberedCard;
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useTranslation } from "../../contexts/MessagesContext";
|
import { useTranslation } from "../../contexts/MessagesContext";
|
||||||
import SectionHeader from "../SectionHeader";
|
import SectionHeader from "../SectionHeader";
|
||||||
import NumberedCard from "../NumberedCard";
|
import NumberCard from "../NumberCard";
|
||||||
import Button from "../Button";
|
import Button from "../Button";
|
||||||
import type { NumberedCardsViewProps } from "./NumberedCards.types";
|
import type { NumberedCardsViewProps } from "./NumberedCards.types";
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ function NumberedCardsView({
|
|||||||
{/* Cards Container */}
|
{/* Cards Container */}
|
||||||
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-024)] lg:grid-cols-3 lg:gap-[var(--spacing-scale-024)]">
|
<div className="grid grid-cols-1 gap-y-[var(--spacing-scale-024)] lg:grid-cols-3 lg:gap-[var(--spacing-scale-024)]">
|
||||||
{cards.map((card, index) => (
|
{cards.map((card, index) => (
|
||||||
<NumberedCard
|
<NumberCard
|
||||||
key={index}
|
key={index}
|
||||||
number={index + 1}
|
number={index + 1}
|
||||||
text={card.text}
|
text={card.text}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { RadioButtonProps } from "./RadioButton.types";
|
|||||||
const RadioButtonContainer = ({
|
const RadioButtonContainer = ({
|
||||||
checked = false,
|
checked = false,
|
||||||
mode = "standard",
|
mode = "standard",
|
||||||
state = "default",
|
state = "default", // This state prop is now only for static display in Storybook/Preview
|
||||||
disabled = false,
|
disabled = false,
|
||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -16,55 +16,75 @@ const RadioButtonContainer = ({
|
|||||||
value,
|
value,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
className = "",
|
className = "",
|
||||||
...props
|
|
||||||
}: RadioButtonProps) => {
|
}: RadioButtonProps) => {
|
||||||
const isInverse = mode === "inverse";
|
const isInverse = mode === "inverse";
|
||||||
|
const isStandard = mode === "standard";
|
||||||
|
|
||||||
// Base tokens (using same design tokens as Checkbox)
|
// Base box styles per Figma - 24px size, circular
|
||||||
const colorContent = isInverse
|
const baseBox = `
|
||||||
? "var(--color-content-inverse-primary)"
|
flex
|
||||||
: "var(--color-content-default-primary)";
|
items-center
|
||||||
|
justify-center
|
||||||
|
shrink-0
|
||||||
|
w-[24px]
|
||||||
|
h-[24px]
|
||||||
|
rounded-full
|
||||||
|
transition-all
|
||||||
|
duration-200
|
||||||
|
ease-in-out
|
||||||
|
p-[4px]
|
||||||
|
`.trim().replace(/\s+/g, " ");
|
||||||
|
|
||||||
// Visual container depending on state
|
// Get box styles based on mode and checked status per Figma designs
|
||||||
const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`;
|
const getBoxStyles = (): string => {
|
||||||
|
// Standard mode styles
|
||||||
|
if (isStandard) {
|
||||||
|
// Default state: tertiary border (or brand primary when checked), with hover and focus states via CSS
|
||||||
|
// Hover changes border to brand primary color
|
||||||
|
// Focus shows shadow (double ring: 2px white inner, 4px dark outer)
|
||||||
|
// When checked, border is brand primary (but changes to invert tertiary on focus)
|
||||||
|
const defaultBorder = checked
|
||||||
|
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
|
||||||
|
: "border-[var(--color-border-default-tertiary,#464646)]";
|
||||||
|
|
||||||
|
// When focused and checked, border should be invert tertiary (#2d2d2d) per Figma
|
||||||
|
const focusBorder = checked
|
||||||
|
? "focus:border-[var(--color-content-invert-tertiary,#2d2d2d)]"
|
||||||
|
: "focus:border-[var(--color-border-default-tertiary,#464646)]";
|
||||||
|
|
||||||
|
return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid ${defaultBorder} hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`;
|
||||||
|
}
|
||||||
|
|
||||||
const stateStyles: Record<string, string> = {
|
// Inverse mode styles
|
||||||
default: "",
|
if (isInverse) {
|
||||||
hover: "",
|
// Default state: white border (or brand primary when checked), transparent background
|
||||||
focus: "",
|
// Hover changes border to inverse brand primary color (#6c6701) for both selected and unselected
|
||||||
|
// Focus shows shadow (double ring: 2px dark inner, 4px white outer)
|
||||||
|
// When checked, border is brand primary (but changes to white on focus)
|
||||||
|
const defaultBorder = checked
|
||||||
|
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
|
||||||
|
: "border-[var(--color-border-invert-primary,white)]";
|
||||||
|
|
||||||
|
// Hover border: inverse brand primary for both selected and unselected per Figma
|
||||||
|
const hoverBorder = "hover:border-[var(--color-border-invert-brand-primary,#6c6701)]";
|
||||||
|
|
||||||
|
// Focus border: when focused and checked, border should be white per Figma
|
||||||
|
const focusBorder = checked
|
||||||
|
? "focus:border-[var(--color-border-invert-primary,white)]"
|
||||||
|
: "focus:border-[var(--color-border-invert-primary,white)]";
|
||||||
|
|
||||||
|
return `${baseBox} bg-transparent border border-solid ${defaultBorder} ${hoverBorder} ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseBox;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Background behavior:
|
const combinedBoxStyles = getBoxStyles();
|
||||||
// - Standard: background does not change on check; only dot appears
|
|
||||||
// - Inverse: transparent background, dot appears on check
|
|
||||||
const backgroundWhenChecked = isInverse
|
|
||||||
? "var(--color-surface-default-transparent)"
|
|
||||||
: "var(--color-surface-default-primary)";
|
|
||||||
|
|
||||||
// Dot color for selected state
|
// Label color
|
||||||
const dotColor = checked
|
const labelColor = isInverse
|
||||||
? isInverse
|
? "var(--color-content-inverse-primary)"
|
||||||
? "var(--color-content-inverse-primary)"
|
: "var(--color-content-default-primary)";
|
||||||
: "var(--color-border-default-brand-primary)"
|
|
||||||
: "transparent";
|
|
||||||
const labelColor = colorContent;
|
|
||||||
|
|
||||||
const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`;
|
|
||||||
|
|
||||||
// Force visible outline for standard / default / unchecked
|
|
||||||
const defaultOutlineClass = isInverse
|
|
||||||
? "outline outline-1 outline-[var(--color-border-inverse-primary)]"
|
|
||||||
: "outline outline-1 outline-[var(--color-border-default-tertiary)]";
|
|
||||||
|
|
||||||
// Apply brand outline only on actual :hover
|
|
||||||
// Standard mode uses default brand primary, inverse mode uses inverse brand primary
|
|
||||||
const conditionalHoverOutlineClass = isInverse
|
|
||||||
? "hover:outline hover:outline-1 hover:outline-[var(--color-border-inverse-brand-primary)]"
|
|
||||||
: "hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]";
|
|
||||||
|
|
||||||
// Focus state for standard/unchecked with brand primary color and specific blur/spread
|
|
||||||
const conditionalFocusClass =
|
|
||||||
"focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]";
|
|
||||||
|
|
||||||
// Generate unique ID for accessibility if not provided
|
// Generate unique ID for accessibility if not provided
|
||||||
const generatedId = useId();
|
const generatedId = useId();
|
||||||
@@ -72,11 +92,13 @@ const RadioButtonContainer = ({
|
|||||||
|
|
||||||
const handleToggle = useCallback(
|
const handleToggle = useCallback(
|
||||||
(_e: React.MouseEvent | React.KeyboardEvent) => {
|
(_e: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
if (!disabled && onChange && !checked) {
|
if (!disabled && onChange) {
|
||||||
|
// Always call onChange when clicked, even if already checked
|
||||||
|
// The parent (RadioGroup) will handle the logic
|
||||||
onChange({ checked: true, value });
|
onChange({ checked: true, value });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[disabled, onChange, checked, value],
|
[disabled, onChange, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||||
@@ -91,7 +113,7 @@ const RadioButtonContainer = ({
|
|||||||
radioId={radioId}
|
radioId={radioId}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
state={state}
|
state={state} // Passed for static display in Storybook/Preview
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={label}
|
label={label}
|
||||||
name={name}
|
name={name}
|
||||||
@@ -99,15 +121,9 @@ const RadioButtonContainer = ({
|
|||||||
ariaLabel={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
className={className}
|
className={className}
|
||||||
combinedBoxStyles={combinedBoxStyles}
|
combinedBoxStyles={combinedBoxStyles}
|
||||||
defaultOutlineClass={defaultOutlineClass}
|
|
||||||
conditionalHoverOutlineClass={conditionalHoverOutlineClass}
|
|
||||||
conditionalFocusClass={conditionalFocusClass}
|
|
||||||
backgroundWhenChecked={backgroundWhenChecked}
|
|
||||||
dotColor={dotColor}
|
|
||||||
labelColor={labelColor}
|
labelColor={labelColor}
|
||||||
onToggle={handleToggle}
|
onToggle={handleToggle}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ export interface RadioButtonViewProps {
|
|||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
className: string;
|
className: string;
|
||||||
combinedBoxStyles: string;
|
combinedBoxStyles: string;
|
||||||
defaultOutlineClass: string;
|
|
||||||
conditionalHoverOutlineClass: string;
|
|
||||||
conditionalFocusClass: string;
|
|
||||||
backgroundWhenChecked: string;
|
|
||||||
dotColor: string;
|
|
||||||
labelColor: string;
|
labelColor: string;
|
||||||
onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void;
|
onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void;
|
||||||
onKeyDown: (_e: React.KeyboardEvent<HTMLSpanElement>) => void;
|
onKeyDown: (_e: React.KeyboardEvent<HTMLSpanElement>) => void;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { RadioButtonViewProps } from "./RadioButton.types";
|
|||||||
export function RadioButtonView({
|
export function RadioButtonView({
|
||||||
radioId,
|
radioId,
|
||||||
checked,
|
checked,
|
||||||
|
mode,
|
||||||
disabled,
|
disabled,
|
||||||
label,
|
label,
|
||||||
name,
|
name,
|
||||||
@@ -10,15 +11,9 @@ export function RadioButtonView({
|
|||||||
ariaLabel,
|
ariaLabel,
|
||||||
className,
|
className,
|
||||||
combinedBoxStyles,
|
combinedBoxStyles,
|
||||||
defaultOutlineClass,
|
|
||||||
conditionalHoverOutlineClass,
|
|
||||||
conditionalFocusClass,
|
|
||||||
backgroundWhenChecked,
|
|
||||||
dotColor,
|
|
||||||
labelColor,
|
labelColor,
|
||||||
onToggle,
|
onToggle,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
...props
|
|
||||||
}: RadioButtonViewProps) {
|
}: RadioButtonViewProps) {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -30,25 +25,25 @@ export function RadioButtonView({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`}
|
className={`group ${combinedBoxStyles} ${disabled ? "" : "cursor-pointer"}`}
|
||||||
style={{
|
tabIndex={disabled ? -1 : 0}
|
||||||
backgroundColor: backgroundWhenChecked,
|
|
||||||
}}
|
|
||||||
tabIndex={0}
|
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
{...(disabled && { "aria-disabled": true })}
|
{...(disabled && { "aria-disabled": true })}
|
||||||
{...(ariaLabel && { "aria-label": ariaLabel })}
|
{...(ariaLabel && { "aria-label": ariaLabel })}
|
||||||
{...(label && !ariaLabel && { "aria-labelledby": `${radioId}-label` })}
|
{...(label && !ariaLabel && { "aria-labelledby": `${radioId}-label` })}
|
||||||
id={radioId}
|
id={radioId}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
{/* Radio dot */}
|
{/* Radio dot - 16px size per Figma */}
|
||||||
|
{/* Selected hover state: darker dot color (#333000) per Figma */}
|
||||||
<div
|
<div
|
||||||
className="w-[16px] h-[16px] rounded-full transition-all duration-200"
|
className={`w-[16px] h-[16px] rounded-full transition-all duration-200 ${
|
||||||
style={{
|
checked && mode === "standard"
|
||||||
backgroundColor: dotColor,
|
? "bg-[var(--color-content-default-brand-primary,#fefcc9)] group-hover:!bg-[#333000]"
|
||||||
}}
|
: checked && mode === "inverse"
|
||||||
|
? "bg-[var(--color-content-default-primary,#000000)]"
|
||||||
|
: "bg-transparent"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{label && (
|
{label && (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export interface RadioOption {
|
export interface RadioOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
subtext?: string;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,52 @@ export function RadioGroupView({
|
|||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = value === option.value;
|
const isSelected = value === option.value;
|
||||||
|
|
||||||
|
// If there's subtext, render radio button without label and handle layout separately
|
||||||
|
if (option.subtext) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className="flex gap-[8px] items-start"
|
||||||
|
>
|
||||||
|
<RadioButton
|
||||||
|
checked={isSelected}
|
||||||
|
mode={mode}
|
||||||
|
state={state}
|
||||||
|
disabled={disabled}
|
||||||
|
name={groupId}
|
||||||
|
value={option.value}
|
||||||
|
ariaLabel={option.ariaLabel || option.label}
|
||||||
|
onChange={({ checked }) => {
|
||||||
|
if (checked) {
|
||||||
|
onOptionChange(option.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-[4px] flex-1">
|
||||||
|
<span
|
||||||
|
className={`font-inter text-[14px] leading-[20px] ${
|
||||||
|
mode === "inverse"
|
||||||
|
? "text-[var(--color-content-inverse-primary)]"
|
||||||
|
: "text-[var(--color-content-default-primary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`font-inter text-[14px] leading-[20px] ${
|
||||||
|
mode === "inverse"
|
||||||
|
? "text-[var(--color-content-inverse-secondary,#1f1f1f)]"
|
||||||
|
: "text-[var(--color-content-default-secondary,#b4b4b4)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.subtext}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no subtext, use RadioButton's built-in label
|
||||||
return (
|
return (
|
||||||
<RadioButton
|
<RadioButton
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
|
|
||||||
interface SectionNumberProps {
|
interface SectionNumberProps {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -8,16 +9,19 @@ interface SectionNumberProps {
|
|||||||
|
|
||||||
const SectionNumber = memo<SectionNumberProps>(({ number }) => {
|
const SectionNumber = memo<SectionNumberProps>(({ number }) => {
|
||||||
const getImageSrc = (num: number): string => {
|
const getImageSrc = (num: number): string => {
|
||||||
switch (num) {
|
const assetPath = (() => {
|
||||||
case 1:
|
switch (num) {
|
||||||
return "/assets/SectionNumber_1.png";
|
case 1:
|
||||||
case 2:
|
return "assets/SectionNumber_1.png";
|
||||||
return "/assets/SectionNumber_2.png";
|
case 2:
|
||||||
case 3:
|
return "assets/SectionNumber_2.png";
|
||||||
return "/assets/SectionNumber_3.png";
|
case 3:
|
||||||
default:
|
return "assets/SectionNumber_3.png";
|
||||||
return "/assets/SectionNumber_1.png";
|
default:
|
||||||
}
|
return "assets/SectionNumber_1.png";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return getAssetPath(assetPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, {
|
|
||||||
Children,
|
|
||||||
type ReactElement,
|
|
||||||
type ReactNode,
|
|
||||||
forwardRef,
|
|
||||||
useId,
|
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useCallback,
|
|
||||||
memo,
|
|
||||||
useImperativeHandle,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import { useClickOutside } from "../../hooks";
|
|
||||||
import { SelectView } from "./Select.view";
|
|
||||||
import type { SelectProps } from "./Select.types";
|
|
||||||
|
|
||||||
const SelectContainer = forwardRef<HTMLButtonElement, SelectProps>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
labelVariant = "default",
|
|
||||||
size = "medium",
|
|
||||||
state = "default",
|
|
||||||
disabled = false,
|
|
||||||
error = false,
|
|
||||||
placeholder = "Select an option",
|
|
||||||
className = "",
|
|
||||||
children,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const generatedId = useId();
|
|
||||||
const selectId = id || `select-${generatedId}`;
|
|
||||||
const labelId = `${selectId}-label`;
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [selectedValue, setSelectedValue] = useState(value || "");
|
|
||||||
const selectRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Sync internal state with external value prop
|
|
||||||
useEffect(() => {
|
|
||||||
if (value !== undefined && value !== selectedValue) {
|
|
||||||
setSelectedValue(value);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
useImperativeHandle(
|
|
||||||
ref,
|
|
||||||
() => selectRef.current as HTMLButtonElement | null,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle click outside to close menu
|
|
||||||
useClickOutside([menuRef, selectRef], () => setIsOpen(false), isOpen);
|
|
||||||
|
|
||||||
// Handle option selection
|
|
||||||
const handleOptionSelect = useCallback(
|
|
||||||
(optionValue: string, optionText: string) => {
|
|
||||||
setSelectedValue(optionValue);
|
|
||||||
setIsOpen(false);
|
|
||||||
if (onChange) {
|
|
||||||
onChange({ target: { value: optionValue, text: optionText } });
|
|
||||||
}
|
|
||||||
// Return focus to the select button for accessibility
|
|
||||||
if (selectRef.current) {
|
|
||||||
selectRef.current.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle select button click
|
|
||||||
const handleSelectClick = useCallback(() => {
|
|
||||||
if (!disabled) {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}
|
|
||||||
}, [disabled, isOpen]);
|
|
||||||
|
|
||||||
// Handle keyboard navigation
|
|
||||||
const handleKeyDown = useCallback(
|
|
||||||
(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
||||||
if (disabled) return;
|
|
||||||
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disabled, isOpen],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSizeStyles = (): string => {
|
|
||||||
const baseStyles = "w-full";
|
|
||||||
|
|
||||||
switch (size) {
|
|
||||||
case "small": {
|
|
||||||
const smallHeight =
|
|
||||||
labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]";
|
|
||||||
return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`;
|
|
||||||
}
|
|
||||||
case "medium":
|
|
||||||
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
|
|
||||||
case "large":
|
|
||||||
return `${baseStyles} h-[40px] pl-[12px] pr-[40px] py-[8px] text-[16px] leading-[24px]`;
|
|
||||||
default:
|
|
||||||
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLabelSizeStyles = (): string => {
|
|
||||||
switch (size) {
|
|
||||||
case "small":
|
|
||||||
return "text-[12px] leading-[14px]";
|
|
||||||
case "medium":
|
|
||||||
return "text-[14px] leading-[16px]";
|
|
||||||
case "large":
|
|
||||||
return "text-[16px] leading-[20px]";
|
|
||||||
default:
|
|
||||||
return "text-[14px] leading-[16px]";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStateStyles = (): {
|
|
||||||
select: string;
|
|
||||||
label: string;
|
|
||||||
} => {
|
|
||||||
if (disabled) {
|
|
||||||
return {
|
|
||||||
select:
|
|
||||||
"bg-[var(--color-content-default-secondary)] border-[var(--color-border-default-tertiary)] cursor-not-allowed opacity-40",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
select: "border-[var(--color-border-default-utility-negative)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "hover":
|
|
||||||
return {
|
|
||||||
select:
|
|
||||||
"border-[var(--color-border-default-tertiary)] shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
case "focus":
|
|
||||||
return {
|
|
||||||
select:
|
|
||||||
"border-[var(--color-border-default-utility-info)] shadow-[0_0_5px_3px_#3281F8]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
select: "border-[var(--color-border-default-tertiary)]",
|
|
||||||
label: "text-[var(--color-content-default-secondary)]",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBorderRadius = (): string => {
|
|
||||||
switch (size) {
|
|
||||||
case "small":
|
|
||||||
return "rounded-[var(--measures-radius-small)]";
|
|
||||||
case "medium":
|
|
||||||
return "rounded-[var(--measures-radius-medium)]";
|
|
||||||
case "large":
|
|
||||||
return "rounded-[var(--measures-radius-large)]";
|
|
||||||
default:
|
|
||||||
return "rounded-[var(--measures-radius-medium)]";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeStyles = getSizeStyles();
|
|
||||||
const labelSizeStyles = getLabelSizeStyles();
|
|
||||||
const stateStyles = getStateStyles();
|
|
||||||
const borderRadius = getBorderRadius();
|
|
||||||
|
|
||||||
const selectClasses = `
|
|
||||||
${sizeStyles}
|
|
||||||
${stateStyles.select}
|
|
||||||
${borderRadius}
|
|
||||||
bg-[var(--color-background-default-primary)]
|
|
||||||
text-[var(--color-content-default-primary)]
|
|
||||||
border
|
|
||||||
font-inter
|
|
||||||
font-normal
|
|
||||||
appearance-none
|
|
||||||
cursor-pointer
|
|
||||||
transition-all
|
|
||||||
duration-200
|
|
||||||
focus:outline-none
|
|
||||||
focus-visible:border focus-visible:border-[var(--color-border-default-utility-info)] focus-visible:shadow-[0_0_5px_3px_#3281F8]
|
|
||||||
text-left
|
|
||||||
justify-start
|
|
||||||
hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]
|
|
||||||
${className}
|
|
||||||
`
|
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, " ");
|
|
||||||
|
|
||||||
const labelClasses = `
|
|
||||||
${labelSizeStyles}
|
|
||||||
${stateStyles.label}
|
|
||||||
font-inter
|
|
||||||
font-medium
|
|
||||||
block
|
|
||||||
mb-[4px]
|
|
||||||
`
|
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, " ");
|
|
||||||
|
|
||||||
const containerClasses =
|
|
||||||
labelVariant === "horizontal"
|
|
||||||
? "flex items-center gap-[12px]"
|
|
||||||
: "flex flex-col";
|
|
||||||
|
|
||||||
const chevronClasses = `${
|
|
||||||
size === "large" ? "w-5 h-5" : "w-4 h-4"
|
|
||||||
} text-[var(--color-content-default-primary)] transition-transform duration-200 ${
|
|
||||||
isOpen ? "rotate-180" : ""
|
|
||||||
}`;
|
|
||||||
|
|
||||||
// Get display text for selected value
|
|
||||||
const getDisplayText = (): string => {
|
|
||||||
if (!selectedValue) return placeholder;
|
|
||||||
|
|
||||||
// Handle options prop
|
|
||||||
if (options && Array.isArray(options)) {
|
|
||||||
const selectedOption = options.find(
|
|
||||||
(option) => option.value === selectedValue,
|
|
||||||
);
|
|
||||||
return selectedOption ? selectedOption.label : placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle children (option elements)
|
|
||||||
const selectedOption = Children.toArray(children).find(
|
|
||||||
(
|
|
||||||
child,
|
|
||||||
): child is ReactElement<{
|
|
||||||
value: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}> => {
|
|
||||||
if (!React.isValidElement(child)) return false;
|
|
||||||
const props = child.props as {
|
|
||||||
value?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
return props.value === selectedValue;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return selectedOption
|
|
||||||
? String(selectedOption.props.children)
|
|
||||||
: placeholder;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectView
|
|
||||||
label={label}
|
|
||||||
placeholder={placeholder}
|
|
||||||
size={size}
|
|
||||||
state={state}
|
|
||||||
disabled={disabled}
|
|
||||||
error={error}
|
|
||||||
labelVariant={labelVariant}
|
|
||||||
className={className}
|
|
||||||
options={options}
|
|
||||||
selectId={selectId}
|
|
||||||
labelId={labelId}
|
|
||||||
isOpen={isOpen}
|
|
||||||
selectedValue={selectedValue}
|
|
||||||
displayText={getDisplayText()}
|
|
||||||
selectClasses={selectClasses}
|
|
||||||
labelClasses={labelClasses}
|
|
||||||
containerClasses={containerClasses}
|
|
||||||
chevronClasses={chevronClasses}
|
|
||||||
onButtonClick={handleSelectClick}
|
|
||||||
onButtonKeyDown={handleKeyDown}
|
|
||||||
onOptionClick={handleOptionSelect}
|
|
||||||
selectRef={selectRef}
|
|
||||||
menuRef={menuRef}
|
|
||||||
ariaLabelledby={label ? labelId : undefined}
|
|
||||||
ariaInvalid={error}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
SelectContainer.displayName = "Select";
|
|
||||||
|
|
||||||
export default memo(SelectContainer);
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import React, { Children, type ReactNode } from "react";
|
|
||||||
import SelectDropdown from "../SelectDropdown";
|
|
||||||
import SelectOption from "../SelectOption";
|
|
||||||
import type { SelectOptionData } from "./Select.types";
|
|
||||||
|
|
||||||
export interface SelectViewProps {
|
|
||||||
label?: string;
|
|
||||||
placeholder: string;
|
|
||||||
size: "small" | "medium" | "large";
|
|
||||||
state: "default" | "hover" | "focus";
|
|
||||||
disabled: boolean;
|
|
||||||
error: boolean;
|
|
||||||
labelVariant: "default" | "horizontal";
|
|
||||||
className: string;
|
|
||||||
options?: SelectOptionData[];
|
|
||||||
children?: ReactNode;
|
|
||||||
// Computed props from container
|
|
||||||
selectId: string;
|
|
||||||
labelId: string;
|
|
||||||
isOpen: boolean;
|
|
||||||
selectedValue: string;
|
|
||||||
displayText: string;
|
|
||||||
selectClasses: string;
|
|
||||||
labelClasses: string;
|
|
||||||
containerClasses: string;
|
|
||||||
chevronClasses: string;
|
|
||||||
// Callbacks
|
|
||||||
onButtonClick: () => void;
|
|
||||||
onButtonKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
|
||||||
onOptionClick: (_value: string, _text: string) => void;
|
|
||||||
// Refs
|
|
||||||
selectRef: React.RefObject<HTMLButtonElement>;
|
|
||||||
menuRef: React.RefObject<HTMLDivElement>;
|
|
||||||
// Additional props
|
|
||||||
ariaLabelledby?: string;
|
|
||||||
ariaInvalid?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SelectView({
|
|
||||||
label,
|
|
||||||
placeholder: _placeholder,
|
|
||||||
size,
|
|
||||||
disabled,
|
|
||||||
error: _error,
|
|
||||||
labelVariant: _labelVariant,
|
|
||||||
options,
|
|
||||||
children,
|
|
||||||
selectId,
|
|
||||||
labelId,
|
|
||||||
isOpen,
|
|
||||||
selectedValue,
|
|
||||||
displayText,
|
|
||||||
selectClasses,
|
|
||||||
labelClasses,
|
|
||||||
containerClasses,
|
|
||||||
chevronClasses,
|
|
||||||
onButtonClick,
|
|
||||||
onButtonKeyDown,
|
|
||||||
onOptionClick,
|
|
||||||
selectRef,
|
|
||||||
menuRef,
|
|
||||||
ariaLabelledby,
|
|
||||||
ariaInvalid,
|
|
||||||
...props
|
|
||||||
}: SelectViewProps) {
|
|
||||||
return (
|
|
||||||
<div className={containerClasses}>
|
|
||||||
{label && (
|
|
||||||
<label
|
|
||||||
id={labelId}
|
|
||||||
htmlFor={selectId}
|
|
||||||
className={`${labelClasses} text-[var(--color-content-default-secondary)]`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
ref={selectRef}
|
|
||||||
id={selectId}
|
|
||||||
disabled={disabled}
|
|
||||||
className={selectClasses}
|
|
||||||
aria-labelledby={ariaLabelledby}
|
|
||||||
aria-invalid={ariaInvalid}
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
onClick={onButtonClick}
|
|
||||||
onKeyDown={onButtonKeyDown}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="text-left">{displayText}</span>
|
|
||||||
</button>
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-[12px] pointer-events-none">
|
|
||||||
<svg
|
|
||||||
className={chevronClasses}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div
|
|
||||||
ref={menuRef}
|
|
||||||
className="absolute top-full left-0 right-0 z-50 mt-1"
|
|
||||||
>
|
|
||||||
<SelectDropdown>
|
|
||||||
{options && Array.isArray(options)
|
|
||||||
? options.map((option) => (
|
|
||||||
<SelectOption
|
|
||||||
key={option.value}
|
|
||||||
selected={option.value === selectedValue}
|
|
||||||
size={size}
|
|
||||||
onClick={() => onOptionClick(option.value, option.label)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</SelectOption>
|
|
||||||
))
|
|
||||||
: Children.map(children, (child) => {
|
|
||||||
if (
|
|
||||||
React.isValidElement(child) &&
|
|
||||||
child.type === "option"
|
|
||||||
) {
|
|
||||||
const optionProps = child.props as {
|
|
||||||
value: string;
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<SelectOption
|
|
||||||
key={optionProps.value}
|
|
||||||
selected={optionProps.value === selectedValue}
|
|
||||||
size={size}
|
|
||||||
onClick={() =>
|
|
||||||
onOptionClick(
|
|
||||||
optionProps.value,
|
|
||||||
String(optionProps.children),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{optionProps.children}
|
|
||||||
</SelectOption>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
})}
|
|
||||||
</SelectDropdown>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./Select.container";
|
|
||||||
export type { SelectProps, SelectOptionData } from "./Select.types";
|
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
Children,
|
||||||
|
type ReactElement,
|
||||||
|
type ReactNode,
|
||||||
|
forwardRef,
|
||||||
|
useId,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
memo,
|
||||||
|
useImperativeHandle,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
import { useClickOutside } from "../../hooks";
|
||||||
|
import { SelectInputView } from "./SelectInput.view";
|
||||||
|
import type { SelectInputProps } from "./SelectInput.types";
|
||||||
|
|
||||||
|
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
state: externalState = "default",
|
||||||
|
disabled = false,
|
||||||
|
error = false,
|
||||||
|
placeholder = "Choose an option",
|
||||||
|
className = "",
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const generatedId = useId();
|
||||||
|
const selectId = id || `select-input-${generatedId}`;
|
||||||
|
const labelId = `${selectId}-label`;
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selectedValue, setSelectedValue] = useState(value || "");
|
||||||
|
const selectRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Internal state management: track if focused and how (mouse vs keyboard)
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null);
|
||||||
|
const wasMouseDownRef = useRef(false);
|
||||||
|
|
||||||
|
// Determine if we should auto-manage focus (only when state is "default" or undefined)
|
||||||
|
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
|
||||||
|
|
||||||
|
// Sync internal state with external value prop
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== undefined && value !== selectedValue) {
|
||||||
|
setSelectedValue(value);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => selectRef.current as HTMLButtonElement | null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle click outside to close menu
|
||||||
|
useClickOutside([menuRef, selectRef], () => setIsOpen(false), isOpen);
|
||||||
|
|
||||||
|
// Handle option selection
|
||||||
|
const handleOptionSelect = useCallback(
|
||||||
|
(optionValue: string, optionText: string) => {
|
||||||
|
setSelectedValue(optionValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ target: { value: optionValue, text: optionText } });
|
||||||
|
}
|
||||||
|
if (selectRef.current) {
|
||||||
|
selectRef.current.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle mouse down to detect mouse clicks
|
||||||
|
const handleMouseDown = useCallback(() => {
|
||||||
|
if (!disabled && shouldAutoManageFocus) {
|
||||||
|
wasMouseDownRef.current = true;
|
||||||
|
}
|
||||||
|
}, [disabled, shouldAutoManageFocus]);
|
||||||
|
|
||||||
|
// Handle select button click
|
||||||
|
const handleSelectClick = useCallback(() => {
|
||||||
|
if (!disabled) {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}
|
||||||
|
}, [disabled, isOpen]);
|
||||||
|
|
||||||
|
// Handle keyboard navigation
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, isOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle focus to detect mouse vs keyboard
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
const method = wasMouseDownRef.current ? "mouse" : "keyboard";
|
||||||
|
|
||||||
|
if (shouldAutoManageFocus) {
|
||||||
|
setIsFocused(true);
|
||||||
|
setFocusMethod(method);
|
||||||
|
wasMouseDownRef.current = false;
|
||||||
|
}
|
||||||
|
}, [disabled, shouldAutoManageFocus]);
|
||||||
|
|
||||||
|
// Handle blur
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
if (shouldAutoManageFocus) {
|
||||||
|
setIsFocused(false);
|
||||||
|
setFocusMethod(null);
|
||||||
|
wasMouseDownRef.current = false;
|
||||||
|
}
|
||||||
|
}, [shouldAutoManageFocus]);
|
||||||
|
|
||||||
|
// Determine actual state:
|
||||||
|
// - Active: when clicked (mouse focus) or when dropdown is open
|
||||||
|
// - Focus: when tabbed (keyboard focus)
|
||||||
|
// - Default: when not focused
|
||||||
|
const actualState = shouldAutoManageFocus
|
||||||
|
? isOpen || isFocused
|
||||||
|
? focusMethod === "mouse" || isOpen
|
||||||
|
? "active"
|
||||||
|
: "focus"
|
||||||
|
: "default"
|
||||||
|
: externalState;
|
||||||
|
|
||||||
|
// Determine if select is filled (has selected value)
|
||||||
|
const isFilled = Boolean(selectedValue && selectedValue.trim().length > 0);
|
||||||
|
|
||||||
|
// Get display text for selected value
|
||||||
|
const getDisplayText = (): string => {
|
||||||
|
if (!selectedValue) return placeholder;
|
||||||
|
|
||||||
|
if (options && Array.isArray(options)) {
|
||||||
|
const selectedOption = options.find(
|
||||||
|
(option) => option.value === selectedValue,
|
||||||
|
);
|
||||||
|
return selectedOption ? selectedOption.label : placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOption = Children.toArray(children).find(
|
||||||
|
(
|
||||||
|
child,
|
||||||
|
): child is ReactElement<{
|
||||||
|
value: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}> => {
|
||||||
|
if (!React.isValidElement(child)) return false;
|
||||||
|
const props = child.props as {
|
||||||
|
value?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
return props.value === selectedValue;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return selectedOption
|
||||||
|
? String(selectedOption.props.children)
|
||||||
|
: placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectInputView
|
||||||
|
label={label}
|
||||||
|
placeholder={placeholder}
|
||||||
|
state={actualState}
|
||||||
|
disabled={disabled}
|
||||||
|
error={error}
|
||||||
|
className={className}
|
||||||
|
options={options}
|
||||||
|
selectId={selectId}
|
||||||
|
labelId={labelId}
|
||||||
|
isOpen={isOpen}
|
||||||
|
selectedValue={selectedValue}
|
||||||
|
displayText={getDisplayText()}
|
||||||
|
isFilled={isFilled}
|
||||||
|
onButtonClick={handleSelectClick}
|
||||||
|
onButtonKeyDown={handleKeyDown}
|
||||||
|
onButtonMouseDown={handleMouseDown}
|
||||||
|
onButtonFocus={handleFocus}
|
||||||
|
onButtonBlur={handleBlur}
|
||||||
|
onOptionClick={handleOptionSelect}
|
||||||
|
selectRef={selectRef}
|
||||||
|
menuRef={menuRef}
|
||||||
|
ariaLabelledby={label ? labelId : undefined}
|
||||||
|
ariaInvalid={error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
SelectInputContainer.displayName = "SelectInput";
|
||||||
|
|
||||||
|
export default memo(SelectInputContainer);
|
||||||
+1
-1
@@ -5,7 +5,7 @@ export interface SelectOptionData {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectProps {
|
export interface SelectInputProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
labelVariant?: "default" | "horizontal";
|
labelVariant?: "default" | "horizontal";
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import React, { Children, type ReactNode } from "react";
|
||||||
|
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||||
|
import SelectDropdown from "../SelectDropdown";
|
||||||
|
import SelectOption from "../SelectOption";
|
||||||
|
import type { SelectOptionData } from "./SelectInput.types";
|
||||||
|
|
||||||
|
export interface SelectInputViewProps {
|
||||||
|
label?: string;
|
||||||
|
placeholder: string;
|
||||||
|
state: "default" | "active" | "hover" | "focus";
|
||||||
|
disabled: boolean;
|
||||||
|
error: boolean;
|
||||||
|
className: string;
|
||||||
|
options?: SelectOptionData[];
|
||||||
|
children?: ReactNode;
|
||||||
|
// Computed props from container
|
||||||
|
selectId: string;
|
||||||
|
labelId: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
selectedValue: string;
|
||||||
|
displayText: string;
|
||||||
|
isFilled: boolean;
|
||||||
|
// Callbacks
|
||||||
|
onButtonClick: () => void;
|
||||||
|
onButtonKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
|
||||||
|
onButtonMouseDown?: () => void;
|
||||||
|
onButtonFocus?: () => void;
|
||||||
|
onButtonBlur?: () => void;
|
||||||
|
onOptionClick: (_value: string, _text: string) => void;
|
||||||
|
// Refs
|
||||||
|
selectRef: React.RefObject<HTMLButtonElement>;
|
||||||
|
menuRef: React.RefObject<HTMLDivElement>;
|
||||||
|
// Additional props
|
||||||
|
ariaLabelledby?: string;
|
||||||
|
ariaInvalid?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectInputView({
|
||||||
|
label,
|
||||||
|
placeholder: _placeholder,
|
||||||
|
state,
|
||||||
|
disabled,
|
||||||
|
error,
|
||||||
|
options,
|
||||||
|
children,
|
||||||
|
selectId,
|
||||||
|
labelId,
|
||||||
|
isOpen,
|
||||||
|
selectedValue,
|
||||||
|
displayText,
|
||||||
|
isFilled,
|
||||||
|
onButtonClick,
|
||||||
|
onButtonKeyDown,
|
||||||
|
onButtonMouseDown,
|
||||||
|
onButtonFocus,
|
||||||
|
onButtonBlur,
|
||||||
|
onOptionClick,
|
||||||
|
selectRef,
|
||||||
|
menuRef,
|
||||||
|
ariaLabelledby,
|
||||||
|
ariaInvalid,
|
||||||
|
}: SelectInputViewProps) {
|
||||||
|
// Styles based on Figma design
|
||||||
|
const containerClasses = "flex flex-col gap-[8px]";
|
||||||
|
const labelClasses = "text-[14px] leading-[20px] font-medium font-inter text-[var(--color-content-default-primary)]";
|
||||||
|
|
||||||
|
// Button styles per Figma
|
||||||
|
const getButtonClasses = (): string => {
|
||||||
|
const baseClasses = `
|
||||||
|
w-full
|
||||||
|
h-[40px]
|
||||||
|
px-[12px]
|
||||||
|
py-[8px]
|
||||||
|
text-[16px]
|
||||||
|
font-medium
|
||||||
|
leading-[20px]
|
||||||
|
rounded-[8px]
|
||||||
|
border
|
||||||
|
border-solid
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-between
|
||||||
|
gap-[12px]
|
||||||
|
transition-all
|
||||||
|
duration-200
|
||||||
|
focus:outline-none
|
||||||
|
focus:ring-0
|
||||||
|
cursor-pointer
|
||||||
|
appearance-none
|
||||||
|
m-0
|
||||||
|
`.trim().replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return `${baseClasses} bg-[var(--color-surface-default-secondary)] text-[var(--color-content-inverse-tertiary,#2d2d2d)] border-[var(--color-border-default-primary)] cursor-not-allowed opacity-40`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return `${baseClasses} bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-[var(--color-border-default-utility-negative)]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === "focus") {
|
||||||
|
// Focus state: secondary background, tertiary border, with focus ring
|
||||||
|
return `${baseClasses} bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border border-solid border-[var(--color-border-default-tertiary)]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === "active" || isOpen) {
|
||||||
|
// Active state per Figma: secondary background, tertiary border
|
||||||
|
return `${baseClasses} bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border-[var(--color-border-default-tertiary)]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default state per Figma: secondary background, primary border (subtle)
|
||||||
|
return `${baseClasses} bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border-[var(--color-border-default-primary)]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClasses = getButtonClasses();
|
||||||
|
|
||||||
|
// Text color based on filled state
|
||||||
|
const textColorClass = isFilled
|
||||||
|
? "text-[var(--color-content-default-primary)]"
|
||||||
|
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
|
||||||
|
|
||||||
|
// Chevron icon
|
||||||
|
const chevronClasses = `w-5 h-5 text-[var(--color-content-default-primary)] transition-transform duration-200 ${
|
||||||
|
isOpen ? "rotate-180" : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClasses}>
|
||||||
|
{label && (
|
||||||
|
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
|
||||||
|
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
|
||||||
|
<label
|
||||||
|
id={labelId}
|
||||||
|
className={labelClasses}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative shrink-0 size-[12px]">
|
||||||
|
<img
|
||||||
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
|
alt="Help"
|
||||||
|
className="block max-w-none size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
ref={selectRef}
|
||||||
|
id={selectId}
|
||||||
|
disabled={disabled}
|
||||||
|
className={buttonClasses}
|
||||||
|
aria-labelledby={ariaLabelledby}
|
||||||
|
aria-invalid={ariaInvalid}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
onClick={onButtonClick}
|
||||||
|
onKeyDown={onButtonKeyDown}
|
||||||
|
onMouseDown={onButtonMouseDown}
|
||||||
|
onFocus={onButtonFocus}
|
||||||
|
onBlur={onButtonBlur}
|
||||||
|
>
|
||||||
|
<span className={`flex-1 text-left pr-[32px] ${textColorClass}`}>
|
||||||
|
{displayText}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center justify-center shrink-0">
|
||||||
|
<svg
|
||||||
|
className={chevronClasses}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{state === "focus" && (
|
||||||
|
<div
|
||||||
|
className="absolute border-2 border-solid border-[var(--color-border-inverse-primary)] inset-0 rounded-[8px] shadow-[0px_0px_0px_2px_var(--color-border-default-primary)] pointer-events-none z-10"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="absolute top-full left-0 right-0 z-50 mt-1"
|
||||||
|
>
|
||||||
|
<SelectDropdown>
|
||||||
|
{options && Array.isArray(options)
|
||||||
|
? options.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
key={option.value}
|
||||||
|
selected={option.value === selectedValue}
|
||||||
|
size="medium"
|
||||||
|
onClick={() => onOptionClick(option.value, option.label)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectOption>
|
||||||
|
))
|
||||||
|
: Children.map(children, (child) => {
|
||||||
|
if (
|
||||||
|
React.isValidElement(child) &&
|
||||||
|
child.type === "option"
|
||||||
|
) {
|
||||||
|
const optionProps = child.props as {
|
||||||
|
value: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<SelectOption
|
||||||
|
key={optionProps.value}
|
||||||
|
selected={optionProps.value === selectedValue}
|
||||||
|
size="medium"
|
||||||
|
onClick={() =>
|
||||||
|
onOptionClick(
|
||||||
|
optionProps.value,
|
||||||
|
String(optionProps.children),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{optionProps.children}
|
||||||
|
</SelectOption>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
})}
|
||||||
|
</SelectDropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./SelectInput.container";
|
||||||
|
export type { SelectInputProps, SelectOptionData } from "./SelectInput.types";
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo, forwardRef, useState, useRef } from "react";
|
||||||
|
import { useComponentId, useFormField } from "../../hooks";
|
||||||
|
import { TextInputView } from "./TextInput.view";
|
||||||
|
import type { TextInputProps } from "./TextInput.types";
|
||||||
|
|
||||||
|
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
state: externalState = "default",
|
||||||
|
disabled = false,
|
||||||
|
error = false,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type = "text",
|
||||||
|
className = "",
|
||||||
|
showHelpIcon = true,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
// Generate unique ID for accessibility if not provided
|
||||||
|
const { id: inputId, labelId } = useComponentId("text-input", id);
|
||||||
|
|
||||||
|
// Internal state management: track if focused and how (mouse vs keyboard)
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null);
|
||||||
|
const wasMouseDownRef = useRef(false);
|
||||||
|
|
||||||
|
// Determine if we should auto-manage focus (only when state is "default" or undefined)
|
||||||
|
// If state is "active", "hover", or "focus", respect it and don't override
|
||||||
|
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
|
||||||
|
|
||||||
|
// Determine actual state:
|
||||||
|
// - Active: when clicked (mouse focus)
|
||||||
|
// - Focus: when tabbed (keyboard focus)
|
||||||
|
// - Default: when not focused
|
||||||
|
const actualState = shouldAutoManageFocus
|
||||||
|
? isFocused
|
||||||
|
? focusMethod === "mouse"
|
||||||
|
? "active"
|
||||||
|
: "focus"
|
||||||
|
: "default"
|
||||||
|
: externalState;
|
||||||
|
|
||||||
|
// Determine if input is filled (has value)
|
||||||
|
const isFilled = Boolean(value && value.trim().length > 0);
|
||||||
|
|
||||||
|
// Fixed size styles (medium only per Figma designs)
|
||||||
|
const sizeStyles = {
|
||||||
|
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
|
||||||
|
label: "text-[14px] leading-[20px] font-medium",
|
||||||
|
container: "gap-[8px]",
|
||||||
|
radius: "var(--measures-radius-200,8px)",
|
||||||
|
};
|
||||||
|
|
||||||
|
// State styles based on Figma designs
|
||||||
|
const getStateStyles = (): {
|
||||||
|
input: string;
|
||||||
|
label: string;
|
||||||
|
inputWrapper: string;
|
||||||
|
focusRing: string;
|
||||||
|
} => {
|
||||||
|
if (disabled) {
|
||||||
|
return {
|
||||||
|
input:
|
||||||
|
"bg-[var(--color-surface-default-secondary)] text-[var(--color-content-inverse-tertiary,#2d2d2d)] border border-solid border-[var(--color-border-default-primary)] cursor-not-allowed",
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
inputWrapper: "relative",
|
||||||
|
focusRing: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const filledStyles = isFilled
|
||||||
|
? "font-medium leading-[20px]"
|
||||||
|
: "font-normal leading-[24px]";
|
||||||
|
return {
|
||||||
|
input: `bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-solid border-[var(--color-border-default-utility-negative)] ${filledStyles}`,
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
inputWrapper: "relative",
|
||||||
|
focusRing: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (actualState) {
|
||||||
|
case "active": {
|
||||||
|
const filledStyles = isFilled
|
||||||
|
? "font-medium leading-[20px]"
|
||||||
|
: "font-normal leading-[24px]";
|
||||||
|
return {
|
||||||
|
input: `bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-2 border-solid border-[var(--color-border-default-tertiary)] ${filledStyles}`,
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
inputWrapper: "relative",
|
||||||
|
focusRing: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "focus": {
|
||||||
|
const filledStyles = isFilled
|
||||||
|
? "font-medium leading-[20px]"
|
||||||
|
: "font-normal leading-[24px]";
|
||||||
|
return {
|
||||||
|
input: `bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border border-solid border-[var(--color-border-default-tertiary)] ${filledStyles}`,
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
inputWrapper: "relative",
|
||||||
|
focusRing:
|
||||||
|
"absolute border-2 border-solid border-[var(--color-border-inverse-primary)] inset-0 rounded-[var(--measures-radius-200,8px)] shadow-[0px_0px_0px_2px_var(--color-border-default-primary)] pointer-events-none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const filledStyles = isFilled
|
||||||
|
? "font-medium leading-[20px]"
|
||||||
|
: "font-normal leading-[24px]";
|
||||||
|
// Default state uses primary border (matches Figma - border color same as background, so border is subtle)
|
||||||
|
return {
|
||||||
|
input: `bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] border border-solid border-[var(--color-border-default-primary)] ${filledStyles}`,
|
||||||
|
label: "text-[var(--color-content-default-secondary)]",
|
||||||
|
inputWrapper: "relative",
|
||||||
|
focusRing: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateStyles = getStateStyles();
|
||||||
|
|
||||||
|
// Container classes (default label variant only)
|
||||||
|
const containerClasses = `flex flex-col ${sizeStyles.container}`;
|
||||||
|
|
||||||
|
const labelClasses = `${sizeStyles.label} font-inter`;
|
||||||
|
|
||||||
|
// Base classes without border (border is added in state styles)
|
||||||
|
const inputClasses = `
|
||||||
|
w-full transition-all duration-200 ease-in-out
|
||||||
|
focus:outline-none focus:ring-0
|
||||||
|
placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)]
|
||||||
|
${sizeStyles.input}
|
||||||
|
${stateStyles.input}
|
||||||
|
${className}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// Text color for filled text (placeholder color is handled above)
|
||||||
|
const textColorClass = isFilled
|
||||||
|
? "text-[var(--color-content-default-primary)]"
|
||||||
|
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
|
||||||
|
|
||||||
|
// Form field handlers with disabled state handling
|
||||||
|
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(disabled, {
|
||||||
|
onChange,
|
||||||
|
onBlur: (e) => {
|
||||||
|
if (shouldAutoManageFocus) {
|
||||||
|
setIsFocused(false);
|
||||||
|
setFocusMethod(null);
|
||||||
|
wasMouseDownRef.current = false;
|
||||||
|
}
|
||||||
|
onBlur?.(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle mouse down to detect mouse clicks
|
||||||
|
const handleMouseDown = () => {
|
||||||
|
if (!disabled && shouldAutoManageFocus) {
|
||||||
|
wasMouseDownRef.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom focus handler to detect mouse vs keyboard
|
||||||
|
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
// Detect if focus came from keyboard (Tab) or mouse (click)
|
||||||
|
// If mouseDown was detected before focus, it's a mouse click (active)
|
||||||
|
// Otherwise, it's keyboard navigation (focus)
|
||||||
|
const method = wasMouseDownRef.current ? "mouse" : "keyboard";
|
||||||
|
|
||||||
|
if (shouldAutoManageFocus) {
|
||||||
|
setIsFocused(true);
|
||||||
|
setFocusMethod(method);
|
||||||
|
// Reset mouse down flag after focus is processed
|
||||||
|
wasMouseDownRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextInputView
|
||||||
|
ref={ref}
|
||||||
|
inputId={inputId}
|
||||||
|
labelId={labelId}
|
||||||
|
state={actualState}
|
||||||
|
disabled={disabled}
|
||||||
|
error={error}
|
||||||
|
label={label}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
name={name}
|
||||||
|
type={type}
|
||||||
|
className={className}
|
||||||
|
containerClasses={containerClasses}
|
||||||
|
labelClasses={labelClasses}
|
||||||
|
inputClasses={`${inputClasses} ${textColorClass}`}
|
||||||
|
borderRadius={sizeStyles.radius}
|
||||||
|
handleChange={handleChange}
|
||||||
|
handleFocus={handleFocus}
|
||||||
|
handleBlur={handleBlur}
|
||||||
|
handleMouseDown={handleMouseDown}
|
||||||
|
showHelpIcon={showHelpIcon}
|
||||||
|
isFilled={isFilled}
|
||||||
|
inputWrapperClasses={stateStyles.inputWrapper}
|
||||||
|
focusRingClasses={stateStyles.focusRing}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
TextInputContainer.displayName = "TextInput";
|
||||||
|
|
||||||
|
export default memo(TextInputContainer);
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
export interface InputProps extends Omit<
|
export interface TextInputProps extends Omit<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
"size" | "onChange" | "onFocus" | "onBlur"
|
"size" | "onChange" | "onFocus" | "onBlur"
|
||||||
> {
|
> {
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
labelVariant?: "default" | "horizontal";
|
|
||||||
state?: "default" | "active" | "hover" | "focus";
|
state?: "default" | "active" | "hover" | "focus";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
@@ -14,13 +12,12 @@ export interface InputProps extends Omit<
|
|||||||
onFocus?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
onFocus?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showHelpIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InputViewProps {
|
export interface TextInputViewProps {
|
||||||
inputId: string;
|
inputId: string;
|
||||||
labelId: string;
|
labelId: string;
|
||||||
size: "small" | "medium" | "large";
|
|
||||||
labelVariant: "default" | "horizontal";
|
|
||||||
state: "default" | "active" | "hover" | "focus";
|
state: "default" | "active" | "hover" | "focus";
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
@@ -37,4 +34,9 @@ export interface InputViewProps {
|
|||||||
handleChange: (_e: React.ChangeEvent<HTMLInputElement>) => void;
|
handleChange: (_e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
handleFocus: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
handleFocus: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
handleBlur: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
handleBlur: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
|
handleMouseDown?: () => void;
|
||||||
|
showHelpIcon?: boolean;
|
||||||
|
isFilled?: boolean;
|
||||||
|
inputWrapperClasses?: string;
|
||||||
|
focusRingClasses?: string;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||||
|
import type { TextInputViewProps } from "./TextInput.types";
|
||||||
|
|
||||||
|
export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
inputId,
|
||||||
|
labelId,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
error: _error,
|
||||||
|
className: _className,
|
||||||
|
containerClasses,
|
||||||
|
labelClasses,
|
||||||
|
inputClasses,
|
||||||
|
borderRadius,
|
||||||
|
handleChange,
|
||||||
|
handleFocus,
|
||||||
|
handleBlur,
|
||||||
|
handleMouseDown,
|
||||||
|
showHelpIcon = true,
|
||||||
|
inputWrapperClasses = "relative",
|
||||||
|
focusRingClasses = "",
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div className={containerClasses}>
|
||||||
|
{label && (
|
||||||
|
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
|
||||||
|
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
|
||||||
|
<label
|
||||||
|
id={labelId}
|
||||||
|
htmlFor={inputId}
|
||||||
|
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-primary)]`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{showHelpIcon && (
|
||||||
|
<div className="relative shrink-0 size-[12px]">
|
||||||
|
<img
|
||||||
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
|
alt="Help"
|
||||||
|
className="block max-w-none size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={inputWrapperClasses}>
|
||||||
|
<div className={disabled ? "opacity-40" : ""}>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
id={inputId}
|
||||||
|
name={name}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
disabled={disabled}
|
||||||
|
className={inputClasses}
|
||||||
|
style={{ borderRadius }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{focusRingClasses && (
|
||||||
|
<div className={focusRingClasses} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
TextInputView.displayName = "TextInputView";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./TextInput.container";
|
||||||
|
export type { TextInputProps } from "./TextInput.types";
|
||||||
@@ -62,4 +62,7 @@ export const ASSETS = {
|
|||||||
|
|
||||||
// Tooltip icons
|
// Tooltip icons
|
||||||
ICON_POINTER: "assets/Icon_Pointer.svg",
|
ICON_POINTER: "assets/Icon_Pointer.svg",
|
||||||
|
|
||||||
|
// Help icon
|
||||||
|
ICON_HELP: "assets/Icon_Help.svg",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+60
-48
@@ -26,34 +26,6 @@ const CheckedInteraction = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const StandardInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const checkboxes = canvas.getAllByRole("checkbox");
|
|
||||||
expect(checkboxes).toHaveLength(2);
|
|
||||||
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await userEvent.click(checkboxes[0]);
|
|
||||||
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await userEvent.click(checkboxes[1]);
|
|
||||||
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const InverseInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const checkboxes = canvas.getAllByRole("checkbox");
|
|
||||||
expect(checkboxes).toHaveLength(2);
|
|
||||||
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await userEvent.click(checkboxes[0]);
|
|
||||||
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await userEvent.click(checkboxes[1]);
|
|
||||||
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Forms/Checkbox",
|
title: "Forms/Checkbox",
|
||||||
component: Checkbox,
|
component: Checkbox,
|
||||||
@@ -137,8 +109,7 @@ export const Checked = {
|
|||||||
|
|
||||||
export const Standard = {
|
export const Standard = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [unchecked, setUnchecked] = React.useState(false);
|
const [checked, setChecked] = React.useState(false);
|
||||||
const [checked, setChecked] = React.useState(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -146,13 +117,7 @@ export const Standard = {
|
|||||||
<h3 className="text-white font-medium">Standard Mode</h3>
|
<h3 className="text-white font-medium">Standard Mode</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Unchecked"
|
label="Standard Checkbox"
|
||||||
checked={unchecked}
|
|
||||||
mode="standard"
|
|
||||||
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Checked"
|
|
||||||
checked={checked}
|
checked={checked}
|
||||||
mode="standard"
|
mode="standard"
|
||||||
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||||
@@ -162,13 +127,11 @@ export const Standard = {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: StandardInteraction.play,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Inverse = {
|
export const Inverse = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [unchecked, setUnchecked] = React.useState(false);
|
const [checked, setChecked] = React.useState(false);
|
||||||
const [checked, setChecked] = React.useState(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -176,13 +139,7 @@ export const Inverse = {
|
|||||||
<h3 className="text-white font-medium">Inverse Mode</h3>
|
<h3 className="text-white font-medium">Inverse Mode</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Unchecked"
|
label="Inverse Checkbox"
|
||||||
checked={unchecked}
|
|
||||||
mode="inverse"
|
|
||||||
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Checked"
|
|
||||||
checked={checked}
|
checked={checked}
|
||||||
mode="inverse"
|
mode="inverse"
|
||||||
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||||
@@ -192,5 +149,60 @@ export const Inverse = {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: InverseInteraction.play,
|
};
|
||||||
|
|
||||||
|
export const Disabled = {
|
||||||
|
args: {
|
||||||
|
checked: false,
|
||||||
|
mode: "standard",
|
||||||
|
state: "default",
|
||||||
|
disabled: true,
|
||||||
|
label: "Disabled checkbox",
|
||||||
|
},
|
||||||
|
render: (args) => <Checkbox {...args} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisabledChecked = {
|
||||||
|
args: {
|
||||||
|
checked: true,
|
||||||
|
mode: "standard",
|
||||||
|
state: "default",
|
||||||
|
disabled: true,
|
||||||
|
label: "Disabled checked checkbox",
|
||||||
|
},
|
||||||
|
render: (args) => <Checkbox {...args} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
// All modes comparison
|
||||||
|
export const AllModes = () => {
|
||||||
|
const [standardChecked, setStandardChecked] = React.useState(false);
|
||||||
|
const [inverseChecked, setInverseChecked] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Checkbox
|
||||||
|
label="Standard Checkbox"
|
||||||
|
checked={standardChecked}
|
||||||
|
mode="standard"
|
||||||
|
onChange={({ checked }) => setStandardChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Checkbox
|
||||||
|
label="Inverse Checkbox"
|
||||||
|
checked={inverseChecked}
|
||||||
|
mode="inverse"
|
||||||
|
onChange={({ checked }) => setInverseChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import React from "react";
|
||||||
|
import CheckboxGroup from "../app/components/CheckboxGroup";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Forms/CheckboxGroup",
|
||||||
|
component: CheckboxGroup,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
backgrounds: {
|
||||||
|
default: "dark",
|
||||||
|
values: [
|
||||||
|
{ name: "light", value: "#ffffff" },
|
||||||
|
{ name: "dark", value: "#000000" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
mode: {
|
||||||
|
control: "select",
|
||||||
|
options: ["standard", "inverse"],
|
||||||
|
description: "Visual mode of the checkbox group",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the checkbox group is disabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
render: () => {
|
||||||
|
const [value, setValue] = React.useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxGroup
|
||||||
|
name="default-checkbox-group"
|
||||||
|
value={value}
|
||||||
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{ value: "option2", label: "Checkbox label" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithSubtext = {
|
||||||
|
render: () => {
|
||||||
|
const [value, setValue] = React.useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxGroup
|
||||||
|
name="subtext-checkbox-group"
|
||||||
|
value={value}
|
||||||
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{
|
||||||
|
value: "option2",
|
||||||
|
label: "Checkbox label",
|
||||||
|
subtext: "Nunc sed hendrerit consequat.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Inverse = {
|
||||||
|
render: () => {
|
||||||
|
const [value, setValue] = React.useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxGroup
|
||||||
|
name="inverse-checkbox-group"
|
||||||
|
value={value}
|
||||||
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{ value: "option2", label: "Checkbox label" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InverseWithSubtext = {
|
||||||
|
render: () => {
|
||||||
|
const [value, setValue] = React.useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxGroup
|
||||||
|
name="inverse-subtext-checkbox-group"
|
||||||
|
value={value}
|
||||||
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{
|
||||||
|
value: "option2",
|
||||||
|
label: "Checkbox label",
|
||||||
|
subtext: "Nunc sed hendrerit consequat.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = {
|
||||||
|
render: () => (
|
||||||
|
<CheckboxGroup
|
||||||
|
name="disabled-checkbox-group"
|
||||||
|
value={[]}
|
||||||
|
mode="standard"
|
||||||
|
disabled
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{ value: "option2", label: "Checkbox label" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllModes = () => {
|
||||||
|
const [standardValue, setStandardValue] = React.useState([]);
|
||||||
|
const [inverseValue, setInverseValue] = React.useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode</h3>
|
||||||
|
<CheckboxGroup
|
||||||
|
name="standard-all-checkbox-group"
|
||||||
|
value={standardValue}
|
||||||
|
onChange={({ value }) => setStandardValue(value)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Checkbox label" },
|
||||||
|
{
|
||||||
|
value: "option2",
|
||||||
|
label: "Checkbox label",
|
||||||
|
subtext: "Nunc sed hendrerit consequat.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode</h3>
|
||||||
|
<CheckboxGroup
|
||||||
|
name="inverse-all-checkbox-group"
|
||||||
|
value={inverseValue}
|
||||||
|
onChange={({ value }) => setInverseValue(value)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option3", label: "Checkbox label" },
|
||||||
|
{
|
||||||
|
value: "option4",
|
||||||
|
label: "Checkbox label",
|
||||||
|
subtext: "Nunc sed hendrerit consequat.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Create from "../app/components/Create";
|
import Create from "../app/components/Create";
|
||||||
import Input from "../app/components/Input";
|
import TextInput from "../app/components/TextInput";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Create",
|
title: "Components/Create",
|
||||||
@@ -57,7 +57,7 @@ Default.args = {
|
|||||||
description: "You can also combine or add new approaches to the list",
|
description: "You can also combine or add new approaches to the list",
|
||||||
children: (
|
children: (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input label="Label" placeholder="Policy name" value="" />
|
<TextInput label="Label" placeholder="Policy name" value="" />
|
||||||
<p className="text-[12px] text-[var(--color-content-default-tertiary)]">
|
<p className="text-[12px] text-[var(--color-content-default-tertiary)]">
|
||||||
0/48
|
0/48
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +77,7 @@ WithStepper.args = {
|
|||||||
description: "You can also combine or add new approaches to the list",
|
description: "You can also combine or add new approaches to the list",
|
||||||
children: (
|
children: (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input label="Label" placeholder="Policy name" value="" />
|
<TextInput label="Label" placeholder="Policy name" value="" />
|
||||||
<p className="text-[12px] text-[var(--color-content-default-tertiary)]">
|
<p className="text-[12px] text-[var(--color-content-default-tertiary)]">
|
||||||
0/48
|
0/48
|
||||||
</p>
|
</p>
|
||||||
@@ -99,7 +99,7 @@ Step2.args = {
|
|||||||
description: "You can also combine or add new approaches to the list",
|
description: "You can also combine or add new approaches to the list",
|
||||||
children: (
|
children: (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input label="Label" placeholder="Enter text" value="" />
|
<TextInput label="Label" placeholder="Enter text" value="" />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
@@ -155,7 +155,7 @@ NextButtonDisabled.args = {
|
|||||||
description: "You can also combine or add new approaches to the list",
|
description: "You can also combine or add new approaches to the list",
|
||||||
children: (
|
children: (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input label="Label" placeholder="Policy name" value="" />
|
<TextInput label="Label" placeholder="Policy name" value="" />
|
||||||
<p className="text-[12px] text-[var(--color-content-default-tertiary)]">
|
<p className="text-[12px] text-[var(--color-content-default-tertiary)]">
|
||||||
0/48
|
0/48
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import NumberCard from "../app/components/NumberCard";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/NumberCard",
|
||||||
|
component: NumberCard,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Individual number card component that displays a step in a process with a numbered icon and descriptive text. Supports explicit size variants (Small, Medium, Large, XLarge) matching Figma designs, or responsive layouts when size is not specified.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
number: {
|
||||||
|
control: { type: "number", min: 1, max: 9 },
|
||||||
|
description: "The number to display on the card",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "The descriptive text for this step",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["Small", "Medium", "Large", "XLarge", undefined],
|
||||||
|
description:
|
||||||
|
"Explicit size variant matching Figma designs. If not specified, uses responsive breakpoints for backward compatibility.",
|
||||||
|
},
|
||||||
|
iconShape: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["blob", "gear", "star"],
|
||||||
|
description:
|
||||||
|
"The shape of the icon background (currently not used, uses PNG images)",
|
||||||
|
},
|
||||||
|
iconColor: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["green", "purple", "orange", "blue"],
|
||||||
|
description:
|
||||||
|
"The color theme for the icon (currently not used, uses PNG images)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "Document how your community makes decisions",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "Document how your community makes decisions",
|
||||||
|
size: "Small",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Small size variant: flex-col layout with items-end, 16px gap, 20px padding, 24px text with 32px line height. Section number positioned top-right.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "Document how your community makes decisions",
|
||||||
|
size: "Medium",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Medium size variant: flex-row layout with items-center, 32px gap, 32px padding, 24px text with 24px line height. Section number on left side.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "Document how your community makes decisions",
|
||||||
|
size: "Large",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Large size variant: flex-col layout with items-start justify-end, 22px gap, 238px height, 32px padding, 24px text with 24px line height. Section number absolute top-right.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const XLarge = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "Document how your community makes decisions",
|
||||||
|
size: "XLarge",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"XLarge size variant: flex-col layout with items-start justify-end, 22px gap, 238px height, 32px padding, 32px text with 32px line height. Section number absolute top-right.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllSizes = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-lg font-semibold">Small</h3>
|
||||||
|
<NumberCard
|
||||||
|
number={1}
|
||||||
|
text="Document how your community makes decisions"
|
||||||
|
size="Small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-lg font-semibold">Medium</h3>
|
||||||
|
<NumberCard
|
||||||
|
number={2}
|
||||||
|
text="Document how your community makes decisions"
|
||||||
|
size="Medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-lg font-semibold">Large</h3>
|
||||||
|
<NumberCard
|
||||||
|
number={3}
|
||||||
|
text="Document how your community makes decisions"
|
||||||
|
size="Large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-lg font-semibold">XLarge</h3>
|
||||||
|
<NumberCard
|
||||||
|
number={1}
|
||||||
|
text="Document how your community makes decisions"
|
||||||
|
size="XLarge"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Shows all four size variants side by side to compare the different layouts and typography.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllNumbers = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "Example card text",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<NumberCard {...args} number={1} text="First step in the process" />
|
||||||
|
<NumberCard
|
||||||
|
{...args}
|
||||||
|
number={2}
|
||||||
|
text="Second step with different content"
|
||||||
|
/>
|
||||||
|
<NumberCard
|
||||||
|
{...args}
|
||||||
|
number={3}
|
||||||
|
text="Third and final step of the workflow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Shows all three numbered cards with different content to demonstrate the visual hierarchy.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LongText = {
|
||||||
|
args: {
|
||||||
|
number: 1,
|
||||||
|
text: "This is a much longer piece of text that demonstrates how the card handles content that spans multiple lines and requires more space to display properly",
|
||||||
|
iconShape: "blob",
|
||||||
|
iconColor: "green",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Demonstrates how the card handles longer text content across different breakpoints.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import NumberedCard from "../app/components/NumberedCard";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Components/NumberedCard",
|
|
||||||
component: NumberedCard,
|
|
||||||
parameters: {
|
|
||||||
layout: "centered",
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component:
|
|
||||||
"Individual numbered card component that displays a step in a process with a numbered icon and descriptive text. Supports responsive layouts across different breakpoints.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
number: {
|
|
||||||
control: { type: "number", min: 1, max: 9 },
|
|
||||||
description: "The number to display on the card",
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
control: { type: "text" },
|
|
||||||
description: "The descriptive text for this step",
|
|
||||||
},
|
|
||||||
iconShape: {
|
|
||||||
control: { type: "select" },
|
|
||||||
options: ["blob", "gear", "star"],
|
|
||||||
description:
|
|
||||||
"The shape of the icon background (currently not used, uses PNG images)",
|
|
||||||
},
|
|
||||||
iconColor: {
|
|
||||||
control: { type: "select" },
|
|
||||||
options: ["green", "purple", "orange", "blue"],
|
|
||||||
description:
|
|
||||||
"The color theme for the icon (currently not used, uses PNG images)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tags: ["autodocs"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default = {
|
|
||||||
args: {
|
|
||||||
number: 1,
|
|
||||||
text: "Document how your community makes decisions",
|
|
||||||
iconShape: "blob",
|
|
||||||
iconColor: "green",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllNumbers = {
|
|
||||||
args: {
|
|
||||||
number: 1,
|
|
||||||
text: "Example card text",
|
|
||||||
iconShape: "blob",
|
|
||||||
iconColor: "green",
|
|
||||||
},
|
|
||||||
render: (args) => (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<NumberedCard {...args} number={1} text="First step in the process" />
|
|
||||||
<NumberedCard
|
|
||||||
{...args}
|
|
||||||
number={2}
|
|
||||||
text="Second step with different content"
|
|
||||||
/>
|
|
||||||
<NumberedCard
|
|
||||||
{...args}
|
|
||||||
number={3}
|
|
||||||
text="Third and final step of the workflow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
story:
|
|
||||||
"Shows all three numbered cards with different content to demonstrate the visual hierarchy.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LongText = {
|
|
||||||
args: {
|
|
||||||
number: 1,
|
|
||||||
text: "This is a much longer piece of text that demonstrates how the card handles content that spans multiple lines and requires more space to display properly",
|
|
||||||
iconShape: "blob",
|
|
||||||
iconColor: "green",
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
story:
|
|
||||||
"Demonstrates how the card handles longer text content across different breakpoints.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
component:
|
component:
|
||||||
"A component system for visually communicating multi-step workflows, processes, or value propositions. The component's modular design with NumberedCard and SectionNumber sub-components makes it ideal for explaining any sequential process while maintaining brand consistency and accessibility standards across the design system.",
|
"A component system for visually communicating multi-step workflows, processes, or value propositions. The component's modular design with NumberCard and SectionNumber sub-components makes it ideal for explaining any sequential process while maintaining brand consistency and accessibility standards across the design system.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+155
-104
@@ -1,96 +1,53 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import RadioButton from "../app/components/RadioButton";
|
import RadioButton from "../app/components/RadioButton";
|
||||||
import { expect } from "@storybook/test";
|
|
||||||
import { userEvent, within } from "@storybook/test";
|
|
||||||
|
|
||||||
// Interaction functions for Storybook play functions
|
export default {
|
||||||
const DefaultInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioButton = canvas.getByRole("radio");
|
|
||||||
await expect(radioButton).toHaveAttribute("aria-checked", "false");
|
|
||||||
await userEvent.click(radioButton);
|
|
||||||
await expect(radioButton).toHaveAttribute("aria-checked", "true");
|
|
||||||
await userEvent.click(radioButton);
|
|
||||||
await expect(radioButton).toHaveAttribute("aria-checked", "true");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CheckedInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioButton = canvas.getByRole("radio");
|
|
||||||
await expect(radioButton).toHaveAttribute("aria-checked", "true");
|
|
||||||
await userEvent.click(radioButton);
|
|
||||||
await expect(radioButton).toHaveAttribute("aria-checked", "true");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const StandardInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await userEvent.click(radioButtons[0]);
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const InverseInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await userEvent.click(radioButtons[0]);
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: "Forms/RadioButton",
|
title: "Forms/RadioButton",
|
||||||
component: RadioButton,
|
component: RadioButton,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "centered",
|
layout: "centered",
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
default: "dark",
|
default: "dark",
|
||||||
values: [{ name: "dark", value: "black" }],
|
values: [
|
||||||
|
{ name: "light", value: "#ffffff" },
|
||||||
|
{ name: "dark", value: "#000000" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tags: ["autodocs"],
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
checked: { control: "boolean" },
|
checked: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the radio button is checked",
|
||||||
|
},
|
||||||
mode: {
|
mode: {
|
||||||
control: { type: "select" },
|
control: "select",
|
||||||
options: ["standard", "inverse"],
|
options: ["standard", "inverse"],
|
||||||
|
description: "Visual mode of the radio button",
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
control: { type: "select" },
|
control: "select",
|
||||||
options: ["default", "hover", "focus"],
|
options: ["default", "hover", "focus"],
|
||||||
|
description: "Interaction state for static display",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: "boolean",
|
||||||
|
description: "Whether the radio button is disabled",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: "text",
|
||||||
|
description: "Label text for the radio button",
|
||||||
},
|
},
|
||||||
label: { control: "text" },
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
checked: false,
|
|
||||||
mode: "standard",
|
|
||||||
state: "default",
|
|
||||||
label: "Radio Button Label",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
export const Default = {
|
export const Default = {
|
||||||
args: {
|
args: {
|
||||||
checked: false,
|
checked: false,
|
||||||
mode: "standard",
|
mode: "standard",
|
||||||
state: "default",
|
state: "default",
|
||||||
|
disabled: false,
|
||||||
label: "Default radio button",
|
label: "Default radio button",
|
||||||
},
|
},
|
||||||
play: DefaultInteraction.play,
|
|
||||||
render: (args) => {
|
render: (args) => {
|
||||||
const [checked, setChecked] = React.useState(args.checked);
|
const [checked, setChecked] = React.useState(args.checked);
|
||||||
return (
|
return (
|
||||||
@@ -108,9 +65,9 @@ export const Checked = {
|
|||||||
checked: true,
|
checked: true,
|
||||||
mode: "standard",
|
mode: "standard",
|
||||||
state: "default",
|
state: "default",
|
||||||
|
disabled: false,
|
||||||
label: "Checked radio button",
|
label: "Checked radio button",
|
||||||
},
|
},
|
||||||
play: CheckedInteraction.play,
|
|
||||||
render: (args) => {
|
render: (args) => {
|
||||||
const [checked, setChecked] = React.useState(args.checked);
|
const [checked, setChecked] = React.useState(args.checked);
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +82,7 @@ export const Checked = {
|
|||||||
|
|
||||||
export const Standard = {
|
export const Standard = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [selectedValue, setSelectedValue] = React.useState("checked");
|
const [checked, setChecked] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -133,36 +90,21 @@ export const Standard = {
|
|||||||
<h3 className="text-white font-medium">Standard Mode</h3>
|
<h3 className="text-white font-medium">Standard Mode</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label="Unchecked"
|
label="Standard Radio Button"
|
||||||
checked={selectedValue === "unchecked"}
|
checked={checked}
|
||||||
name="standard-example"
|
|
||||||
value="unchecked"
|
|
||||||
mode="standard"
|
mode="standard"
|
||||||
onChange={({ checked }) => {
|
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||||
if (checked) setSelectedValue("unchecked");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RadioButton
|
|
||||||
label="Checked"
|
|
||||||
checked={selectedValue === "checked"}
|
|
||||||
name="standard-example"
|
|
||||||
value="checked"
|
|
||||||
mode="standard"
|
|
||||||
onChange={({ checked }) => {
|
|
||||||
if (checked) setSelectedValue("checked");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: StandardInteraction.play,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Inverse = {
|
export const Inverse = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [selectedValue, setSelectedValue] = React.useState("checked");
|
const [checked, setChecked] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -170,29 +112,138 @@ export const Inverse = {
|
|||||||
<h3 className="text-white font-medium">Inverse Mode</h3>
|
<h3 className="text-white font-medium">Inverse Mode</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<RadioButton
|
<RadioButton
|
||||||
label="Unchecked"
|
label="Inverse Radio Button"
|
||||||
checked={selectedValue === "unchecked"}
|
checked={checked}
|
||||||
name="inverse-example"
|
|
||||||
value="unchecked"
|
|
||||||
mode="inverse"
|
mode="inverse"
|
||||||
onChange={({ checked }) => {
|
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||||
if (checked) setSelectedValue("unchecked");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<RadioButton
|
|
||||||
label="Checked"
|
|
||||||
checked={selectedValue === "checked"}
|
|
||||||
name="inverse-example"
|
|
||||||
value="checked"
|
|
||||||
mode="inverse"
|
|
||||||
onChange={({ checked }) => {
|
|
||||||
if (checked) setSelectedValue("checked");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: InverseInteraction.play,
|
};
|
||||||
|
|
||||||
|
export const Disabled = {
|
||||||
|
args: {
|
||||||
|
checked: false,
|
||||||
|
mode: "standard",
|
||||||
|
state: "default",
|
||||||
|
disabled: true,
|
||||||
|
label: "Disabled radio button",
|
||||||
|
},
|
||||||
|
render: (args) => <RadioButton {...args} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisabledChecked = {
|
||||||
|
args: {
|
||||||
|
checked: true,
|
||||||
|
mode: "standard",
|
||||||
|
state: "default",
|
||||||
|
disabled: true,
|
||||||
|
label: "Disabled checked radio button",
|
||||||
|
},
|
||||||
|
render: (args) => <RadioButton {...args} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
// All modes comparison
|
||||||
|
export const AllModes = () => {
|
||||||
|
const [standardChecked, setStandardChecked] = React.useState(false);
|
||||||
|
const [inverseChecked, setInverseChecked] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioButton
|
||||||
|
label="Standard Radio Button"
|
||||||
|
checked={standardChecked}
|
||||||
|
mode="standard"
|
||||||
|
onChange={({ checked }) => setStandardChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioButton
|
||||||
|
label="Inverse Radio Button"
|
||||||
|
checked={inverseChecked}
|
||||||
|
mode="inverse"
|
||||||
|
onChange={({ checked }) => setInverseChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// All states for standard mode
|
||||||
|
export const StandardAllStates = () => {
|
||||||
|
const [unchecked, setUnchecked] = React.useState(false);
|
||||||
|
const [checked, setChecked] = React.useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode - Unselected</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioButton
|
||||||
|
label="Unselected (default, hover, focus)"
|
||||||
|
checked={unchecked}
|
||||||
|
mode="standard"
|
||||||
|
onChange={({ checked }) => setUnchecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode - Selected</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioButton
|
||||||
|
label="Selected (default, hover, focus)"
|
||||||
|
checked={checked}
|
||||||
|
mode="standard"
|
||||||
|
onChange={({ checked }) => setChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// All states for inverse mode
|
||||||
|
export const InverseAllStates = () => {
|
||||||
|
const [unchecked, setUnchecked] = React.useState(false);
|
||||||
|
const [checked, setChecked] = React.useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode - Unselected</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioButton
|
||||||
|
label="Unselected (default, hover, focus)"
|
||||||
|
checked={unchecked}
|
||||||
|
mode="inverse"
|
||||||
|
onChange={({ checked }) => setUnchecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode - Selected</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioButton
|
||||||
|
label="Selected (default, hover, focus)"
|
||||||
|
checked={checked}
|
||||||
|
mode="inverse"
|
||||||
|
onChange={({ checked }) => setChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+125
-153
@@ -1,203 +1,175 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import RadioGroup from "../app/components/RadioGroup";
|
import RadioGroup from "../app/components/RadioGroup";
|
||||||
import { expect } from "@storybook/test";
|
|
||||||
import { userEvent, within } from "@storybook/test";
|
|
||||||
|
|
||||||
// Interaction functions for Storybook play functions
|
export default {
|
||||||
const DefaultInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioGroup = canvas.getByRole("radiogroup");
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
|
||||||
await expect(radioGroup).toBeInTheDocument();
|
|
||||||
await expect(radioButtons).toHaveLength(3);
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const StandardInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioGroup = canvas.getByRole("radiogroup");
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
|
||||||
await expect(radioGroup).toBeInTheDocument();
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await userEvent.click(radioButtons[0]);
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const InverseInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioGroup = canvas.getByRole("radiogroup");
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
|
||||||
await expect(radioGroup).toBeInTheDocument();
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await userEvent.click(radioButtons[1]);
|
|
||||||
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
|
|
||||||
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
|
|
||||||
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const InteractiveInteraction = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
const radioGroup = canvas.getByRole("radiogroup");
|
|
||||||
const radioButtons = canvas.getAllByRole("radio");
|
|
||||||
await expect(radioGroup).toBeInTheDocument();
|
|
||||||
await expect(canvas.getByText("Selected: option1")).toBeVisible();
|
|
||||||
await userEvent.click(radioButtons[1]);
|
|
||||||
await expect(canvas.getByText("Selected: option2")).toBeVisible();
|
|
||||||
await userEvent.click(radioButtons[2]);
|
|
||||||
await expect(canvas.getByText("Selected: option3")).toBeVisible();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: "Forms/RadioGroup",
|
title: "Forms/RadioGroup",
|
||||||
component: RadioGroup,
|
component: RadioGroup,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "centered",
|
layout: "centered",
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
default: "dark",
|
default: "dark",
|
||||||
values: [{ name: "dark", value: "black" }],
|
values: [
|
||||||
|
{ name: "light", value: "#ffffff" },
|
||||||
|
{ name: "dark", value: "#000000" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tags: ["autodocs"],
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
mode: {
|
mode: {
|
||||||
control: { type: "select" },
|
control: "select",
|
||||||
options: ["standard", "inverse"],
|
options: ["standard", "inverse"],
|
||||||
|
description: "Visual mode of the radio group",
|
||||||
},
|
},
|
||||||
state: {
|
disabled: {
|
||||||
control: { type: "select" },
|
control: "boolean",
|
||||||
options: ["default", "hover", "focus"],
|
description: "Whether the radio group is disabled",
|
||||||
},
|
},
|
||||||
value: { control: "text" },
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
mode: "standard",
|
|
||||||
state: "default",
|
|
||||||
value: "option1",
|
|
||||||
options: [
|
|
||||||
{ value: "option1", label: "Option 1" },
|
|
||||||
{ value: "option2", label: "Option 2" },
|
|
||||||
{ value: "option3", label: "Option 3" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
export const Default = {
|
export const Default = {
|
||||||
args: {
|
render: () => {
|
||||||
mode: "standard",
|
const [value, setValue] = React.useState("");
|
||||||
state: "default",
|
|
||||||
value: "option1",
|
|
||||||
options: [
|
|
||||||
{ value: "option1", label: "Option 1" },
|
|
||||||
{ value: "option2", label: "Option 2" },
|
|
||||||
{ value: "option3", label: "Option 3" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
play: DefaultInteraction.play,
|
|
||||||
render: (args) => {
|
|
||||||
const [value, setValue] = React.useState(args.value);
|
|
||||||
return (
|
return (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
{...args}
|
name="default-radio-group"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={({ value: newValue }) => setValue(newValue)}
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Standard = {
|
export const WithSubtext = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [value, setValue] = React.useState("option2");
|
const [value, setValue] = React.useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<RadioGroup
|
||||||
<div className="space-y-2">
|
name="subtext-radio-group"
|
||||||
<h3 className="text-white font-medium">Standard Mode</h3>
|
value={value}
|
||||||
<RadioGroup
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
name="standard-example"
|
mode="standard"
|
||||||
value={value}
|
options={[
|
||||||
mode="standard"
|
{ value: "option1", label: "Option 1" },
|
||||||
options={[
|
{
|
||||||
{ value: "option1", label: "Option 1" },
|
value: "option2",
|
||||||
{ value: "option2", label: "Option 2" },
|
label: "Option 2",
|
||||||
{ value: "option3", label: "Option 3" },
|
subtext: "Lorem ipsum dolor sit amet consectetur",
|
||||||
]}
|
},
|
||||||
onChange={({ value: newValue }) => setValue(newValue)}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: StandardInteraction.play,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Inverse = {
|
export const Inverse = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [value, setValue] = React.useState("option1");
|
const [value, setValue] = React.useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<RadioGroup
|
||||||
<div className="space-y-2">
|
name="inverse-radio-group"
|
||||||
<h3 className="text-white font-medium">Inverse Mode</h3>
|
value={value}
|
||||||
<RadioGroup
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
name="inverse-example"
|
mode="inverse"
|
||||||
value={value}
|
options={[
|
||||||
mode="inverse"
|
{ value: "option1", label: "Option 1" },
|
||||||
options={[
|
{ value: "option2", label: "Option 2" },
|
||||||
{ value: "option1", label: "Option 1" },
|
{ value: "option3", label: "Option 3" },
|
||||||
{ value: "option2", label: "Option 2" },
|
]}
|
||||||
{ value: "option3", label: "Option 3" },
|
/>
|
||||||
]}
|
|
||||||
onChange={({ value: newValue }) => setValue(newValue)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: InverseInteraction.play,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Interactive = {
|
export const InverseWithSubtext = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [value, setValue] = React.useState("option1");
|
const [value, setValue] = React.useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<RadioGroup
|
||||||
<div className="space-y-2">
|
name="inverse-subtext-radio-group"
|
||||||
<h3 className="text-white font-medium">Interactive Example</h3>
|
value={value}
|
||||||
<p className="text-gray-400 text-sm">Selected: {value}</p>
|
onChange={({ value: newValue }) => setValue(newValue)}
|
||||||
<RadioGroup
|
mode="inverse"
|
||||||
name="interactive-example"
|
options={[
|
||||||
value={value}
|
{ value: "option1", label: "Option 1" },
|
||||||
mode="standard"
|
{
|
||||||
options={[
|
value: "option2",
|
||||||
{ value: "option1", label: "Option 1" },
|
label: "Option 2",
|
||||||
{ value: "option2", label: "Option 2" },
|
subtext: "Lorem ipsum dolor sit amet consectetur",
|
||||||
{ value: "option3", label: "Option 3" },
|
},
|
||||||
]}
|
]}
|
||||||
onChange={({ value }) => setValue(value)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
play: InteractiveInteraction.play,
|
};
|
||||||
|
|
||||||
|
export const Disabled = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup
|
||||||
|
name="disabled-radio-group"
|
||||||
|
value=""
|
||||||
|
mode="standard"
|
||||||
|
disabled
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllModes = () => {
|
||||||
|
const [standardValue, setStandardValue] = React.useState("");
|
||||||
|
const [inverseValue, setInverseValue] = React.useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Standard Mode</h3>
|
||||||
|
<RadioGroup
|
||||||
|
name="standard-all-radio-group"
|
||||||
|
value={standardValue}
|
||||||
|
onChange={({ value }) => setStandardValue(value)}
|
||||||
|
mode="standard"
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{
|
||||||
|
value: "option2",
|
||||||
|
label: "Option 2",
|
||||||
|
subtext: "Lorem ipsum dolor sit amet consectetur",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-white">Inverse Mode</h3>
|
||||||
|
<RadioGroup
|
||||||
|
name="inverse-all-radio-group"
|
||||||
|
value={inverseValue}
|
||||||
|
onChange={({ value }) => setInverseValue(value)}
|
||||||
|
mode="inverse"
|
||||||
|
options={[
|
||||||
|
{ value: "option3", label: "Option 1" },
|
||||||
|
{
|
||||||
|
value: "option4",
|
||||||
|
label: "Option 2",
|
||||||
|
subtext: "Lorem ipsum dolor sit amet consectetur",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import Select from "../app/components/Select";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Forms/Select",
|
|
||||||
component: Select,
|
|
||||||
argTypes: {
|
|
||||||
size: {
|
|
||||||
control: { type: "select" },
|
|
||||||
options: ["small", "medium", "large"],
|
|
||||||
},
|
|
||||||
labelVariant: {
|
|
||||||
control: { type: "select" },
|
|
||||||
options: ["default", "horizontal"],
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
control: { type: "select" },
|
|
||||||
options: ["default", "hover", "focus", "error", "disabled"],
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
control: { type: "boolean" },
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
control: { type: "boolean" },
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
control: { type: "text" },
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
control: { type: "text" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Template = (args) => {
|
|
||||||
const [value, setValue] = useState("");
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
{...args}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
options={[
|
|
||||||
{ value: "option1", label: "Option 1" },
|
|
||||||
{ value: "option2", label: "Option 2" },
|
|
||||||
{ value: "option3", label: "Option 3" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
|
||||||
Default.args = {
|
|
||||||
label: "Default Select",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Small = Template.bind({});
|
|
||||||
Small.args = {
|
|
||||||
label: "Small Select",
|
|
||||||
size: "small",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Medium = Template.bind({});
|
|
||||||
Medium.args = {
|
|
||||||
label: "Medium Select",
|
|
||||||
size: "medium",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Large = Template.bind({});
|
|
||||||
Large.args = {
|
|
||||||
label: "Large Select",
|
|
||||||
size: "large",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DefaultLabel = Template.bind({});
|
|
||||||
DefaultLabel.args = {
|
|
||||||
label: "Default (Top Label)",
|
|
||||||
labelVariant: "default",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HorizontalLabel = Template.bind({});
|
|
||||||
HorizontalLabel.args = {
|
|
||||||
label: "Horizontal (Left Label)",
|
|
||||||
labelVariant: "horizontal",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Active = Template.bind({});
|
|
||||||
Active.args = {
|
|
||||||
label: "Active State",
|
|
||||||
state: "default",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Hover = Template.bind({});
|
|
||||||
Hover.args = {
|
|
||||||
label: "Hover State",
|
|
||||||
state: "hover",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Focus = Template.bind({});
|
|
||||||
Focus.args = {
|
|
||||||
label: "Focus State",
|
|
||||||
state: "focus",
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Error = Template.bind({});
|
|
||||||
Error.args = {
|
|
||||||
label: "Error State",
|
|
||||||
error: true,
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Disabled = Template.bind({});
|
|
||||||
Disabled.args = {
|
|
||||||
label: "Disabled State",
|
|
||||||
disabled: true,
|
|
||||||
placeholder: "Select",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Interactive = Template.bind({});
|
|
||||||
Interactive.args = {
|
|
||||||
label: "Interactive Select",
|
|
||||||
placeholder: "Choose an option",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Comparison stories
|
|
||||||
export const AllSizes = () => {
|
|
||||||
const [smallValue, setSmallValue] = useState("");
|
|
||||||
const [mediumValue, setMediumValue] = useState("");
|
|
||||||
const [largeValue, setLargeValue] = useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Select
|
|
||||||
label="Small"
|
|
||||||
size="small"
|
|
||||||
value={smallValue}
|
|
||||||
onChange={(e) => setSmallValue(e.target.value)}
|
|
||||||
placeholder="Select"
|
|
||||||
>
|
|
||||||
<option value="item1">Context Menu Item 1</option>
|
|
||||||
<option value="item2">Context Menu Item 2</option>
|
|
||||||
<option value="item3">Context Menu Item 3</option>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
label="Medium"
|
|
||||||
size="medium"
|
|
||||||
value={mediumValue}
|
|
||||||
onChange={(e) => setMediumValue(e.target.value)}
|
|
||||||
placeholder="Select"
|
|
||||||
>
|
|
||||||
<option value="item1">Context Menu Item 1</option>
|
|
||||||
<option value="item2">Context Menu Item 2</option>
|
|
||||||
<option value="item3">Context Menu Item 3</option>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
label="Large"
|
|
||||||
size="large"
|
|
||||||
value={largeValue}
|
|
||||||
onChange={(e) => setLargeValue(e.target.value)}
|
|
||||||
placeholder="Select"
|
|
||||||
>
|
|
||||||
<option value="item1">Context Menu Item 1</option>
|
|
||||||
<option value="item2">Context Menu Item 2</option>
|
|
||||||
<option value="item3">Context Menu Item 3</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllStates = () => {
|
|
||||||
const [defaultValue, setDefaultValue] = useState("");
|
|
||||||
const [errorValue, setErrorValue] = useState("");
|
|
||||||
const [disabledValue, setDisabledValue] = useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Select
|
|
||||||
label="Default State"
|
|
||||||
value={defaultValue}
|
|
||||||
onChange={(e) => setDefaultValue(e.target.value)}
|
|
||||||
placeholder="Select"
|
|
||||||
>
|
|
||||||
<option value="item1">Context Menu Item 1</option>
|
|
||||||
<option value="item2">Context Menu Item 2</option>
|
|
||||||
<option value="item3">Context Menu Item 3</option>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
label="Error State"
|
|
||||||
error={true}
|
|
||||||
value={errorValue}
|
|
||||||
onChange={(e) => setErrorValue(e.target.value)}
|
|
||||||
placeholder="Select"
|
|
||||||
>
|
|
||||||
<option value="item1">Context Menu Item 1</option>
|
|
||||||
<option value="item2">Context Menu Item 2</option>
|
|
||||||
<option value="item3">Context Menu Item 3</option>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
label="Disabled State"
|
|
||||||
disabled={true}
|
|
||||||
value={disabledValue}
|
|
||||||
onChange={(e) => setDisabledValue(e.target.value)}
|
|
||||||
placeholder="Select"
|
|
||||||
>
|
|
||||||
<option value="item1">Context Menu Item 1</option>
|
|
||||||
<option value="item2">Context Menu Item 2</option>
|
|
||||||
<option value="item3">Context Menu Item 3</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import SelectInput from "../app/components/SelectInput";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Forms/SelectInput",
|
||||||
|
component: SelectInput,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
state: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["default", "active", "focus"],
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => {
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
return (
|
||||||
|
<SelectInput
|
||||||
|
{...args}
|
||||||
|
value={value}
|
||||||
|
onChange={(data) => setValue(data.target.value)}
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default story
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
label: "Default Select Input",
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
state: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
// States
|
||||||
|
export const Active = Template.bind({});
|
||||||
|
Active.args = {
|
||||||
|
label: "Active State",
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
state: "active",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Focus = Template.bind({});
|
||||||
|
Focus.args = {
|
||||||
|
label: "Focus State",
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
state: "focus",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Error = Template.bind({});
|
||||||
|
Error.args = {
|
||||||
|
label: "Error State",
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
error: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = Template.bind({});
|
||||||
|
Disabled.args = {
|
||||||
|
label: "Disabled State",
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interactive example
|
||||||
|
export const Interactive = (args) => {
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SelectInput
|
||||||
|
{...args}
|
||||||
|
value={value}
|
||||||
|
onChange={(data) => setValue(data.target.value)}
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-600">Current value: "{value}"</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Interactive.args = {
|
||||||
|
label: "Interactive Select Input",
|
||||||
|
placeholder: "Choose an option",
|
||||||
|
state: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
// All states comparison
|
||||||
|
export const AllStates = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Select Input States</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SelectInput
|
||||||
|
label="Default State"
|
||||||
|
placeholder="Choose an option"
|
||||||
|
value=""
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label="Active State"
|
||||||
|
placeholder="Choose an option"
|
||||||
|
state="active"
|
||||||
|
value=""
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label="Focus State"
|
||||||
|
placeholder="Choose an option"
|
||||||
|
state="focus"
|
||||||
|
value=""
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label="Error State"
|
||||||
|
placeholder="Choose an option"
|
||||||
|
error={true}
|
||||||
|
value=""
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label="Disabled State"
|
||||||
|
placeholder="Choose an option"
|
||||||
|
disabled={true}
|
||||||
|
value=""
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Input from "../app/components/Input";
|
import TextInput from "../app/components/TextInput";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Forms/Input",
|
title: "Forms/TextInput",
|
||||||
component: Input,
|
component: TextInput,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: "centered",
|
layout: "centered",
|
||||||
},
|
},
|
||||||
@@ -38,12 +38,12 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = (args) => <Input {...args} />;
|
const Template = (args) => <TextInput {...args} />;
|
||||||
|
|
||||||
// Default story
|
// Default story
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Default.args = {
|
Default.args = {
|
||||||
label: "Default Input",
|
label: "Default Text Input",
|
||||||
placeholder: "Enter text...",
|
placeholder: "Enter text...",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
labelVariant: "default",
|
labelVariant: "default",
|
||||||
@@ -53,7 +53,7 @@ Default.args = {
|
|||||||
// Size variants
|
// Size variants
|
||||||
export const Small = Template.bind({});
|
export const Small = Template.bind({});
|
||||||
Small.args = {
|
Small.args = {
|
||||||
label: "Small Input",
|
label: "Small Text Input",
|
||||||
placeholder: "Small size",
|
placeholder: "Small size",
|
||||||
size: "small",
|
size: "small",
|
||||||
labelVariant: "default",
|
labelVariant: "default",
|
||||||
@@ -62,7 +62,7 @@ Small.args = {
|
|||||||
|
|
||||||
export const Medium = Template.bind({});
|
export const Medium = Template.bind({});
|
||||||
Medium.args = {
|
Medium.args = {
|
||||||
label: "Medium Input",
|
label: "Medium Text Input",
|
||||||
placeholder: "Medium size",
|
placeholder: "Medium size",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
labelVariant: "default",
|
labelVariant: "default",
|
||||||
@@ -71,7 +71,7 @@ Medium.args = {
|
|||||||
|
|
||||||
export const Large = Template.bind({});
|
export const Large = Template.bind({});
|
||||||
Large.args = {
|
Large.args = {
|
||||||
label: "Large Input",
|
label: "Large Text Input",
|
||||||
placeholder: "Large size",
|
placeholder: "Large size",
|
||||||
size: "large",
|
size: "large",
|
||||||
labelVariant: "default",
|
labelVariant: "default",
|
||||||
@@ -151,7 +151,7 @@ export const Interactive = (args) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<TextInput
|
||||||
{...args}
|
{...args}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
@@ -161,7 +161,7 @@ export const Interactive = (args) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Interactive.args = {
|
Interactive.args = {
|
||||||
label: "Interactive Input",
|
label: "Interactive Text Input",
|
||||||
placeholder: "Type something...",
|
placeholder: "Type something...",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
labelVariant: "default",
|
labelVariant: "default",
|
||||||
@@ -174,7 +174,7 @@ export const AllSizes = () => (
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">Small Size</h3>
|
<h3 className="text-lg font-semibold mb-4">Small Size</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<TextInput
|
||||||
label="Small Default"
|
label="Small Default"
|
||||||
placeholder="Small with top label"
|
placeholder="Small with top label"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -186,13 +186,13 @@ export const AllSizes = () => (
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">Medium Size</h3>
|
<h3 className="text-lg font-semibold mb-4">Medium Size</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<TextInput
|
||||||
label="Medium Default"
|
label="Medium Default"
|
||||||
placeholder="Medium with top label"
|
placeholder="Medium with top label"
|
||||||
size="medium"
|
size="medium"
|
||||||
labelVariant="default"
|
labelVariant="default"
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Medium Horizontal"
|
label="Medium Horizontal"
|
||||||
placeholder="Medium with left label"
|
placeholder="Medium with left label"
|
||||||
size="medium"
|
size="medium"
|
||||||
@@ -204,13 +204,13 @@ export const AllSizes = () => (
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">Large Size</h3>
|
<h3 className="text-lg font-semibold mb-4">Large Size</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<TextInput
|
||||||
label="Large Default"
|
label="Large Default"
|
||||||
placeholder="Large with top label"
|
placeholder="Large with top label"
|
||||||
size="large"
|
size="large"
|
||||||
labelVariant="default"
|
labelVariant="default"
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Large Horizontal"
|
label="Large Horizontal"
|
||||||
placeholder="Large with left label"
|
placeholder="Large with left label"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -225,39 +225,39 @@ export const AllSizes = () => (
|
|||||||
export const AllStates = () => (
|
export const AllStates = () => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">Input States</h3>
|
<h3 className="text-lg font-semibold mb-4">Text Input States</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<TextInput
|
||||||
label="Default State"
|
label="Default State"
|
||||||
placeholder="Default input"
|
placeholder="Default input"
|
||||||
size="medium"
|
size="medium"
|
||||||
state="default"
|
state="default"
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Active State"
|
label="Active State"
|
||||||
placeholder="Active input"
|
placeholder="Active input"
|
||||||
size="medium"
|
size="medium"
|
||||||
state="active"
|
state="active"
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Hover State"
|
label="Hover State"
|
||||||
placeholder="Hover input"
|
placeholder="Hover input"
|
||||||
size="medium"
|
size="medium"
|
||||||
state="hover"
|
state="hover"
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Focus State"
|
label="Focus State"
|
||||||
placeholder="Focused input"
|
placeholder="Focused input"
|
||||||
size="medium"
|
size="medium"
|
||||||
state="focus"
|
state="focus"
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Error State"
|
label="Error State"
|
||||||
placeholder="Error input"
|
placeholder="Error input"
|
||||||
size="medium"
|
size="medium"
|
||||||
error={true}
|
error={true}
|
||||||
/>
|
/>
|
||||||
<Input
|
<TextInput
|
||||||
label="Disabled State"
|
label="Disabled State"
|
||||||
placeholder="Disabled input"
|
placeholder="Disabled input"
|
||||||
size="medium"
|
size="medium"
|
||||||
@@ -4,7 +4,7 @@ import { screen, fireEvent, waitFor } from "@testing-library/react";
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { renderWithProviders } from "../utils/test-utils";
|
import { renderWithProviders } from "../utils/test-utils";
|
||||||
import Create from "../../app/components/Create";
|
import Create from "../../app/components/Create";
|
||||||
import Input from "../../app/components/Input";
|
import TextInput from "../../app/components/TextInput";
|
||||||
|
|
||||||
type CreateProps = React.ComponentProps<typeof Create>;
|
type CreateProps = React.ComponentProps<typeof Create>;
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ describe("Create", () => {
|
|||||||
it("traps focus within create dialog", async () => {
|
it("traps focus within create dialog", async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<Create {...defaultProps}>
|
<Create {...defaultProps}>
|
||||||
<Input label="Test Input" />
|
<TextInput label="Test Input" />
|
||||||
</Create>,
|
</Create>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Select from "../../app/components/Select";
|
import SelectInput from "../../app/components/SelectInput";
|
||||||
import { componentTestSuite } from "../utils/componentTestSuite";
|
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||||
|
|
||||||
type SelectProps = React.ComponentProps<typeof Select>;
|
type SelectInputProps = React.ComponentProps<typeof SelectInput>;
|
||||||
|
|
||||||
componentTestSuite<SelectProps>({
|
componentTestSuite<SelectInputProps>({
|
||||||
component: Select,
|
component: SelectInput,
|
||||||
name: "Select",
|
name: "SelectInput",
|
||||||
props: {
|
props: {
|
||||||
label: "Test Select",
|
label: "Test Select Input",
|
||||||
placeholder: "Select an option",
|
placeholder: "Select an option",
|
||||||
options: [
|
options: [
|
||||||
{ value: "option1", label: "Option 1" },
|
{ value: "option1", label: "Option 1" },
|
||||||
{ value: "option2", label: "Option 2" },
|
{ value: "option2", label: "Option 2" },
|
||||||
],
|
],
|
||||||
} as SelectProps,
|
} as SelectInputProps,
|
||||||
requiredProps: ["options"],
|
requiredProps: ["options"],
|
||||||
optionalProps: {
|
optionalProps: {},
|
||||||
size: "medium",
|
|
||||||
},
|
|
||||||
primaryRole: "button",
|
primaryRole: "button",
|
||||||
testCases: {
|
testCases: {
|
||||||
renders: true,
|
renders: true,
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Input from "../../app/components/Input";
|
import TextInput from "../../app/components/TextInput";
|
||||||
import { componentTestSuite } from "../utils/componentTestSuite";
|
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||||
|
|
||||||
type InputProps = React.ComponentProps<typeof Input>;
|
type TextInputProps = React.ComponentProps<typeof TextInput>;
|
||||||
|
|
||||||
componentTestSuite<InputProps>({
|
componentTestSuite<TextInputProps>({
|
||||||
component: Input,
|
component: TextInput,
|
||||||
name: "Input",
|
name: "TextInput",
|
||||||
props: {
|
props: {
|
||||||
label: "Test input",
|
label: "Test text input",
|
||||||
} as InputProps,
|
} as TextInputProps,
|
||||||
requiredProps: ["label"],
|
requiredProps: ["label"],
|
||||||
optionalProps: {
|
optionalProps: {
|
||||||
placeholder: "Enter value",
|
placeholder: "Enter value",
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import NumberCard from "../../app/components/NumberCard";
|
||||||
|
|
||||||
|
describe("NumberCard Component", () => {
|
||||||
|
const defaultProps = {
|
||||||
|
number: 1,
|
||||||
|
text: "Test Card Text",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renders number card with all required information", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Test Card Text")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with different numbers", () => {
|
||||||
|
const { rerender } = render(<NumberCard {...defaultProps} number={42} />);
|
||||||
|
expect(screen.getByText("42")).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(<NumberCard {...defaultProps} number={999} />);
|
||||||
|
expect(screen.getByText("999")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with different text content", () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<NumberCard {...defaultProps} text="Different Text" />,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Different Text")).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(<NumberCard {...defaultProps} text="Another Text" />);
|
||||||
|
expect(screen.getByText("Another Text")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper responsive layout classes when size is not specified", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "lg:flex-col");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper responsive spacing when size is not specified", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass("p-5", "sm:p-8", "lg:p-8");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper responsive gap when size is not specified", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass("gap-4", "sm:gap-8", "lg:gap-[22px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper responsive height when size is not specified", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass("lg:h-[238px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper background and shadow", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"bg-[var(--color-surface-inverse-primary)]",
|
||||||
|
"shadow-lg",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper border radius", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass("rounded-[12px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders section number in correct position for responsive mode", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const numberElement = screen.getByText("1");
|
||||||
|
expect(numberElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders text content in correct position for responsive mode", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check that it's in a container with proper positioning
|
||||||
|
const textContainer = textElement.closest("div");
|
||||||
|
expect(textContainer).toHaveClass(
|
||||||
|
"sm:flex-1",
|
||||||
|
"lg:absolute",
|
||||||
|
"lg:bottom-8",
|
||||||
|
"lg:left-8",
|
||||||
|
"lg:right-16",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper font classes to text", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass("font-bricolage-grotesque");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper text sizing for responsive mode", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass(
|
||||||
|
"text-[24px]",
|
||||||
|
"sm:text-[24px]",
|
||||||
|
"lg:text-[24px]",
|
||||||
|
"xl:text-[32px]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies proper text color", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass("text-[#141414]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles long text content gracefully", () => {
|
||||||
|
const longText =
|
||||||
|
"This is a very long text that should wrap properly and not break the layout of the number card component";
|
||||||
|
render(<NumberCard {...defaultProps} text={longText} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(longText)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maintains proper responsive behavior when size is not specified", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
|
||||||
|
// Mobile first approach
|
||||||
|
expect(card).toHaveClass("flex-col", "gap-4", "p-5");
|
||||||
|
|
||||||
|
// Small breakpoint
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"sm:flex-row",
|
||||||
|
"sm:gap-8",
|
||||||
|
"sm:p-8",
|
||||||
|
"sm:items-center",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Large breakpoint
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"lg:flex-col",
|
||||||
|
"lg:gap-[22px]",
|
||||||
|
"lg:p-8",
|
||||||
|
"lg:items-start",
|
||||||
|
"lg:relative",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with proper flex layout", () => {
|
||||||
|
render(<NumberCard {...defaultProps} />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass("flex");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies Small size variant correctly", () => {
|
||||||
|
render(<NumberCard {...defaultProps} size="Small" />);
|
||||||
|
|
||||||
|
// For Small size, text is directly in card div (no wrapper), so use closest("div")
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div");
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"flex",
|
||||||
|
"flex-col",
|
||||||
|
"items-end",
|
||||||
|
"justify-center",
|
||||||
|
"gap-4",
|
||||||
|
"p-5",
|
||||||
|
"relative",
|
||||||
|
);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass("text-[24px]", "leading-[32px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies Medium size variant correctly", () => {
|
||||||
|
render(<NumberCard {...defaultProps} size="Medium" />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"flex",
|
||||||
|
"flex-row",
|
||||||
|
"items-center",
|
||||||
|
"gap-8",
|
||||||
|
"p-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass("text-[24px]", "leading-[24px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies Large size variant correctly", () => {
|
||||||
|
render(<NumberCard {...defaultProps} size="Large" />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"flex",
|
||||||
|
"flex-col",
|
||||||
|
"items-start",
|
||||||
|
"justify-end",
|
||||||
|
"gap-[22px]",
|
||||||
|
"h-[238px]",
|
||||||
|
"p-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass("text-[24px]", "leading-[24px]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies XLarge size variant correctly", () => {
|
||||||
|
render(<NumberCard {...defaultProps} size="XLarge" />);
|
||||||
|
|
||||||
|
const card = screen
|
||||||
|
.getByText("Test Card Text")
|
||||||
|
.closest("div").parentElement;
|
||||||
|
expect(card).toHaveClass(
|
||||||
|
"flex",
|
||||||
|
"flex-col",
|
||||||
|
"items-start",
|
||||||
|
"justify-end",
|
||||||
|
"gap-[22px]",
|
||||||
|
"h-[238px]",
|
||||||
|
"p-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const textElement = screen.getByText("Test Card Text");
|
||||||
|
expect(textElement).toHaveClass("text-[32px]", "leading-[32px]");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import NumberedCard from "../../app/components/NumberedCard";
|
|
||||||
|
|
||||||
describe("NumberedCard Component", () => {
|
|
||||||
const defaultProps = {
|
|
||||||
number: 1,
|
|
||||||
text: "Test Card Text",
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders numbered card with all required information", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
expect(screen.getByText("1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test Card Text")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders with different numbers", () => {
|
|
||||||
const { rerender } = render(<NumberedCard {...defaultProps} number={42} />);
|
|
||||||
expect(screen.getByText("42")).toBeInTheDocument();
|
|
||||||
|
|
||||||
rerender(<NumberedCard {...defaultProps} number={999} />);
|
|
||||||
expect(screen.getByText("999")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders with different text content", () => {
|
|
||||||
const { rerender } = render(
|
|
||||||
<NumberedCard {...defaultProps} text="Different Text" />,
|
|
||||||
);
|
|
||||||
expect(screen.getByText("Different Text")).toBeInTheDocument();
|
|
||||||
|
|
||||||
rerender(<NumberedCard {...defaultProps} text="Another Text" />);
|
|
||||||
expect(screen.getByText("Another Text")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper responsive layout classes", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "lg:flex-row");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper responsive spacing", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("p-5", "sm:p-8", "lg:p-8");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper responsive gap", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("gap-4", "sm:gap-8", "lg:gap-0");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper responsive height", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("lg:h-[238px]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper background and shadow", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass(
|
|
||||||
"bg-[var(--color-surface-inverse-primary)]",
|
|
||||||
"shadow-lg",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper border radius", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("rounded-[12px]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders section number in correct position", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const numberElement = screen.getByText("1");
|
|
||||||
expect(numberElement).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check that it's in a container with proper positioning
|
|
||||||
const numberContainer = numberElement.closest("div");
|
|
||||||
expect(numberContainer).toHaveClass(
|
|
||||||
"absolute",
|
|
||||||
"inset-0",
|
|
||||||
"flex",
|
|
||||||
"items-center",
|
|
||||||
"justify-center",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders text content in correct position", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const textElement = screen.getByText("Test Card Text");
|
|
||||||
expect(textElement).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check that it's in a container with proper positioning
|
|
||||||
const textContainer = textElement.closest("div");
|
|
||||||
expect(textContainer).toHaveClass(
|
|
||||||
"sm:flex-1",
|
|
||||||
"lg:absolute",
|
|
||||||
"lg:bottom-8",
|
|
||||||
"lg:left-8",
|
|
||||||
"lg:right-16",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper font classes to text", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const textElement = screen.getByText("Test Card Text");
|
|
||||||
expect(textElement).toHaveClass("font-bricolage-grotesque");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper text sizing", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const textElement = screen.getByText("Test Card Text");
|
|
||||||
expect(textElement).toHaveClass(
|
|
||||||
"text-[24px]",
|
|
||||||
"sm:text-[24px]",
|
|
||||||
"lg:text-[24px]",
|
|
||||||
"xl:text-[32px]",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper text color", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const textElement = screen.getByText("Test Card Text");
|
|
||||||
expect(textElement).toHaveClass("text-[#141414]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles long text content gracefully", () => {
|
|
||||||
const longText =
|
|
||||||
"This is a very long text that should wrap properly and not break the layout of the numbered card component";
|
|
||||||
render(<NumberedCard {...defaultProps} text={longText} />);
|
|
||||||
|
|
||||||
expect(screen.getByText(longText)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maintains proper responsive behavior", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
|
|
||||||
// Mobile first approach
|
|
||||||
expect(card).toHaveClass("flex-col", "gap-4", "p-5");
|
|
||||||
|
|
||||||
// Small breakpoint
|
|
||||||
expect(card).toHaveClass(
|
|
||||||
"sm:flex-row",
|
|
||||||
"sm:gap-8",
|
|
||||||
"sm:p-8",
|
|
||||||
"sm:items-center",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Large breakpoint
|
|
||||||
expect(card).toHaveClass(
|
|
||||||
"lg:flex-row",
|
|
||||||
"lg:gap-0",
|
|
||||||
"lg:p-8",
|
|
||||||
"lg:items-stretch",
|
|
||||||
"lg:relative",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders with proper flex layout", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("flex");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("applies proper items alignment", () => {
|
|
||||||
render(<NumberedCard {...defaultProps} />);
|
|
||||||
|
|
||||||
const card = screen
|
|
||||||
.getByText("Test Card Text")
|
|
||||||
.closest("div").parentElement;
|
|
||||||
expect(card).toHaveClass("sm:items-center", "lg:items-stretch");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -72,10 +72,10 @@ describe("NumberedCards Component", () => {
|
|||||||
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
|
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders NumberedCard components with correct props", () => {
|
test("renders NumberCard components with correct props", () => {
|
||||||
render(<NumberedCards title="Test" subtitle="Test" cards={mockCards} />);
|
render(<NumberedCards title="Test" subtitle="Test" cards={mockCards} />);
|
||||||
|
|
||||||
// Check that NumberedCard components receive correct props
|
// Check that NumberCard components receive correct props
|
||||||
expect(screen.getByText("1")).toBeInTheDocument(); // First card number
|
expect(screen.getByText("1")).toBeInTheDocument(); // First card number
|
||||||
expect(screen.getByText("2")).toBeInTheDocument(); // Second card number
|
expect(screen.getByText("2")).toBeInTheDocument(); // Second card number
|
||||||
expect(screen.getByText("3")).toBeInTheDocument(); // Third card number
|
expect(screen.getByText("3")).toBeInTheDocument(); // Third card number
|
||||||
|
|||||||
+4
-2
@@ -76,8 +76,10 @@ export default defineConfig({
|
|||||||
workerTimeout: 120000, // 2min for worker timeout
|
workerTimeout: 120000, // 2min for worker timeout
|
||||||
poolTimeout: 120000, // 2min for pool timeout
|
poolTimeout: 120000, // 2min for pool timeout
|
||||||
// Optimize dependencies
|
// Optimize dependencies
|
||||||
deps: {
|
server: {
|
||||||
inline: ["@testing-library/jest-dom"], // Inline testing library
|
deps: {
|
||||||
|
inline: ["@testing-library/jest-dom"], // Inline testing library
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user