diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx index 4d8e60c..3635324 100644 --- a/app/components-preview/page.tsx +++ b/app/components-preview/page.tsx @@ -1,29 +1,20 @@ "use client"; import { useState } from "react"; -import Tooltip from "../components/Tooltip"; -import Alert from "../components/Alert"; -import Button from "../components/Button"; -import Stepper from "../components/Stepper"; -import Progress from "../components/Progress"; -import Create from "../components/Create"; -import Input from "../components/Input"; -import InputWithCounter from "../components/InputWithCounter"; -import IconCard from "../components/IconCard"; -import { getAssetPath } from "../../lib/assetUtils"; +import TextInput from "../components/TextInput"; +import Checkbox from "../components/Checkbox"; +import CheckboxGroup from "../components/CheckboxGroup"; +import RadioGroup from "../components/RadioGroup"; export default function ComponentsPreview() { - const [alertVisible, setAlertVisible] = useState({ - default: true, - positive: true, - warning: true, - danger: true, - banner: true, - }); - - const [createOpen, setCreateOpen] = useState(false); - const [createStep, setCreateStep] = useState(1); - const [policyName, setPolicyName] = useState(""); + const [defaultInputValue, setDefaultInputValue] = useState(""); + const [activeInputValue, setActiveInputValue] = useState(""); + const [errorInputValue, setErrorInputValue] = useState(""); + const [standardCheckbox, setStandardCheckbox] = useState(false); + const [inverseCheckbox, setInverseCheckbox] = useState(false); + const [checkboxGroupValues, setCheckboxGroupValues] = useState([]); + const [radioValue, setRadioValue] = useState(""); + const [inverseRadioValue, setInverseRadioValue] = useState(""); return (
@@ -37,600 +28,231 @@ export default function ComponentsPreview() {

- {/* Button Section */} + {/* Text Input Section */}

- Button Component + Text Input Component

- All Variants + States

-
- - - - - - - - -
-
- -
-

- All Sizes - Danger Variant -

-
- - - - - -
-
- -
-

- All Sizes - Danger Inverse Variant -

-
- - - - - -
-
- -
-

- All Sizes - Ghost Variant -

-
- - - - - -
-
- -
-

- All Sizes - Ghost Inverse Variant -

-
- - - - - -
-
- -
-

- States - Danger Variant -

-
- - -
-
- -
-

- States - Danger Inverse Variant -

-
- - -
-
- -
-

- States - Ghost Variant -

-
- - -
-
- -
-

- States - Ghost Inverse Variant -

-
- - -
-
-
-
-
- - {/* Tooltip Section */} -
-

- Tooltip Component -

- -
-
- - - - - - - - - - - - - - - -
-
-
- - {/* Alert Section */} -
-

- Alert Component -

- -
- {/* Toast Alerts */} -
-

- Toast Alerts -

- - {alertVisible.default && ( - - setAlertVisible({ ...alertVisible, default: false }) - } - /> - )} - - {alertVisible.positive && ( - - setAlertVisible({ ...alertVisible, positive: false }) - } - /> - )} - - {alertVisible.warning && ( - - setAlertVisible({ ...alertVisible, warning: false }) - } - /> - )} - - {alertVisible.danger && ( - - setAlertVisible({ ...alertVisible, danger: false }) - } - /> - )} -
- - {/* Banner Alerts */} -
-

- Banner Alerts -

- - {alertVisible.banner && ( - - setAlertVisible({ ...alertVisible, banner: false }) - } - /> - )} - - - - - - -
-
-
- - {/* Stepper Section */} -
-

- Stepper Component -

- -
-
-
-

- Step 1 of 5 -

- -
-
-

- Step 2 of 5 -

- -
-
-

- Step 3 of 5 -

- -
-
-

- Step 4 of 5 -

- -
-
-

- Step 5 of 5 -

- -
-
-
-
- - {/* Progress Section */} -
-

- Progress Component -

- -
-
-
-

