Skip to content

feat: improved captcha look - #2798

Merged
HughParry merged 17 commits into
mainfrom
feat/improved-captcha-look
Aug 11, 2026
Merged

feat: improved captcha look#2798
HughParry merged 17 commits into
mainfrom
feat/improved-captcha-look

Conversation

@HughParry

Copy link
Copy Markdown
Contributor

CI Check Do Not Merge

🤖 Generated with Claude Code

HughParry and others added 3 commits June 15, 2026 15:42
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) <noreply@anthropic.com>
Follow-up to the Material 3 purple tonal redesign. Per design feedback the
widgets were too rounded ("everything is round") and drop shadows read as
dated, so:

- shape scale dialled back: card 28->12px, full-pill buttons 100->8px,
  widget 16->8px, header 18->8px, tiles 14->6px / 16->8px
- WIDGET_BORDER_RADIUS 16->8px, Modal 28->12px, puzzle surface 20->12px,
  custom checkbox 8->6px
- elevation tokens flattened to none in both themes (widget, widgetHover,
  card, buttonPrimary) so borders, not shadows, separate the surfaces

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/procaptcha-common/src/reactComponents/Checkbox.tsx Fixed
HughParry and others added 11 commits July 6, 2026 13:34
- buttons use minHeight so wrapped localized labels aren't clipped
- reload button hover uses a primaryContainer.hover state-layer token
  (visible in dark mode, unlike the brightness filter)
- checkbox falls back to the native control in forced-colors mode
- puzzle target/piece colors move into theme.palette.puzzle tokens,
  removing the hard-coded purples and isDark ternaries
- drop the dead all-none elevation tokens and the CSS wired to them,
  plus unused grey/purple/muted palette entries
- surface/onSurface/background/font colors derive from single constants
- reuse the selected predicate for the tile overlay; format all files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts the styling changes from fa6e170 (style(widget): rein in
roundedness and drop decorative shadows): shape scale back to the round
M3 values (card 28px, pill buttons, widget 16px, header 18px, tiles
14/16px), WIDGET_BORDER_RADIUS 16px, Modal 28px, puzzle surface 20px,
checkbox 8px, and the elevation tokens back to real shadows with their
widget/card/button wiring (re-added where the earlier cleanup had
removed the then-all-none tokens).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ changeset

Use encodeURIComponent for the SVG data-URI tick color instead of a
single-occurrence String.replace (CodeQL js/incomplete-sanitization).
Add changeset for the improved captcha look across the widget packages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the single tinted surface into Material 3 surface roles so the widget
sits on a white surface while the dialog uses surfaceContainerHigh, and drop
every drop shadow in favour of surface/outline separation.

- Remove drop shadows from the widget, dialog card, modal and puzzle; hover
  and drag feedback now use M3 state layers and outline rings.
- Add shared typography and stateLayer tokens; align the shape scale to M3.
- Buttons: label-large type, 40dp height, spec padding, state-layer hover in
  place of a brightness filter, no resting elevation on the filled variant.
- Add the missing focus indicators (3dp outline, 2dp offset, keyboard-only).
- Use the onSurfaceVariant role for secondary text instead of opacity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYpduC2rvsLYgYM5NqNHZC
…-look

# Conflicts:
#	packages/procaptcha-react/src/components/CaptchaComponent.tsx
The merge brought in main's new widget-skeleton, procaptcha-react and
procaptcha-puzzle test suites, which pin the palette this branch replaced:
a `grey` ramp that no longer exists, `#fff`/`#303030` surfaces, a shared
error red, and outlined buttons. Git merged them cleanly as text but they
neither compiled nor passed.

Each assertion is rewritten against the role it is really about rather than
re-pinned to a new hex, so the tests keep their original intent:

- theme: assert the M3 role pairings (background.default is
  surfaceContainerHigh, contrastText is onSurface) instead of literal
  greys, and that both modes share one shape/type/state-layer scale.
- elements: the on-page widget takes `surface`, and the checkbox spinner
  the outline + primary roles.
- button: M3 text and filled buttons are borderless pills whose hover is a
  state layer, not an opaque grey fill.
- puzzle: PuzzleCanvas now requires a `theme` prop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hPGTJ23KyDz2WuUgzz2pi
@HughParry

Copy link
Copy Markdown
Contributor Author
Screen.Recording.2026-08-11.at.12.41.00.mov

HughParry and others added 2 commits August 11, 2026 12:47
…-look

# Conflicts:
#	packages/procaptcha-react/src/components/Button.tsx
…okens

#2930 landed tests that pin which palette key each element interpolates.
Three of them named keys this branch deliberately moved away from:

- The widget shell reads `surface`, not `background.default`. Both still
  exist; under M3 `background.default` is the dialog container's role
  (surfaceContainerHigh), which is what separates a resting affordance from
  a modal container.
- The checkbox spinner is an M3 circular progress — a neutral `border`
  track with a `primary` active arc — where it used to draw the whole ring
  in `background.contrastText`.

