diff --git a/app/components/Checkbox.js b/app/components/Checkbox.js index b5ad828..70a7672 100644 --- a/app/components/Checkbox.js +++ b/app/components/Checkbox.js @@ -69,9 +69,9 @@ const Checkbox = memo( const conditionalHoverOutlineClass = "hover:outline hover:outline-1 hover:outline-[var(--color-border-default-brand-primary)]"; - // Focus state for standard/unchecked with utility info color and specific blur/spread + // 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-border-default-utility-info)]"; + "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) => { if (disabled) return; diff --git a/app/components/RadioButton.js b/app/components/RadioButton.js new file mode 100644 index 0000000..0bc51bc --- /dev/null +++ b/app/components/RadioButton.js @@ -0,0 +1,148 @@ +"use client"; + +import React, { memo, useCallback } from "react"; + +const RadioButton = ({ + checked = false, + mode = "standard", + state = "default", + disabled = false, + label, + onChange, + id, + name, + value, + ariaLabel, + className = "", + ...props +}) => { + const isInverse = mode === "inverse"; + + // Base tokens (using same design tokens as Checkbox) + const colorSurface = isInverse + ? "var(--color-surface-inverse-primary)" + : "var(--color-surface-default-primary)"; + const colorContent = isInverse + ? "var(--color-content-inverse-primary)" + : "var(--color-content-default-primary)"; + const colorBrand = isInverse + ? "var(--color-content-inverse-brand-primary)" + : "var(--color-content-default-brand-primary)"; + + // 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`; + + const stateStyles = { + default: "", + hover: "", + focus: "", + }; + + // 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)"; + + // 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)]"; + + // Generate unique ID for accessibility if not provided + const radioId = id || `radio-${Math.random().toString(36).substr(2, 9)}`; + + const handleToggle = useCallback( + (e) => { + if (!disabled && onChange && !checked) { + onChange({ checked: true, value }); + } + }, + [disabled, onChange, checked, value] + ); + + return ( +