- Progress: 1-0 -

- -
-
-

- Progress: 1-1 -

- -
-
-

- Progress: 1-2 -

- -
-
-

- Progress: 1-3 -

- -
-
-

- Progress: 1-4 -

- -
-
-

- Progress: 1-5 -

- -
-
-

- Progress: 2-0 -

- -
-
-

- Progress: 2-1 -

- -
-
-

- Progress: 2-2 -

- -
-
-

- Progress: 3-0 -

- -
-
-

- Progress: 3-1 -

- -
-
-

- Progress: 3-2 -

- -
-
-
-
- - {/* Create Component Section */} -
-

- Create Component -

- -
-
- - -
-

- Step {createStep} of 3 -

- - -
-
-
- - 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} - > -
- {createStep === 1 && ( - - )} - {createStep === 2 && ( -
- -

- Select how conflicts should be resolved in your group. -

-
- )} - {createStep === 3 && (
-

- Review your policy configuration before finalizing. -

-
-

- Policy details will appear here -

+ setDefaultInputValue(e.target.value)} + /> + setActiveInputValue(e.target.value)} + /> + + setErrorInputValue(e.target.value)} + error + /> +
+
+ +
+
+ + + {/* Checkbox Section */} +
+

+ Checkbox Component +

+ +
+
+
+

+ Standard Mode +

+
+ setStandardCheckbox(checked)} + /> +
+
+
+

+ Inverse Mode +

+
+ setInverseCheckbox(checked)} + /> +
+
+
+
+
+ + {/* Checkbox Group Section */} +
+

+ Checkbox Group Component +

+ +
+
+
+

+ Standard Mode +

+
+ setCheckboxGroupValues(value)} + mode="standard" + options={[ + { value: "option1", label: "Checkbox label" }, + { + value: "option2", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> +
+
+
+

+ Inverse Mode +

+
+ setCheckboxGroupValues(value)} + mode="inverse" + options={[ + { value: "option3", label: "Checkbox label" }, + { + value: "option4", + label: "Checkbox label", + subtext: "Nunc sed hendrerit consequat.", + }, + ]} + /> +
- )} -
- -
+ + - {/* IconCard Component Section */} -
-

- IconCard Component -

+ {/* Radio Group Section */} +
+

+ Radio Group Component +

-
- +
+

+ Standard Mode +