Assertions follow the elements; no styling changed to satisfy a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hPGTJ23KyDz2WuUgzz2pi
@HughParry

Copy link
Copy Markdown
Contributor Author

What changed

Merged main and resolved the conflict. Below is the split you asked for — styling vs structural — with the honest answer that there are a few structural changes, all small, plus one I'd flag for a decision.

Markdown / docs: none 🎉

The only .md file touched is .changeset/improved-captcha-look.md, which is the changeset for this PR. No docs, READMEs or other markdown were restructured.


1. Styling changes (the bulk of the PR)

A Material 3 purple tonal theme. Values only — no API shape involved.

widget-skeleton/src/theme.ts — the substance of the PR:

  • Purple tonal palette replacing the previous blue/grey (primary.main #487DFA → purple ramp)
  • M3 surface roles: surface, surfaceContainerHigh, onSurface, onSurfaceVariant, primaryContainer
  • State-layer opacities (stateLayer.hover/focus/pressed) instead of shadow-based elevation
  • An M3 type scale slice (labelLarge, bodyMedium, titleMedium)
  • error.main #f44336#b3261e (M3 error role)
  • Elevation tokens set to none, kept as tokens so a shadowed scheme can be reinstated in one place

widget-skeleton/src/constants.tsWIDGET_BORDER_RADIUS 8px16px.

widget-skeleton/src/elements/skeleton.ts moves the shell onto surface/border and adds a shadowless hover state layer; checkbox.ts redraws the spinner as an M3 circular progress (neutral track, primary active arc).

ComponentsButton.tsx, Checkbox.tsx, Reload.tsx, CaptchaComponent.tsx, CaptchaWidget.tsx, Modal.tsx, PuzzleCanvas.tsx consume the new tokens. Button.tsx also gains an M3 focus indicator (3dp outline, 2dp offset) driven by :focus-visible.


2. Structural changes (four, each with a reason)

a. withAlpha(hex, opacity) — new exported helper in theme.ts
Five lines, pure. Needed because M3 state layers and translucent tokens are colour-plus-opacity, and the alternative is hardcoded rgba() literals that no longer track the palette. Picked up automatically by the existing export * from "./theme.js", so index.ts is untouched.

b. PuzzleCanvas gains a required theme: Theme prop (and ProcaptchaWidget passes it — the single +1 line)
Unavoidable: the canvas previously hardcoded its colours, and a component can't be themed without receiving the theme. Importing lightTheme directly instead would have broken dark mode.

c. New palette keyssurface, onSurface, onSurfaceVariant, surfaceContainerHigh, primaryContainer, puzzle, plus stateLayer and typography
Additive. Theme is inferred (typeof lightTheme | typeof darkTheme) and nothing outside widget-skeleton constructs a theme literal, so this widens the type without breaking consumers.

d. ⚠️ palette.grey was removed — worth a decision
The grey scale was on the palette on main and is gone here. Nothing in this repo reads palette.grey, so it's safe internally — but @prosopo/widget-skeleton is published (2.8.5, not private), so an external consumer reading theme.palette.grey[300] would break. It's the only genuinely breaking removal in the PR. Happy to restore it as a deprecated passthrough if you'd rather keep the surface stable; I left it alone since dropping it looks deliberate.


3. Conflict resolution

One conflict, in procaptcha-react/src/components/Button.tsx, and only in the import block. main added isEventTrusted (used by the click guard) while this branch added withAlpha. Both were kept — main's if (!isEventTrusted(e)) guard is preserved verbatim.

4. Test updates

#2930 landed tests on main that pin which palette key each element interpolates. Three named keys this branch deliberately moved away from:

  • The widget shell now reads surface, not background.default. Both still exist — under M3 background.default is the dialog container's role (surfaceContainerHigh), which is what separates a resting affordance from a modal container.
  • The checkbox spinner is now an M3 circular progress (border track + primary arc) where it previously drew the whole ring in background.contrastText.

The assertions were pointed at the new tokens. No styling was changed to satisfy a test — in each case the element moved first and the test followed.

5. Verification

turbo run test 83/83 tasks, turbo run typecheck 49/49, npm run lint clean (biome + licence + refs + testCheck). Also ran the stack locally and eyeballed the widget across the demo pages.

🤖 Generated with Claude Code

https://claude.ai/code/session_017hPGTJ23KyDz2WuUgzz2pi

@HughParry
HughParry enabled auto-merge (squash) August 11, 2026 12:28
@HughParry
HughParry merged commit b525956 into main Aug 11, 2026
12 checks passed
@HughParry
HughParry deleted the feat/improved-captcha-look branch August 11, 2026 12:39
HughParry added a commit that referenced this pull request Aug 13, 2026
Ports main's Material 3 restyle (#2798), the trusted-event gate
(isEventTrusted), the selectable error label and the remounting reset()
onto the vanilla DOM components, and re-points the locale and bundle
tests that main added at the modules that replaced their React originals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdXVDYG6wybfWpMmAhqDpC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants