Number Card and Form Component Updates #37

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