+
+ setRadioValue(value)} + mode="standard" + options={[ + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, + { value: "option3", label: "Option 3" }, + ]} /> - } - title="Worker's cooperatives" - description="Employee-owned businesses often need to clarify how power is shared, decisions are made, and how processes operate within their organizations." - onClick={() => { - // IconCard clicked handler - }} - /> + setRadioValue(value)} + mode="standard" + options={[ + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, + { value: "option3", label: "Option 3" }, + ]} + /> + +
+
+
+

+ Inverse Mode +

+
+ setInverseRadioValue(value)} + mode="inverse" + options={[ + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, + { value: "option3", label: "Option 3" }, + ]} + /> + setInverseRadioValue(value)} + mode="inverse" + options={[ + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, + { value: "option3", label: "Option 3" }, + ]} + /> + +
+
diff --git a/app/components/Checkbox/Checkbox.container.tsx b/app/components/Checkbox/Checkbox.container.tsx index 1961614..a8a00b1 100644 --- a/app/components/Checkbox/Checkbox.container.tsx +++ b/app/components/Checkbox/Checkbox.container.tsx @@ -21,49 +21,59 @@ const CheckboxContainer = memo( ...props }) => { const isInverse = mode === "inverse"; + const isStandard = mode === "standard"; - // Base tokens (rough placeholders leveraging existing CSS variables) - const colorContent = isInverse - ? "var(--color-content-inverse-primary)" - : "var(--color-content-default-primary)"; + // Generate unique ID for accessibility if not provided + const { id: checkboxId, labelId } = useComponentId("checkbox", id); - // Visual container depending on state - const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`; + // Base box styles per Figma + const baseBox = ` + flex + items-center + justify-center + shrink-0 + w-[24px] + h-[24px] + rounded-[4px] + transition-all + duration-200 + ease-in-out + `.trim().replace(/\s+/g, " "); - const stateStyles: Record = { - default: "", - hover: "", - focus: "", + // Get box styles based on state and checked status per Figma designs + const getBoxStyles = (): string => { + // Standard mode styles + if (isStandard) { + // Default state: tertiary border, with hover and focus states via CSS + // Hover changes border to brand primary color + // Focus removes border and shows shadow (double ring: 2px white inner, 4px dark outer) + return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid border-[var(--color-border-default-tertiary,#464646)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] focus:border-transparent focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`; + } + + // Inverse mode styles per Figma + if (isInverse) { + // Inverse: transparent background, white border + // Hover changes border to brand primary color + // Focus shows shadow (2px dark inner, 4px white outer) - note: reversed from standard + return `${baseBox} bg-transparent border border-solid border-[var(--color-border-invert-primary,white)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`; + } + + return baseBox; }; - // Background behavior: - // - Standard: background does not change on check; only checkmark appears - // - Inverse: transparent background, checkmark appears on check - const backgroundWhenChecked = isInverse - ? "var(--color-surface-default-transparent)" - : "var(--color-surface-default-primary)"; + const combinedBoxStyles = getBoxStyles(); + + // Checkmark color per Figma const checkGlyphColor = checked - ? isInverse - ? "var(--color-content-inverse-primary)" - : "var(--color-border-default-brand-primary)" + ? isStandard + ? "var(--color-content-default-brand-primary, #fefcc9)" // Light yellow/cream for standard mode + : "var(--color-content-inverse-primary, #000000)" // Black for inverse mode : "transparent"; - const labelColor = colorContent; - const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`; - - // Force visible outline for standard / default / unchecked - // Outline classes instead of inline styles so hover can override - const defaultOutlineClass = isInverse - ? "outline outline-1 outline-[var(--color-border-inverse-primary)]" - : "outline outline-1 outline-[var(--color-border-default-tertiary)]"; - - // Apply brand outline only on actual :hover, and only when standard/unchecked - const conditionalHoverOutlineClass = - "hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]"; - - // Focus state for standard/unchecked with brand primary color and specific blur/spread - const conditionalFocusClass = - "focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]"; + // Label color + const labelColor = isInverse + ? "var(--color-content-inverse-primary)" + : "var(--color-content-default-primary)"; const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => { if (disabled) return; @@ -74,9 +84,6 @@ const CheckboxContainer = memo( }); }; - // Generate unique ID for accessibility if not provided - const { id: checkboxId, labelId } = useComponentId("checkbox", id); - const accessibilityProps = { role: "checkbox" as const, "aria-checked": checked, @@ -107,10 +114,6 @@ const CheckboxContainer = memo( value={value} className={className} combinedBoxStyles={combinedBoxStyles} - defaultOutlineClass={defaultOutlineClass} - conditionalHoverOutlineClass={conditionalHoverOutlineClass} - conditionalFocusClass={conditionalFocusClass} - backgroundWhenChecked={backgroundWhenChecked} checkGlyphColor={checkGlyphColor} labelColor={labelColor} accessibilityProps={accessibilityProps} diff --git a/app/components/Checkbox/Checkbox.types.ts b/app/components/Checkbox/Checkbox.types.ts index 2a0d28d..70ba736 100644 --- a/app/components/Checkbox/Checkbox.types.ts +++ b/app/components/Checkbox/Checkbox.types.ts @@ -27,10 +27,6 @@ export interface CheckboxViewProps { value?: string; className: string; combinedBoxStyles: string; - defaultOutlineClass: string; - conditionalHoverOutlineClass: string; - conditionalFocusClass: string; - backgroundWhenChecked: string; checkGlyphColor: string; labelColor: string; accessibilityProps: React.HTMLAttributes; diff --git a/app/components/Checkbox/Checkbox.view.tsx b/app/components/Checkbox/Checkbox.view.tsx index f4d4144..b6b2a14 100644 --- a/app/components/Checkbox/Checkbox.view.tsx +++ b/app/components/Checkbox/Checkbox.view.tsx @@ -9,10 +9,6 @@ export function CheckboxView({ value, className, combinedBoxStyles, - defaultOutlineClass, - conditionalHoverOutlineClass, - conditionalFocusClass, - backgroundWhenChecked, checkGlyphColor, labelColor, accessibilityProps, @@ -30,18 +26,16 @@ export function CheckboxView({ {...accessibilityProps} onClick={onToggle} onKeyDown={onKeyDown} - className={`${combinedBoxStyles} ${defaultOutlineClass} ${conditionalHoverOutlineClass} ${conditionalFocusClass} p-[var(--measures-spacing-004)]`} - style={{ - backgroundColor: backgroundWhenChecked, - }} + className={`${combinedBoxStyles} p-[4px] ${disabled ? "" : "cursor-pointer"}`} > - {/* Simple check glyph */} + {/* Checkmark SVG per Figma - 16px size */}
+ {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 ( +
+ { + onOptionChange(option.value, checked); + }} + /> +
+ + {option.label} + + + {option.subtext} + +
+
+ ); + } + + // If no subtext, use Checkbox's built-in label + return ( + { + onOptionChange(option.value, checked); + }} + /> + ); + })} +
+ ); +} diff --git a/app/components/CheckboxGroup/index.tsx b/app/components/CheckboxGroup/index.tsx new file mode 100644 index 0000000..eabbbee --- /dev/null +++ b/app/components/CheckboxGroup/index.tsx @@ -0,0 +1 @@ +export { default } from "./CheckboxGroup.container"; diff --git a/app/components/Input/Input.container.tsx b/app/components/Input/Input.container.tsx deleted file mode 100644 index 7804504..0000000 --- a/app/components/Input/Input.container.tsx +++ /dev/null @@ -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( - ( - { - 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(disabled, { - onChange, - onFocus, - onBlur, - }); - - return ( - - ); - }, -); - -InputContainer.displayName = "Input"; - -export default memo(InputContainer); diff --git a/app/components/Input/Input.view.tsx b/app/components/Input/Input.view.tsx deleted file mode 100644 index aae9d0a..0000000 --- a/app/components/Input/Input.view.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { forwardRef } from "react"; -import type { InputViewProps } from "./Input.types"; - -export const InputView = forwardRef( - ( - { - 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 ( -
- {label && ( - - )} -
- -
-
- ); - }, -); - -InputView.displayName = "InputView"; diff --git a/app/components/Input/index.tsx b/app/components/Input/index.tsx deleted file mode 100644 index c788c53..0000000 --- a/app/components/Input/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./Input.container"; -export type { InputProps } from "./Input.types"; diff --git a/app/components/NumberCard.tsx b/app/components/NumberCard.tsx new file mode 100644 index 0000000..5b4c1b4 --- /dev/null +++ b/app/components/NumberCard.tsx @@ -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(({ 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 ( +
+ {/* Section Number - Direct child for Small */} + + + {/* Card Content */} +

+ {text} +

+
+ ); + } + + return ( +
+ {/* Section Number */} +
+ +
+ + {/* Card Content */} +
+

+ {text} +

+
+
+ ); + } + + // Responsive breakpoints for backward compatibility (matches original behavior) + // Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl) + return ( +
+ {/* Section Number - Responsive positioning */} +
+ +
+ + {/* Card Content - Responsive positioning */} +
+

