From 4b9d8324f60db0af473bdd74b4c8a6b46c5ad054 Mon Sep 17 00:00:00 2001 From: Hugh Parry Date: Mon, 15 Jun 2026 15:42:04 +0100 Subject: [PATCH 1/5] feat(widget): Material 3 purple tonal theme for captcha UI Restyle the widget, image, frictionless (placeholder/PoW checkbox) and puzzle captchas into a unified Material 3 "tonal" key off the Prosopo brand purple, with a matching dark-mode scheme. - theme.ts: repurpose palette to the purple ramp + add M3 tokens (primaryContainer, surface/onSurface, muted, titleAccent, checkbox, overlay, tile, shape radii, elevation) for both light and dark - widget: M3 surface + elevation + 16px radius, purple two-tone spinner, custom-painted purple checkbox with tick (themes correctly in dark) - image: M3 card + tonal header, rounded tiles with purple selection overlay + check badge + scale-in, pill buttons, tonal reload - puzzle: theme-driven purple surface, piece, target ring and header Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/reactComponents/Checkbox.tsx | 32 +++-- .../src/reactComponents/Reload.tsx | 29 ++-- .../src/components/ProcaptchaWidget.tsx | 1 + .../src/components/PuzzleCanvas.tsx | 55 +++++--- .../src/components/Button.tsx | 42 +++--- .../src/components/CaptchaComponent.tsx | 25 ++-- .../src/components/CaptchaWidget.tsx | 26 +++- .../procaptcha-react/src/components/Modal.tsx | 2 +- packages/widget-skeleton/src/constants.ts | 2 +- .../widget-skeleton/src/elements/checkbox.ts | 4 +- .../widget-skeleton/src/elements/skeleton.ts | 10 +- packages/widget-skeleton/src/theme.ts | 127 +++++++++++++++--- 12 files changed, 250 insertions(+), 105 deletions(-) diff --git a/packages/procaptcha-common/src/reactComponents/Checkbox.tsx b/packages/procaptcha-common/src/reactComponents/Checkbox.tsx index 7e894c7c75..f2706d38b9 100644 --- a/packages/procaptcha-common/src/reactComponents/Checkbox.tsx +++ b/packages/procaptcha-common/src/reactComponents/Checkbox.tsx @@ -86,10 +86,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` @@ -119,16 +115,34 @@ 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 = theme.palette.checkbox.tick.replace("#", "%23"); + 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, + appearance: "none", flex: 1, margin: "15px", minWidth: "28px", minHeight: "28px", + borderRadius: "8px", + borderStyle: "solid", + 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(); diff --git a/packages/procaptcha-common/src/reactComponents/Reload.tsx b/packages/procaptcha-common/src/reactComponents/Reload.tsx index 8aa1e190cb..7c9d50d45c 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]}`, + // Material 3 tonal icon button. + backgroundColor: theme.palette.primaryContainer.main, + color: theme.palette.primaryContainer.contrastText, + border: "none", borderRadius: "50%", - transition: "background-color 0.3s", - boxShadow: `0px 1px 3px 0px ${theme.palette.grey[500]}`, + transition: "filter 0.25s, background-color 0.25s", + filter: hover ? "brightness(0.95)" : "none", justifyContent: "center", alignItems: "center", margin: "0 auto", }; - return { - ...baseStyle, - backgroundColor: hover - ? theme.palette.grey[700] - : theme.palette.background.default, - }; }, [hover, theme]); return (