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
6 changed files with 410 additions and 5 deletions
Showing only changes of commit 87a1e1d2a8 - Show all commits
+61 -5
View File
@@ -3,6 +3,7 @@
import { useState } from "react";
import TextInput from "../components/TextInput";
import Checkbox from "../components/Checkbox";
import CheckboxGroup from "../components/CheckboxGroup";
import RadioGroup from "../components/RadioGroup";
export default function ComponentsPreview() {
@@ -11,6 +12,7 @@ export default function ComponentsPreview() {
const [errorInputValue, setErrorInputValue] = useState("");
const [standardCheckbox, setStandardCheckbox] = useState(false);
const [inverseCheckbox, setInverseCheckbox] = useState(false);
const [checkboxGroupValues, setCheckboxGroupValues] = useState<string[]>([]);
const [radioValue, setRadioValue] = useState("");
return (
@@ -108,11 +110,65 @@ export default function ComponentsPreview() {
</div>
</section>
{/* Radio Group Section */}
<section className="space-y-[var(--spacing-scale-024)]">
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
Radio Group Component
</h2>
{/* 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>
</section>
{/* Radio Group Section */}
<section className="space-y-[var(--spacing-scale-024)]">
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
Radio Group Component
</h2>
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
<div className="space-y-[var(--spacing-scale-016)]">
@@ -0,0 +1,64 @@
"use client";
import { memo, useCallback, useId, useState, useEffect } 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
const [checkedValues, setCheckedValues] = useState<string[]>(value || []);
// Sync internal state with external value prop
useEffect(() => {
if (value !== undefined) {
setCheckedValues(value);
}
}, [value]);
const handleOptionChange = useCallback(
(optionValue: string, checked: boolean) => {
if (disabled) return;
const newCheckedValues = checked
? [...checkedValues, optionValue]
: checkedValues.filter((v) => v !== optionValue);
setCheckedValues(newCheckedValues);
if (onChange) {
onChange({ value: newCheckedValues });
}
},
[disabled, checkedValues, onChange],
);
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";
+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>
);
};