+ {text} +

+
+
+ ); +}); + +NumberCard.displayName = "NumberCard"; + +export default NumberCard; diff --git a/app/components/NumberedCard.tsx b/app/components/NumberedCard.tsx deleted file mode 100644 index 317fca2..0000000 --- a/app/components/NumberedCard.tsx +++ /dev/null @@ -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(({ number, text }) => { - return ( -
- {/* Section Number - Top right (lg breakpoint) */} -
- -
- - {/* Card Content - Bottom left (lg breakpoint) */} -
-

- {text} -

-
-
- ); -}); - -NumberedCard.displayName = "NumberedCard"; - -export default NumberedCard; diff --git a/app/components/NumberedCards/NumberedCards.view.tsx b/app/components/NumberedCards/NumberedCards.view.tsx index acc48fe..d77fb0b 100644 --- a/app/components/NumberedCards/NumberedCards.view.tsx +++ b/app/components/NumberedCards/NumberedCards.view.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "../../contexts/MessagesContext"; import SectionHeader from "../SectionHeader"; -import NumberedCard from "../NumberedCard"; +import NumberCard from "../NumberCard"; import Button from "../Button"; import type { NumberedCardsViewProps } from "./NumberedCards.types"; @@ -35,7 +35,7 @@ function NumberedCardsView({ {/* Cards Container */}
{cards.map((card, index) => ( - { const isInverse = mode === "inverse"; + const isStandard = mode === "standard"; - // Base tokens (using same design tokens as Checkbox) - const colorContent = isInverse - ? "var(--color-content-inverse-primary)" - : "var(--color-content-default-primary)"; + // Base box styles per Figma - 24px size, circular + const baseBox = ` + flex + items-center + justify-center + shrink-0 + w-[24px] + h-[24px] + rounded-full + transition-all + duration-200 + ease-in-out + p-[4px] + `.trim().replace(/\s+/g, " "); - // Visual container depending on state - const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`; + // Get box styles based on mode and checked status per Figma designs + const getBoxStyles = (): string => { + // Standard mode styles + if (isStandard) { + // Default state: tertiary border (or brand primary when checked), with hover and focus states via CSS + // Hover changes border to brand primary color + // Focus shows shadow (double ring: 2px white inner, 4px dark outer) + // When checked, border is brand primary (but changes to invert tertiary on focus) + const defaultBorder = checked + ? "border-[var(--color-border-default-brand-primary,#fdfaa8)]" + : "border-[var(--color-border-default-tertiary,#464646)]"; + + // When focused and checked, border should be invert tertiary (#2d2d2d) per Figma + const focusBorder = checked + ? "focus:border-[var(--color-content-invert-tertiary,#2d2d2d)]" + : "focus:border-[var(--color-border-default-tertiary,#464646)]"; + + return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid ${defaultBorder} hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`; + } - const stateStyles: Record = { - default: "", - hover: "", - focus: "", + // Inverse mode styles + if (isInverse) { + // Default state: white border (or brand primary when checked), transparent background + // Hover changes border to inverse brand primary color (#6c6701) for both selected and unselected + // Focus shows shadow (double ring: 2px dark inner, 4px white outer) + // When checked, border is brand primary (but changes to white on focus) + const defaultBorder = checked + ? "border-[var(--color-border-default-brand-primary,#fdfaa8)]" + : "border-[var(--color-border-invert-primary,white)]"; + + // Hover border: inverse brand primary for both selected and unselected per Figma + const hoverBorder = "hover:border-[var(--color-border-invert-brand-primary,#6c6701)]"; + + // Focus border: when focused and checked, border should be white per Figma + const focusBorder = checked + ? "focus:border-[var(--color-border-invert-primary,white)]" + : "focus:border-[var(--color-border-invert-primary,white)]"; + + return `${baseBox} bg-transparent border border-solid ${defaultBorder} ${hoverBorder} ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`; + } + + return baseBox; }; - // Background behavior: - // - Standard: background does not change on check; only dot appears - // - Inverse: transparent background, dot appears on check - const backgroundWhenChecked = isInverse - ? "var(--color-surface-default-transparent)" - : "var(--color-surface-default-primary)"; + const combinedBoxStyles = getBoxStyles(); - // Dot color for selected state - const dotColor = checked - ? isInverse - ? "var(--color-content-inverse-primary)" - : "var(--color-border-default-brand-primary)" - : "transparent"; - const labelColor = colorContent; - - const combinedBoxStyles = `${baseBox} ${stateStyles[state]}`; - - // Force visible outline for standard / default / unchecked - const defaultOutlineClass = isInverse - ? "outline outline-1 outline-[var(--color-border-inverse-primary)]" - : "outline outline-1 outline-[var(--color-border-default-tertiary)]"; - - // Apply brand outline only on actual :hover - // Standard mode uses default brand primary, inverse mode uses inverse brand primary - const conditionalHoverOutlineClass = isInverse - ? "hover:outline hover:outline-1 hover:outline-[var(--color-border-inverse-brand-primary)]" - : "hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]"; - - // Focus state for standard/unchecked with brand primary color and specific blur/spread - const conditionalFocusClass = - "focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]"; + // Label color + const labelColor = isInverse + ? "var(--color-content-inverse-primary)" + : "var(--color-content-default-primary)"; // Generate unique ID for accessibility if not provided const generatedId = useId(); @@ -72,11 +92,13 @@ const RadioButtonContainer = ({ const handleToggle = useCallback( (_e: React.MouseEvent | React.KeyboardEvent) => { - if (!disabled && onChange && !checked) { + if (!disabled && onChange) { + // Always call onChange when clicked, even if already checked + // The parent (RadioGroup) will handle the logic onChange({ checked: true, value }); } }, - [disabled, onChange, checked, value], + [disabled, onChange, value], ); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -91,7 +113,7 @@ const RadioButtonContainer = ({ radioId={radioId} checked={checked} mode={mode} - state={state} + state={state} // Passed for static display in Storybook/Preview disabled={disabled} label={label} name={name} @@ -99,15 +121,9 @@ const RadioButtonContainer = ({ ariaLabel={ariaLabel} className={className} combinedBoxStyles={combinedBoxStyles} - defaultOutlineClass={defaultOutlineClass} - conditionalHoverOutlineClass={conditionalHoverOutlineClass} - conditionalFocusClass={conditionalFocusClass} - backgroundWhenChecked={backgroundWhenChecked} - dotColor={dotColor} labelColor={labelColor} onToggle={handleToggle} onKeyDown={handleKeyDown} - {...props} /> ); }; diff --git a/app/components/RadioButton/RadioButton.types.ts b/app/components/RadioButton/RadioButton.types.ts index 0cfd80d..ed1c247 100644 --- a/app/components/RadioButton/RadioButton.types.ts +++ b/app/components/RadioButton/RadioButton.types.ts @@ -24,11 +24,6 @@ export interface RadioButtonViewProps { ariaLabel?: string; className: string; combinedBoxStyles: string; - defaultOutlineClass: string; - conditionalHoverOutlineClass: string; - conditionalFocusClass: string; - backgroundWhenChecked: string; - dotColor: string; labelColor: string; onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void; onKeyDown: (_e: React.KeyboardEvent) => void; diff --git a/app/components/RadioButton/RadioButton.view.tsx b/app/components/RadioButton/RadioButton.view.tsx index 0d73dca..80970fa 100644 --- a/app/components/RadioButton/RadioButton.view.tsx +++ b/app/components/RadioButton/RadioButton.view.tsx @@ -3,6 +3,7 @@ import type { RadioButtonViewProps } from "./RadioButton.types"; export function RadioButtonView({ radioId, checked, + mode, disabled, label, name, @@ -10,15 +11,9 @@ export function RadioButtonView({ ariaLabel, className, combinedBoxStyles, - defaultOutlineClass, - conditionalHoverOutlineClass, - conditionalFocusClass, - backgroundWhenChecked, - dotColor, labelColor, onToggle, onKeyDown, - ...props }: RadioButtonViewProps) { return (