Start organizational migration
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useComponentId } from "../../../hooks";
|
||||
import { CheckboxView } from "./Checkbox.view";
|
||||
import type { CheckboxProps } from "./Checkbox.types";
|
||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
||||
|
||||
const CheckboxContainer = memo<CheckboxProps>(
|
||||
({
|
||||
checked = false,
|
||||
mode: modeProp = "standard",
|
||||
state: stateProp = "default",
|
||||
disabled = false,
|
||||
label,
|
||||
className = "",
|
||||
onChange,
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
ariaLabel,
|
||||
...props
|
||||
}) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
const state = normalizeState(stateProp);
|
||||
|
||||
const isInverse = mode === "inverse";
|
||||
const isStandard = mode === "standard";
|
||||
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const { id: checkboxId, labelId } = useComponentId("checkbox", id);
|
||||
|
||||
// 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, " ");
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
const combinedBoxStyles = getBoxStyles();
|
||||
|
||||
// Checkmark color per Figma
|
||||
const checkGlyphColor = checked
|
||||
? 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";
|
||||
|
||||
// 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;
|
||||
onChange?.({
|
||||
checked: !checked,
|
||||
value,
|
||||
event: e,
|
||||
});
|
||||
};
|
||||
|
||||
const accessibilityProps = {
|
||||
role: "checkbox" as const,
|
||||
"aria-checked": checked,
|
||||
...(disabled && { "aria-disabled": true, tabIndex: -1 }),
|
||||
...(!disabled && { tabIndex: 0 }),
|
||||
...(ariaLabel && { "aria-label": ariaLabel }),
|
||||
...(label && !ariaLabel && { "aria-labelledby": labelId }),
|
||||
id: checkboxId,
|
||||
...props,
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleToggle(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CheckboxView
|
||||
labelId={labelId}
|
||||
checked={checked}
|
||||
mode={mode}
|
||||
state={state}
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
name={name}
|
||||
value={value}
|
||||
className={className}
|
||||
combinedBoxStyles={combinedBoxStyles}
|
||||
checkGlyphColor={checkGlyphColor}
|
||||
labelColor={labelColor}
|
||||
accessibilityProps={accessibilityProps}
|
||||
onToggle={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CheckboxContainer.displayName = "Checkbox";
|
||||
|
||||
export default CheckboxContainer;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
/**
|
||||
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
mode?: ModeValue;
|
||||
/**
|
||||
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
state?: StateValue;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
className?: string;
|
||||
onChange?: (_data: {
|
||||
checked: boolean;
|
||||
value?: string;
|
||||
event: React.MouseEvent | React.KeyboardEvent;
|
||||
}) => void;
|
||||
id?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface CheckboxViewProps {
|
||||
labelId: string;
|
||||
checked: boolean;
|
||||
mode: "standard" | "inverse";
|
||||
state: "default" | "hover" | "focus" | "selected";
|
||||
disabled: boolean;
|
||||
label?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
className: string;
|
||||
combinedBoxStyles: string;
|
||||
checkGlyphColor: string;
|
||||
labelColor: string;
|
||||
accessibilityProps: React.HTMLAttributes<HTMLSpanElement>;
|
||||
onToggle: (_e: React.MouseEvent | React.KeyboardEvent) => void;
|
||||
onKeyDown: (_e: React.KeyboardEvent<HTMLSpanElement>) => void;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { CheckboxViewProps } from "./Checkbox.types";
|
||||
|
||||
export function CheckboxView({
|
||||
labelId,
|
||||
checked,
|
||||
disabled,
|
||||
label,
|
||||
name,
|
||||
value,
|
||||
className,
|
||||
combinedBoxStyles,
|
||||
checkGlyphColor,
|
||||
labelColor,
|
||||
accessibilityProps,
|
||||
onToggle,
|
||||
onKeyDown,
|
||||
}: CheckboxViewProps) {
|
||||
return (
|
||||
<label
|
||||
className={`inline-flex items-center gap-[8px] cursor-pointer select-none ${
|
||||
disabled ? "opacity-60 cursor-not-allowed" : ""
|
||||
} ${className}`}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<span
|
||||
{...accessibilityProps}
|
||||
onClick={onToggle}
|
||||
onKeyDown={onKeyDown}
|
||||
className={`${combinedBoxStyles} p-[4px] ${disabled ? "" : "cursor-pointer"}`}
|
||||
>
|
||||
{/* Checkmark SVG per Figma - 16px size */}
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 12 12"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className="block"
|
||||
>
|
||||
<polyline
|
||||
points="2.5 6 5 8.5 10 3.5"
|
||||
stroke={checkGlyphColor}
|
||||
strokeWidth="1.25"
|
||||
fill="none"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
{label && (
|
||||
<span
|
||||
id={labelId}
|
||||
className="font-inter text-[14px] leading-[18px]"
|
||||
style={{ color: labelColor }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
{/* Hidden native input for form compatibility */}
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={() => {}}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="sr-only"
|
||||
readOnly
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Checkbox.container";
|
||||
export type { CheckboxProps } from "./Checkbox.types";
|
||||
Reference in New Issue
Block a user