Merge pull request 'Number Card and Form Component Updates' (#37) from adilallo/component/NumberedCardUpdate into main
Reviewed-on: #37
This commit was merged in pull request #37.
This commit is contained in:
+221
-599
@@ -1,29 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Tooltip from "../components/Tooltip";
|
||||
import Alert from "../components/Alert";
|
||||
import Button from "../components/Button";
|
||||
import Stepper from "../components/Stepper";
|
||||
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";
|
||||
import TextInput from "../components/TextInput";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import CheckboxGroup from "../components/CheckboxGroup";
|
||||
import RadioGroup from "../components/RadioGroup";
|
||||
|
||||
export default function ComponentsPreview() {
|
||||
const [alertVisible, setAlertVisible] = useState({
|
||||
default: true,
|
||||
positive: true,
|
||||
warning: true,
|
||||
danger: true,
|
||||
banner: true,
|
||||
});
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createStep, setCreateStep] = useState(1);
|
||||
const [policyName, setPolicyName] = useState("");
|
||||
const [defaultInputValue, setDefaultInputValue] = useState("");
|
||||
const [activeInputValue, setActiveInputValue] = useState("");
|
||||
const [errorInputValue, setErrorInputValue] = useState("");
|
||||
const [standardCheckbox, setStandardCheckbox] = useState(false);
|
||||
const [inverseCheckbox, setInverseCheckbox] = useState(false);
|
||||
const [checkboxGroupValues, setCheckboxGroupValues] = useState<string[]>([]);
|
||||
const [radioValue, setRadioValue] = useState("");
|
||||
const [inverseRadioValue, setInverseRadioValue] = useState("");
|
||||
|
||||
return (
|
||||
<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>
|
||||
</header>
|
||||
|
||||
{/* Button Section */}
|
||||
{/* Text Input 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)]">
|
||||
Button Component
|
||||
Text Input 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)]">
|
||||
All Variants
|
||||
States
|
||||
</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)]">
|
||||
<p className="font-inter text-[16px] leading-[24px] text-[var(--color-content-default-primary)]">
|
||||
Review your policy configuration before finalizing.
|
||||
</p>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-200,8px)] p-[var(--spacing-scale-016)]">
|
||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
|
||||
Policy details will appear here
|
||||
</p>
|
||||
<TextInput
|
||||
label="Default Text Input"
|
||||
placeholder="Enter text"
|
||||
value={defaultInputValue}
|
||||
onChange={(e) => setDefaultInputValue(e.target.value)}
|
||||
/>
|
||||
<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>
|
||||
</Create>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* IconCard 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)]">
|
||||
IconCard Component
|
||||
</h2>
|
||||
{/* Radio 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)]">
|
||||
Radio 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="flex flex-wrap gap-[var(--spacing-scale-024)]">
|
||||
<IconCard
|
||||
icon={
|
||||
<img
|
||||
src={getAssetPath("assets/Vector_WorkerCoop.svg")}
|
||||
alt=""
|
||||
className="w-[36px] h-[36px]"
|
||||
width="36"
|
||||
height="36"
|
||||
<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)]">
|
||||
<RadioGroup
|
||||
name="default-radio"
|
||||
value={radioValue}
|
||||
onChange={({ value }) => setRadioValue(value)}
|
||||
mode="standard"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
title="Worker's cooperatives"
|
||||
description="Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations."
|
||||
onClick={() => {
|
||||
// IconCard clicked handler
|
||||
}}
|
||||
/>
|
||||
<RadioGroup
|
||||
name="interactive-radio"
|
||||
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
|
||||
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>
|
||||
</section>
|
||||
|
||||
@@ -21,49 +21,59 @@ const CheckboxContainer = memo<CheckboxProps>(
|
||||
...props
|
||||
}) => {
|
||||
const isInverse = mode === "inverse";
|
||||
const isStandard = mode === "standard";
|
||||
|
||||
// Base tokens (rough placeholders leveraging existing CSS variables)
|
||||
const colorContent = isInverse
|
||||
? "var(--color-content-inverse-primary)"
|
||||
: "var(--color-content-default-primary)";
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const { id: checkboxId, labelId } = useComponentId("checkbox", id);
|
||||
|
||||
// Visual container depending on state
|
||||
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`;
|
||||
// Base box styles per Figma
|
||||
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> = {
|
||||
default: "",
|
||||
hover: "",
|
||||
focus: "",
|
||||
// Get box styles based on state and checked status per Figma designs
|
||||
const getBoxStyles = (): string => {
|
||||
// Standard mode styles
|
||||
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:
|
||||
// - Standard: background does not change on check; only checkmark appears
|
||||
// - Inverse: transparent background, checkmark appears on check
|
||||
const backgroundWhenChecked = isInverse
|
||||
? "var(--color-surface-default-transparent)"
|
||||
: "var(--color-surface-default-primary)";
|
||||
const combinedBoxStyles = getBoxStyles();
|
||||
|
||||
// Checkmark color per Figma
|
||||
const checkGlyphColor = checked
|
||||
? isInverse
|
||||
? "var(--color-content-inverse-primary)"
|
||||
: "var(--color-border-default-brand-primary)"
|
||||
? isStandard
|
||||
? "var(--color-content-default-brand-primary, #fefcc9)" // Light yellow/cream for standard mode
|
||||
: "var(--color-content-inverse-primary, #000000)" // Black for inverse mode
|
||||
: "transparent";
|
||||
const labelColor = colorContent;
|
||||
|
||||
const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`;
|
||||
|
||||
// Force visible outline for standard / default / unchecked
|
||||
// Outline classes instead of inline styles so hover can override
|
||||
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)]";
|
||||
// Label color
|
||||
const labelColor = isInverse
|
||||
? "var(--color-content-inverse-primary)"
|
||||
: "var(--color-content-default-primary)";
|
||||
|
||||
const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
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 = {
|
||||
role: "checkbox" as const,
|
||||
"aria-checked": checked,
|
||||
@@ -107,10 +114,6 @@ const CheckboxContainer = memo<CheckboxProps>(
|
||||
value={value}
|
||||
className={className}
|
||||
combinedBoxStyles={combinedBoxStyles}
|
||||
defaultOutlineClass={defaultOutlineClass}
|
||||
conditionalHoverOutlineClass={conditionalHoverOutlineClass}
|
||||
conditionalFocusClass={conditionalFocusClass}
|
||||
backgroundWhenChecked={backgroundWhenChecked}
|
||||
checkGlyphColor={checkGlyphColor}
|
||||
labelColor={labelColor}
|
||||
accessibilityProps={accessibilityProps}
|
||||
|
||||
@@ -27,10 +27,6 @@ export interface CheckboxViewProps {
|
||||
value?: string;
|
||||
className: string;
|
||||
combinedBoxStyles: string;
|
||||
defaultOutlineClass: string;
|
||||
conditionalHoverOutlineClass: string;
|
||||
conditionalFocusClass: string;
|
||||
backgroundWhenChecked: string;
|
||||
checkGlyphColor: string;
|
||||
labelColor: string;
|
||||
accessibilityProps: React.HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
@@ -9,10 +9,6 @@ export function CheckboxView({
|
||||
value,
|
||||
className,
|
||||
combinedBoxStyles,
|
||||
defaultOutlineClass,
|
||||
conditionalHoverOutlineClass,
|
||||
conditionalFocusClass,
|
||||
backgroundWhenChecked,
|
||||
checkGlyphColor,
|
||||
labelColor,
|
||||
accessibilityProps,
|
||||
@@ -30,18 +26,16 @@ export function CheckboxView({
|
||||
{...accessibilityProps}
|
||||
onClick={onToggle}
|
||||
onKeyDown={onKeyDown}
|
||||
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`}
|
||||
style={{
|
||||
backgroundColor: backgroundWhenChecked,
|
||||
}}
|
||||
className={`${combinedBoxStyles} p-[4px] ${disabled ? "" : "cursor-pointer"}`}
|
||||
>
|
||||
{/* Simple check glyph */}
|
||||
{/* Checkmark SVG per Figma - 16px size */}
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 12 12"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className="block"
|
||||
>
|
||||
<polyline
|
||||
points="2.5 6 5 8.5 10 3.5"
|
||||
@@ -63,7 +57,7 @@ export function CheckboxView({
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
{/* Hidden native input for form compatibility (optional for now) */}
|
||||
{/* Hidden native input for form compatibility */}
|
||||
<input
|
||||
type="checkbox"
|
||||
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 SectionHeader from "../SectionHeader";
|
||||
import NumberedCard from "../NumberedCard";
|
||||
import NumberCard from "../NumberCard";
|
||||
import Button from "../Button";
|
||||
import type { NumberedCardsViewProps } from "./NumberedCards.types";
|
||||
|
||||
@@ -35,7 +35,7 @@ function NumberedCardsView({
|
||||
{/* Cards Container */}
|
||||
<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) => (
|
||||
<NumberedCard
|
||||
<NumberCard
|
||||
key={index}
|
||||
number={index + 1}
|
||||
text={card.text}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { RadioButtonProps } from "./RadioButton.types";
|
||||
const RadioButtonContainer = ({
|
||||
checked = false,
|
||||
mode = "standard",
|
||||
state = "default",
|
||||
state = "default", // This state prop is now only for static display in Storybook/Preview
|
||||
disabled = false,
|
||||
label,
|
||||
onChange,
|
||||
@@ -16,55 +16,75 @@ const RadioButtonContainer = ({
|
||||
value,
|
||||
ariaLabel,
|
||||
className = "",
|
||||
...props
|
||||
}: RadioButtonProps) => {
|
||||
const isInverse = mode === "inverse";
|
||||
const isStandard = mode === "standard";
|
||||
|
||||
// Base tokens (using same design tokens as Checkbox)
|
||||
const colorContent = isInverse
|
||||
? "var(--color-content-inverse-primary)"
|
||||
: "var(--color-content-default-primary)";
|
||||
// Base box styles per Figma - 24px size, circular
|
||||
const baseBox = `
|
||||
flex
|
||||
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
|
||||
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`;
|
||||
// Get box styles based on mode and checked status per Figma designs
|
||||
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> = {
|
||||
default: "",
|
||||
hover: "",
|
||||
focus: "",
|
||||
// Inverse mode styles
|
||||
if (isInverse) {
|
||||
// Default state: white border (or brand primary when checked), transparent background
|
||||
// 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:
|
||||
// - 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)";
|
||||
const combinedBoxStyles = getBoxStyles();
|
||||
|
||||
// Dot color for selected state
|
||||
const dotColor = checked
|
||||
? isInverse
|
||||
? "var(--color-content-inverse-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)]";
|
||||
// Label color
|
||||
const labelColor = isInverse
|
||||
? "var(--color-content-inverse-primary)"
|
||||
: "var(--color-content-default-primary)";
|
||||
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const generatedId = useId();
|
||||
@@ -72,11 +92,13 @@ const RadioButtonContainer = ({
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(_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 });
|
||||
}
|
||||
},
|
||||
[disabled, onChange, checked, value],
|
||||
[disabled, onChange, value],
|
||||
);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
@@ -91,7 +113,7 @@ const RadioButtonContainer = ({
|
||||
radioId={radioId}
|
||||
checked={checked}
|
||||
mode={mode}
|
||||
state={state}
|
||||
state={state} // Passed for static display in Storybook/Preview
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
name={name}
|
||||
@@ -99,15 +121,9 @@ const RadioButtonContainer = ({
|
||||
ariaLabel={ariaLabel}
|
||||
className={className}
|
||||
combinedBoxStyles={combinedBoxStyles}
|
||||
defaultOutlineClass={defaultOutlineClass}
|
||||
conditionalHoverOutlineClass={conditionalHoverOutlineClass}
|
||||
conditionalFocusClass={conditionalFocusClass}
|
||||
backgroundWhenChecked={backgroundWhenChecked}
|
||||
dotColor={dotColor}
|
||||
labelColor={labelColor}
|
||||
onToggle={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,11 +24,6 @@ export interface RadioButtonViewProps {
|
||||
ariaLabel?: string;
|
||||
className: string;
|
||||
combinedBoxStyles: string;
|
||||
defaultOutlineClass: string;
|
||||
conditionalHoverOutlineClass: string;
|
||||
conditionalFocusClass: string;
|
||||
backgroundWhenChecked: string;
|
||||
dotColor: string;
|
||||
labelColor: string;
|
||||
onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void;
|
||||
onKeyDown: (_e: React.KeyboardEvent<HTMLSpanElement>) => void;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { RadioButtonViewProps } from "./RadioButton.types";
|
||||
export function RadioButtonView({
|
||||
radioId,
|
||||
checked,
|
||||
mode,
|
||||
disabled,
|
||||
label,
|
||||
name,
|
||||
@@ -10,15 +11,9 @@ export function RadioButtonView({
|
||||
ariaLabel,
|
||||
className,
|
||||
combinedBoxStyles,
|
||||
defaultOutlineClass,
|
||||
conditionalHoverOutlineClass,
|
||||
conditionalFocusClass,
|
||||
backgroundWhenChecked,
|
||||
dotColor,
|
||||
labelColor,
|
||||
onToggle,
|
||||
onKeyDown,
|
||||
...props
|
||||
}: RadioButtonViewProps) {
|
||||
return (
|
||||
<label
|
||||
@@ -30,25 +25,25 @@ export function RadioButtonView({
|
||||
>
|
||||
<span
|
||||
onKeyDown={onKeyDown}
|
||||
className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`}
|
||||
style={{
|
||||
backgroundColor: backgroundWhenChecked,
|
||||
}}
|
||||
tabIndex={0}
|
||||
className={`group ${combinedBoxStyles} ${disabled ? "" : "cursor-pointer"}`}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
role="radio"
|
||||
aria-checked={checked}
|
||||
{...(disabled && { "aria-disabled": true })}
|
||||
{...(ariaLabel && { "aria-label": ariaLabel })}
|
||||
{...(label && !ariaLabel && { "aria-labelledby": `${radioId}-label` })}
|
||||
id={radioId}
|
||||
{...props}
|
||||
>
|
||||
{/* Radio dot */}
|
||||
{/* Radio dot - 16px size per Figma */}
|
||||
{/* Selected hover state: darker dot color (#333000) per Figma */}
|
||||
<div
|
||||
className="w-[16px] h-[16px] rounded-full transition-all duration-200"
|
||||
style={{
|
||||
backgroundColor: dotColor,
|
||||
}}
|
||||
className={`w-[16px] h-[16px] rounded-full transition-all duration-200 ${
|
||||
checked && mode === "standard"
|
||||
? "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>
|
||||
{label && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface RadioOption {
|
||||
value: string;
|
||||
label: string;
|
||||
subtext?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,52 @@ export function RadioGroupView({
|
||||
{options.map((option) => {
|
||||
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 (
|
||||
<RadioButton
|
||||
key={option.value}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { getAssetPath } from "../../lib/assetUtils";
|
||||
|
||||
interface SectionNumberProps {
|
||||
number: number;
|
||||
@@ -8,16 +9,19 @@ interface SectionNumberProps {
|
||||
|
||||
const SectionNumber = memo<SectionNumberProps>(({ number }) => {
|
||||
const getImageSrc = (num: number): string => {
|
||||
switch (num) {
|
||||
case 1:
|
||||
return "/assets/SectionNumber_1.png";
|
||||
case 2:
|
||||
return "/assets/SectionNumber_2.png";
|
||||
case 3:
|
||||
return "/assets/SectionNumber_3.png";
|
||||
default:
|
||||
return "/assets/SectionNumber_1.png";
|
||||
}
|
||||
const assetPath = (() => {
|
||||
switch (num) {
|
||||
case 1:
|
||||
return "assets/SectionNumber_1.png";
|
||||
case 2:
|
||||
return "assets/SectionNumber_2.png";
|
||||
case 3:
|
||||
return "assets/SectionNumber_3.png";
|
||||
default:
|
||||
return "assets/SectionNumber_1.png";
|
||||
}
|
||||
})();
|
||||
return getAssetPath(assetPath);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface SelectProps {
|
||||
export interface SelectInputProps {
|
||||
id?: string;
|
||||
label?: string;
|
||||
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>,
|
||||
"size" | "onChange" | "onFocus" | "onBlur"
|
||||
> {
|
||||
size?: "small" | "medium" | "large";
|
||||
labelVariant?: "default" | "horizontal";
|
||||
state?: "default" | "active" | "hover" | "focus";
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
@@ -14,13 +12,12 @@ export interface InputProps extends Omit<
|
||||
onFocus?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
className?: string;
|
||||
showHelpIcon?: boolean;
|
||||
}
|
||||
|
||||
export interface InputViewProps {
|
||||
export interface TextInputViewProps {
|
||||
inputId: string;
|
||||
labelId: string;
|
||||
size: "small" | "medium" | "large";
|
||||
labelVariant: "default" | "horizontal";
|
||||
state: "default" | "active" | "hover" | "focus";
|
||||
disabled: boolean;
|
||||
error: boolean;
|
||||
@@ -37,4 +34,9 @@ export interface InputViewProps {
|
||||
handleChange: (_e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleFocus: (_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
|
||||
ICON_POINTER: "assets/Icon_Pointer.svg",
|
||||
|
||||
// Help icon
|
||||
ICON_HELP: "assets/Icon_Help.svg",
|
||||
} 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 {
|
||||
title: "Forms/Checkbox",
|
||||
component: Checkbox,
|
||||
@@ -137,8 +109,7 @@ export const Checked = {
|
||||
|
||||
export const Standard = {
|
||||
render: () => {
|
||||
const [unchecked, setUnchecked] = React.useState(false);
|
||||
const [checked, setChecked] = React.useState(true);
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -146,13 +117,7 @@ export const Standard = {
|
||||
<h3 className="text-white font-medium">Standard Mode</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Checkbox
|
||||
label="Unchecked"
|
||||
checked={unchecked}
|
||||
mode="standard"
|
||||
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Checked"
|
||||
label="Standard Checkbox"
|
||||
checked={checked}
|
||||
mode="standard"
|
||||
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||
@@ -162,13 +127,11 @@ export const Standard = {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: StandardInteraction.play,
|
||||
};
|
||||
|
||||
export const Inverse = {
|
||||
render: () => {
|
||||
const [unchecked, setUnchecked] = React.useState(false);
|
||||
const [checked, setChecked] = React.useState(true);
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -176,13 +139,7 @@ export const Inverse = {
|
||||
<h3 className="text-white font-medium">Inverse Mode</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Checkbox
|
||||
label="Unchecked"
|
||||
checked={unchecked}
|
||||
mode="inverse"
|
||||
onChange={({ checked: newChecked }) => setUnchecked(newChecked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Checked"
|
||||
label="Inverse Checkbox"
|
||||
checked={checked}
|
||||
mode="inverse"
|
||||
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||
@@ -192,5 +149,60 @@ export const Inverse = {
|
||||
</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 Input from "../app/components/Input";
|
||||
import TextInput from "../app/components/TextInput";
|
||||
|
||||
export default {
|
||||
title: "Components/Create",
|
||||
@@ -57,7 +57,7 @@ Default.args = {
|
||||
description: "You can also combine or add new approaches to the list",
|
||||
children: (
|
||||
<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)]">
|
||||
0/48
|
||||
</p>
|
||||
@@ -77,7 +77,7 @@ WithStepper.args = {
|
||||
description: "You can also combine or add new approaches to the list",
|
||||
children: (
|
||||
<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)]">
|
||||
0/48
|
||||
</p>
|
||||
@@ -99,7 +99,7 @@ Step2.args = {
|
||||
description: "You can also combine or add new approaches to the list",
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
<Input label="Label" placeholder="Enter text" value="" />
|
||||
<TextInput label="Label" placeholder="Enter text" value="" />
|
||||
</div>
|
||||
),
|
||||
showBackButton: true,
|
||||
@@ -155,7 +155,7 @@ NextButtonDisabled.args = {
|
||||
description: "You can also combine or add new approaches to the list",
|
||||
children: (
|
||||
<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)]">
|
||||
0/48
|
||||
</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: {
|
||||
description: {
|
||||
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 RadioButton from "../app/components/RadioButton";
|
||||
import { expect } from "@storybook/test";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
// Interaction functions for Storybook play functions
|
||||
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 = {
|
||||
export default {
|
||||
title: "Forms/RadioButton",
|
||||
component: RadioButton,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
backgrounds: {
|
||||
default: "dark",
|
||||
values: [{ name: "dark", value: "black" }],
|
||||
values: [
|
||||
{ name: "light", value: "#ffffff" },
|
||||
{ name: "dark", value: "#000000" },
|
||||
],
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
checked: { control: "boolean" },
|
||||
checked: {
|
||||
control: "boolean",
|
||||
description: "Whether the radio button is checked",
|
||||
},
|
||||
mode: {
|
||||
control: { type: "select" },
|
||||
control: "select",
|
||||
options: ["standard", "inverse"],
|
||||
description: "Visual mode of the radio button",
|
||||
},
|
||||
state: {
|
||||
control: { type: "select" },
|
||||
control: "select",
|
||||
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 = {
|
||||
args: {
|
||||
checked: false,
|
||||
mode: "standard",
|
||||
state: "default",
|
||||
disabled: false,
|
||||
label: "Default radio button",
|
||||
},
|
||||
play: DefaultInteraction.play,
|
||||
render: (args) => {
|
||||
const [checked, setChecked] = React.useState(args.checked);
|
||||
return (
|
||||
@@ -108,9 +65,9 @@ export const Checked = {
|
||||
checked: true,
|
||||
mode: "standard",
|
||||
state: "default",
|
||||
disabled: false,
|
||||
label: "Checked radio button",
|
||||
},
|
||||
play: CheckedInteraction.play,
|
||||
render: (args) => {
|
||||
const [checked, setChecked] = React.useState(args.checked);
|
||||
return (
|
||||
@@ -125,7 +82,7 @@ export const Checked = {
|
||||
|
||||
export const Standard = {
|
||||
render: () => {
|
||||
const [selectedValue, setSelectedValue] = React.useState("checked");
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -133,36 +90,21 @@ export const Standard = {
|
||||
<h3 className="text-white font-medium">Standard Mode</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<RadioButton
|
||||
label="Unchecked"
|
||||
checked={selectedValue === "unchecked"}
|
||||
name="standard-example"
|
||||
value="unchecked"
|
||||
label="Standard Radio Button"
|
||||
checked={checked}
|
||||
mode="standard"
|
||||
onChange={({ checked }) => {
|
||||
if (checked) setSelectedValue("unchecked");
|
||||
}}
|
||||
/>
|
||||
<RadioButton
|
||||
label="Checked"
|
||||
checked={selectedValue === "checked"}
|
||||
name="standard-example"
|
||||
value="checked"
|
||||
mode="standard"
|
||||
onChange={({ checked }) => {
|
||||
if (checked) setSelectedValue("checked");
|
||||
}}
|
||||
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: StandardInteraction.play,
|
||||
};
|
||||
|
||||
export const Inverse = {
|
||||
render: () => {
|
||||
const [selectedValue, setSelectedValue] = React.useState("checked");
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -170,29 +112,138 @@ export const Inverse = {
|
||||
<h3 className="text-white font-medium">Inverse Mode</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<RadioButton
|
||||
label="Unchecked"
|
||||
checked={selectedValue === "unchecked"}
|
||||
name="inverse-example"
|
||||
value="unchecked"
|
||||
label="Inverse Radio Button"
|
||||
checked={checked}
|
||||
mode="inverse"
|
||||
onChange={({ checked }) => {
|
||||
if (checked) setSelectedValue("unchecked");
|
||||
}}
|
||||
/>
|
||||
<RadioButton
|
||||
label="Checked"
|
||||
checked={selectedValue === "checked"}
|
||||
name="inverse-example"
|
||||
value="checked"
|
||||
mode="inverse"
|
||||
onChange={({ checked }) => {
|
||||
if (checked) setSelectedValue("checked");
|
||||
}}
|
||||
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
||||
/>
|
||||
</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 RadioGroup from "../app/components/RadioGroup";
|
||||
import { expect } from "@storybook/test";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
// Interaction functions for Storybook play functions
|
||||
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 = {
|
||||
export default {
|
||||
title: "Forms/RadioGroup",
|
||||
component: RadioGroup,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
backgrounds: {
|
||||
default: "dark",
|
||||
values: [{ name: "dark", value: "black" }],
|
||||
values: [
|
||||
{ name: "light", value: "#ffffff" },
|
||||
{ name: "dark", value: "#000000" },
|
||||
],
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
mode: {
|
||||
control: { type: "select" },
|
||||
control: "select",
|
||||
options: ["standard", "inverse"],
|
||||
description: "Visual mode of the radio group",
|
||||
},
|
||||
state: {
|
||||
control: { type: "select" },
|
||||
options: ["default", "hover", "focus"],
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
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 = {
|
||||
args: {
|
||||
mode: "standard",
|
||||
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);
|
||||
render: () => {
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
{...args}
|
||||
name="default-radio-group"
|
||||
value={value}
|
||||
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: () => {
|
||||
const [value, setValue] = React.useState("option2");
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-white font-medium">Standard Mode</h3>
|
||||
<RadioGroup
|
||||
name="standard-example"
|
||||
value={value}
|
||||
mode="standard"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
]}
|
||||
onChange={({ value: newValue }) => setValue(newValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RadioGroup
|
||||
name="subtext-radio-group"
|
||||
value={value}
|
||||
onChange={({ value: newValue }) => setValue(newValue)}
|
||||
mode="standard"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{
|
||||
value: "option2",
|
||||
label: "Option 2",
|
||||
subtext: "Lorem ipsum dolor sit amet consectetur",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: StandardInteraction.play,
|
||||
};
|
||||
|
||||
export const Inverse = {
|
||||
render: () => {
|
||||
const [value, setValue] = React.useState("option1");
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-white font-medium">Inverse Mode</h3>
|
||||
<RadioGroup
|
||||
name="inverse-example"
|
||||
value={value}
|
||||
mode="inverse"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
]}
|
||||
onChange={({ value: newValue }) => setValue(newValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RadioGroup
|
||||
name="inverse-radio-group"
|
||||
value={value}
|
||||
onChange={({ value: newValue }) => setValue(newValue)}
|
||||
mode="inverse"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: InverseInteraction.play,
|
||||
};
|
||||
|
||||
export const Interactive = {
|
||||
export const InverseWithSubtext = {
|
||||
render: () => {
|
||||
const [value, setValue] = React.useState("option1");
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-white font-medium">Interactive Example</h3>
|
||||
<p className="text-gray-400 text-sm">Selected: {value}</p>
|
||||
<RadioGroup
|
||||
name="interactive-example"
|
||||
value={value}
|
||||
mode="standard"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
]}
|
||||
onChange={({ value }) => setValue(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RadioGroup
|
||||
name="inverse-subtext-radio-group"
|
||||
value={value}
|
||||
onChange={({ value: newValue }) => setValue(newValue)}
|
||||
mode="inverse"
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{
|
||||
value: "option2",
|
||||
label: "Option 2",
|
||||
subtext: "Lorem ipsum dolor sit amet consectetur",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
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 Input from "../app/components/Input";
|
||||
import TextInput from "../app/components/TextInput";
|
||||
|
||||
export default {
|
||||
title: "Forms/Input",
|
||||
component: Input,
|
||||
title: "Forms/TextInput",
|
||||
component: TextInput,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
@@ -38,12 +38,12 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => <Input {...args} />;
|
||||
const Template = (args) => <TextInput {...args} />;
|
||||
|
||||
// Default story
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
label: "Default Input",
|
||||
label: "Default Text Input",
|
||||
placeholder: "Enter text...",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
@@ -53,7 +53,7 @@ Default.args = {
|
||||
// Size variants
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
label: "Small Input",
|
||||
label: "Small Text Input",
|
||||
placeholder: "Small size",
|
||||
size: "small",
|
||||
labelVariant: "default",
|
||||
@@ -62,7 +62,7 @@ Small.args = {
|
||||
|
||||
export const Medium = Template.bind({});
|
||||
Medium.args = {
|
||||
label: "Medium Input",
|
||||
label: "Medium Text Input",
|
||||
placeholder: "Medium size",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
@@ -71,7 +71,7 @@ Medium.args = {
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
label: "Large Input",
|
||||
label: "Large Text Input",
|
||||
placeholder: "Large size",
|
||||
size: "large",
|
||||
labelVariant: "default",
|
||||
@@ -151,7 +151,7 @@ export const Interactive = (args) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
<TextInput
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
@@ -161,7 +161,7 @@ export const Interactive = (args) => {
|
||||
);
|
||||
};
|
||||
Interactive.args = {
|
||||
label: "Interactive Input",
|
||||
label: "Interactive Text Input",
|
||||
placeholder: "Type something...",
|
||||
size: "medium",
|
||||
labelVariant: "default",
|
||||
@@ -174,7 +174,7 @@ export const AllSizes = () => (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Small Size</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
<TextInput
|
||||
label="Small Default"
|
||||
placeholder="Small with top label"
|
||||
size="small"
|
||||
@@ -186,13 +186,13 @@ export const AllSizes = () => (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Medium Size</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
<TextInput
|
||||
label="Medium Default"
|
||||
placeholder="Medium with top label"
|
||||
size="medium"
|
||||
labelVariant="default"
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Medium Horizontal"
|
||||
placeholder="Medium with left label"
|
||||
size="medium"
|
||||
@@ -204,13 +204,13 @@ export const AllSizes = () => (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Large Size</h3>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
<TextInput
|
||||
label="Large Default"
|
||||
placeholder="Large with top label"
|
||||
size="large"
|
||||
labelVariant="default"
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Large Horizontal"
|
||||
placeholder="Large with left label"
|
||||
size="large"
|
||||
@@ -225,39 +225,39 @@ export const AllSizes = () => (
|
||||
export const AllStates = () => (
|
||||
<div className="space-y-6">
|
||||
<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">
|
||||
<Input
|
||||
<TextInput
|
||||
label="Default State"
|
||||
placeholder="Default input"
|
||||
size="medium"
|
||||
state="default"
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Active State"
|
||||
placeholder="Active input"
|
||||
size="medium"
|
||||
state="active"
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Hover State"
|
||||
placeholder="Hover input"
|
||||
size="medium"
|
||||
state="hover"
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Focus State"
|
||||
placeholder="Focused input"
|
||||
size="medium"
|
||||
state="focus"
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Error State"
|
||||
placeholder="Error input"
|
||||
size="medium"
|
||||
error={true}
|
||||
/>
|
||||
<Input
|
||||
<TextInput
|
||||
label="Disabled State"
|
||||
placeholder="Disabled input"
|
||||
size="medium"
|
||||
@@ -4,7 +4,7 @@ import { screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { renderWithProviders } from "../utils/test-utils";
|
||||
import Create from "../../app/components/Create";
|
||||
import Input from "../../app/components/Input";
|
||||
import TextInput from "../../app/components/TextInput";
|
||||
|
||||
type CreateProps = React.ComponentProps<typeof Create>;
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("Create", () => {
|
||||
it("traps focus within create dialog", async () => {
|
||||
renderWithProviders(
|
||||
<Create {...defaultProps}>
|
||||
<Input label="Test Input" />
|
||||
<TextInput label="Test Input" />
|
||||
</Create>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import React from "react";
|
||||
import Select from "../../app/components/Select";
|
||||
import SelectInput from "../../app/components/SelectInput";
|
||||
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||
|
||||
type SelectProps = React.ComponentProps<typeof Select>;
|
||||
type SelectInputProps = React.ComponentProps<typeof SelectInput>;
|
||||
|
||||
componentTestSuite<SelectProps>({
|
||||
component: Select,
|
||||
name: "Select",
|
||||
componentTestSuite<SelectInputProps>({
|
||||
component: SelectInput,
|
||||
name: "SelectInput",
|
||||
props: {
|
||||
label: "Test Select",
|
||||
label: "Test Select Input",
|
||||
placeholder: "Select an option",
|
||||
options: [
|
||||
{ value: "option1", label: "Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
],
|
||||
} as SelectProps,
|
||||
} as SelectInputProps,
|
||||
requiredProps: ["options"],
|
||||
optionalProps: {
|
||||
size: "medium",
|
||||
},
|
||||
optionalProps: {},
|
||||
primaryRole: "button",
|
||||
testCases: {
|
||||
renders: true,
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from "react";
|
||||
import Input from "../../app/components/Input";
|
||||
import TextInput from "../../app/components/TextInput";
|
||||
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||
|
||||
type InputProps = React.ComponentProps<typeof Input>;
|
||||
type TextInputProps = React.ComponentProps<typeof TextInput>;
|
||||
|
||||
componentTestSuite<InputProps>({
|
||||
component: Input,
|
||||
name: "Input",
|
||||
componentTestSuite<TextInputProps>({
|
||||
component: TextInput,
|
||||
name: "TextInput",
|
||||
props: {
|
||||
label: "Test input",
|
||||
} as InputProps,
|
||||
label: "Test text input",
|
||||
} as TextInputProps,
|
||||
requiredProps: ["label"],
|
||||
optionalProps: {
|
||||
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();
|
||||
});
|
||||
|
||||
test("renders NumberedCard components with correct props", () => {
|
||||
test("renders NumberCard components with correct props", () => {
|
||||
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("2")).toBeInTheDocument(); // Second 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
|
||||
poolTimeout: 120000, // 2min for pool timeout
|
||||
// Optimize dependencies
|
||||
deps: {
|
||||
inline: ["@testing-library/jest-dom"], // Inline testing library
|
||||
server: {
|
||||
deps: {
|
||||
inline: ["@testing-library/jest-dom"], // Inline testing library
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user