diff --git a/.changeset/improved-captcha-look.md b/.changeset/improved-captcha-look.md new file mode 100644 index 0000000000..3406021169 --- /dev/null +++ b/.changeset/improved-captcha-look.md @@ -0,0 +1,8 @@ +--- +"@prosopo/procaptcha-common": patch +"@prosopo/procaptcha-puzzle": patch +"@prosopo/procaptcha-react": patch +"@prosopo/widget-skeleton": patch +--- + +Refresh the captcha widget appearance with a Material 3-inspired theme — updated surfaces, elevation, and restyled checkbox, button and puzzle canvas — across the widget-skeleton, procaptcha-common, procaptcha-react and procaptcha-puzzle packages. diff --git a/packages/procaptcha-common/src/reactComponents/Checkbox.tsx b/packages/procaptcha-common/src/reactComponents/Checkbox.tsx index 7e894c7c75..ae5da736dd 100644 --- a/packages/procaptcha-common/src/reactComponents/Checkbox.tsx +++ b/packages/procaptcha-common/src/reactComponents/Checkbox.tsx @@ -45,6 +45,16 @@ const checkboxBefore = css`{ } }`; +// In forced-colors mode (Windows High Contrast) backgrounds are overridden, so +// the custom-painted tick can disappear — fall back to the native control, +// which the OS draws in system colors. !important beats the inline styles. +const checkboxForcedColors = css` + @media (forced-colors: active) { + appearance: auto !important; + background-image: none !important; + } +`; + const baseStyle: CSSProperties = { width: "28px", height: "28px", @@ -86,10 +96,6 @@ export const Checkbox: FC = ({ error, loading, }: CheckboxProps) => { - const checkboxStyleBase: CSSProperties = { - ...baseStyle, - border: `1px solid ${theme.palette.background.contrastText}`, - }; const [hover, setHover] = useState(false); const ResponsiveLabel = styled.label` @@ -117,18 +123,33 @@ export const Checkbox: FC = ({ } `; - // biome-ignore lint/correctness/useExhaustiveDependencies: TODO fix const checkboxStyle: CSSProperties = useMemo(() => { + // White (token) tick painted directly onto the box so the checked state is + // identical in light and dark mode — the native control can't be themed. + const tickColor = encodeURIComponent(theme.palette.checkbox.tick); + const checkImage = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='${tickColor}' d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E")`; return { - ...checkboxStyleBase, - borderColor: hover - ? theme.palette.background.contrastText - : theme.palette.border, - appearance: checked ? "auto" : "none", + ...baseStyle, flex: 1, margin: "15px", minWidth: "28px", minHeight: "28px", + borderRadius: "8px", + borderWidth: "2px", + borderColor: checked + ? theme.palette.checkbox.fill + : hover + ? theme.palette.primary.main + : theme.palette.checkbox.border, + backgroundColor: checked + ? theme.palette.checkbox.fill + : theme.palette.surface, + backgroundImage: checked ? checkImage : "none", + backgroundRepeat: "no-repeat", + backgroundPosition: "center", + backgroundSize: "20px 20px", + transition: + "background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease", }; }, [hover, theme, checked]); const id = generateRandomId(); @@ -152,7 +173,7 @@ export const Checkbox: FC = ({ id={id} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} - css={checkboxBefore} + css={[checkboxBefore, checkboxForcedColors]} type={"checkbox"} aria-live={"assertive"} aria-label={labelText} diff --git a/packages/procaptcha-common/src/reactComponents/Reload.tsx b/packages/procaptcha-common/src/reactComponents/Reload.tsx index 8aa1e190cb..7a22f6f156 100644 --- a/packages/procaptcha-common/src/reactComponents/Reload.tsx +++ b/packages/procaptcha-common/src/reactComponents/Reload.tsx @@ -25,8 +25,8 @@ const buttonStyleBase = { paddingTop: "6px", paddingBottom: "6px", cursor: "pointer", - height: "39px", - width: "39px", + height: "42px", + width: "42px", borderRadius: "50%", display: "flex", }; @@ -41,26 +41,19 @@ export const ReloadButton: FC = ({ ); const [hover, setHover] = useState(false); const buttonStyle = useMemo(() => { - const baseStyle = { + return { ...buttonStyleBase, - backgroundColor: theme.palette.background.default, - color: hover - ? theme.palette.primary.contrastText - : theme.palette.background.contrastText, - border: `1px solid ${theme.palette.grey[500]}`, - borderRadius: "50%", - transition: "background-color 0.3s", - boxShadow: `0px 1px 3px 0px ${theme.palette.grey[500]}`, + // Material 3 tonal icon button; hover swaps to the state-layer fill so + // the feedback is visible in dark mode too (a brightness filter is not). + backgroundColor: hover + ? theme.palette.primaryContainer.hover + : theme.palette.primaryContainer.main, + color: theme.palette.primaryContainer.contrastText, + transition: "background-color 0.25s", justifyContent: "center", alignItems: "center", margin: "0 auto", }; - return { - ...baseStyle, - backgroundColor: hover - ? theme.palette.grey[700] - : theme.palette.background.default, - }; }, [hover, theme]); return (