@prosopo/procaptcha-puzzle 2.13.2 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +6 -6
  2. package/.turbo/turbo-build$colon$tsc.log +19 -19
  3. package/.turbo/turbo-build.log +7 -7
  4. package/CHANGELOG.md +50 -0
  5. package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
  6. package/dist/cjs/components/procaptchaWidget.cjs +225 -0
  7. package/dist/cjs/components/puzzleCanvas.cjs +450 -0
  8. package/dist/cjs/index.cjs +7 -2
  9. package/dist/components/procaptchaPuzzle.d.ts +7 -0
  10. package/dist/components/procaptchaPuzzle.d.ts.map +1 -0
  11. package/dist/components/procaptchaPuzzle.js +21 -0
  12. package/dist/components/procaptchaPuzzle.js.map +1 -0
  13. package/dist/components/procaptchaWidget.d.ts +7 -0
  14. package/dist/components/procaptchaWidget.d.ts.map +1 -0
  15. package/dist/components/procaptchaWidget.js +225 -0
  16. package/dist/components/procaptchaWidget.js.map +1 -0
  17. package/dist/components/{PuzzleCanvas.d.ts → puzzleCanvas.d.ts} +6 -4
  18. package/dist/components/puzzleCanvas.d.ts.map +1 -0
  19. package/dist/components/puzzleCanvas.js +450 -0
  20. package/dist/components/puzzleCanvas.js.map +1 -0
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/tests/domHarness.d.ts +20 -0
  26. package/dist/tests/domHarness.d.ts.map +1 -0
  27. package/dist/tests/domHarness.js +56 -0
  28. package/dist/tests/domHarness.js.map +1 -0
  29. package/dist/tests/procaptchaPuzzle.test-d.js +13 -10
  30. package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
  31. package/dist/tests/procaptchaPuzzle.unit.test.js +44 -33
  32. package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -1
  33. package/dist/tests/procaptchaWidget.unit.test.js +108 -175
  34. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  35. package/dist/tests/puzzleCanvas.unit.test.js +47 -80
  36. package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
  37. package/package.json +8 -12
  38. package/src/components/procaptchaPuzzle.ts +54 -0
  39. package/src/components/procaptchaWidget.ts +392 -0
  40. package/src/components/puzzleCanvas.ts +653 -0
  41. package/src/index.ts +3 -2
  42. package/src/tests/domHarness.ts +122 -0
  43. package/src/tests/procaptchaPuzzle.test-d.ts +33 -17
  44. package/src/tests/procaptchaPuzzle.unit.test.ts +59 -44
  45. package/src/tests/procaptchaWidget.unit.test.ts +145 -258
  46. package/src/tests/puzzleCanvas.unit.test.ts +76 -120
  47. package/tsconfig.cjs.json +1 -2
  48. package/tsconfig.json +1 -2
  49. package/tsconfig.tsbuildinfo +1 -1
  50. package/vite.test.config.ts +3 -4
  51. package/dist/cjs/components/ProcaptchaPuzzle.cjs +0 -7
  52. package/dist/cjs/components/ProcaptchaWidget.cjs +0 -206
  53. package/dist/cjs/components/PuzzleCanvas.cjs +0 -476
  54. package/dist/components/ProcaptchaPuzzle.d.ts +0 -3
  55. package/dist/components/ProcaptchaPuzzle.d.ts.map +0 -1
  56. package/dist/components/ProcaptchaPuzzle.js +0 -7
  57. package/dist/components/ProcaptchaPuzzle.js.map +0 -1
  58. package/dist/components/ProcaptchaWidget.d.ts +0 -4
  59. package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
  60. package/dist/components/ProcaptchaWidget.js +0 -207
  61. package/dist/components/ProcaptchaWidget.js.map +0 -1
  62. package/dist/components/PuzzleCanvas.d.ts.map +0 -1
  63. package/dist/components/PuzzleCanvas.js +0 -476
  64. package/dist/components/PuzzleCanvas.js.map +0 -1
  65. package/dist/tests/setup.d.ts +0 -5
  66. package/dist/tests/setup.d.ts.map +0 -1
  67. package/dist/tests/setup.js +0 -3
  68. package/dist/tests/setup.js.map +0 -1
  69. package/src/components/ProcaptchaPuzzle.tsx +0 -36
  70. package/src/components/ProcaptchaWidget.tsx +0 -320
  71. package/src/components/PuzzleCanvas.tsx +0 -638
  72. package/src/tests/setup.ts +0 -26
@@ -16,12 +16,11 @@ import { defineConfig, mergeConfig } from "vitest/config";
16
16
 
17
17
  process.env.NODE_ENV = "test";
18
18
 
19
- // The widgets render into the DOM and the manager clears its timeouts through
20
- // `window`, so jsdom is required; the setup file tells React it is under test
21
- // so act() flushes renders synchronously.
19
+ // The widgets build real DOM and the manager clears its timeouts through
20
+ // `window`, so jsdom is required.
22
21
  export default mergeConfig(
23
22
  ViteTestConfig(),
24
23
  defineConfig({
25
- test: { environment: "jsdom", setupFiles: ["./src/tests/setup.ts"] },
24
+ test: { environment: "jsdom" },
26
25
  }),
27
26
  );
@@ -1,7 +0,0 @@
1
- let react = require("react");
2
- let _emotion_react_jsx_runtime = require("@emotion/react/jsx-runtime");
3
- //#region src/components/ProcaptchaPuzzle.tsx
4
- var ProcaptchaWidget = (0, react.lazy)(async () => Promise.resolve().then(() => require("./ProcaptchaWidget.cjs")));
5
- var ProcaptchaPuzzle = (props) => /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)(react.Suspense, { children: /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)(ProcaptchaWidget, { ...props }) });
6
- //#endregion
7
- exports.ProcaptchaPuzzle = ProcaptchaPuzzle;
@@ -1,206 +0,0 @@
1
- const require_Manager = require("../services/Manager.cjs");
2
- const require_PuzzleCanvas = require("./PuzzleCanvas.cjs");
3
- let _prosopo_locale = require("@prosopo/locale");
4
- let _prosopo_procaptcha_common = require("@prosopo/procaptcha-common");
5
- let _prosopo_types = require("@prosopo/types");
6
- let _prosopo_widget_skeleton = require("@prosopo/widget-skeleton");
7
- let react = require("react");
8
- let _emotion_react_jsx_runtime = require("@emotion/react/jsx-runtime");
9
- //#region src/components/ProcaptchaWidget.tsx
10
- var PROCAPTCHA_EXECUTE_EVENT = "procaptcha:execute";
11
- var Procaptcha = (props) => {
12
- const { t, ready: isTranslationReady } = (0, _prosopo_locale.useTranslation)();
13
- const config = props.config;
14
- const i18n = props.i18n;
15
- const theme = "light" === config.theme ? _prosopo_widget_skeleton.lightTheme : _prosopo_widget_skeleton.darkTheme;
16
- const frictionlessState = props.frictionlessState;
17
- const callbacks = props.callbacks || {};
18
- const [state, _updateState] = (0, _prosopo_procaptcha_common.useProcaptcha)(react.useState, react.useRef);
19
- const [loading, setLoading] = (0, react.useState)(false);
20
- const [puzzlePhase, setPuzzlePhase] = (0, react.useState)("checkbox");
21
- const [challengeData, setChallengeData] = (0, react.useState)(null);
22
- const [showRetry, setShowRetry] = (0, react.useState)(false);
23
- const updateState = (0, _prosopo_procaptcha_common.buildUpdateState)(state, _updateState);
24
- const hpRef = (0, react.useRef)(null);
25
- const manager = (0, react.useRef)(require_Manager.Manager(config, state, updateState, callbacks, frictionlessState, () => hpRef.current?.value || void 0));
26
- const lastCoordsRef = (0, react.useRef)(null);
27
- const sessionInvalidatedFiredRef = (0, react.useRef)(false);
28
- (0, react.useEffect)(() => {
29
- if (!config.language) return;
30
- if (i18n) {
31
- if (i18n.language !== config.language) i18n.changeLanguage(config.language);
32
- return;
33
- }
34
- (0, _prosopo_locale.loadI18next)(false, config.language);
35
- }, [i18n, config.language]);
36
- (0, react.useEffect)(() => {
37
- if (!props.autoStart) return;
38
- setLoading(true);
39
- setShowRetry(false);
40
- const coords = props.startCoords;
41
- lastCoordsRef.current = coords ?? null;
42
- manager.current.start(coords?.x ?? 0, coords?.y ?? 0).then((challenge) => {
43
- if (challenge) {
44
- setChallengeData(challenge);
45
- setPuzzlePhase("dragging");
46
- }
47
- setLoading(false);
48
- }, () => setLoading(false));
49
- }, [props.autoStart, props.startCoords]);
50
- (0, react.useEffect)(() => {
51
- if (!state.error) return void 0;
52
- setLoading(false);
53
- setPuzzlePhase("checkbox");
54
- setChallengeData(null);
55
- setShowRetry(false);
56
- if (state.error.key !== "CAPTCHA.NO_SESSION_FOUND") return void 0;
57
- if (props.onSessionInvalidated && !sessionInvalidatedFiredRef.current) {
58
- sessionInvalidatedFiredRef.current = true;
59
- const coords = lastCoordsRef.current;
60
- props.onSessionInvalidated(coords?.x, coords?.y);
61
- return;
62
- }
63
- if (frictionlessState) {
64
- const timer = setTimeout(() => {
65
- frictionlessState.restart();
66
- }, 100);
67
- return () => clearTimeout(timer);
68
- }
69
- }, [
70
- state.error,
71
- frictionlessState,
72
- props.onSessionInvalidated
73
- ]);
74
- (0, react.useEffect)(() => {
75
- const handleExecuteEvent = async () => {
76
- if (loading) return;
77
- setLoading(true);
78
- setShowRetry(false);
79
- try {
80
- const challenge = await manager.current.start();
81
- if (challenge) {
82
- setChallengeData(challenge);
83
- setPuzzlePhase("dragging");
84
- }
85
- } catch (error) {
86
- callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
87
- } finally {
88
- setLoading(false);
89
- }
90
- };
91
- const container = props.container;
92
- const invisible = config.mode === _prosopo_types.ModeEnum.invisible;
93
- container?.addEventListener(PROCAPTCHA_EXECUTE_EVENT, handleExecuteEvent);
94
- if (invisible) document.addEventListener(PROCAPTCHA_EXECUTE_EVENT, handleExecuteEvent);
95
- return () => {
96
- container?.removeEventListener(PROCAPTCHA_EXECUTE_EVENT, handleExecuteEvent);
97
- if (invisible) document.removeEventListener(PROCAPTCHA_EXECUTE_EVENT, handleExecuteEvent);
98
- };
99
- }, [
100
- config.mode,
101
- callbacks.onError,
102
- loading,
103
- props.container
104
- ]);
105
- const handlePuzzleComplete = (0, react.useCallback)(async (finalX, finalY, puzzleEvents) => {
106
- setPuzzlePhase("submitting");
107
- let verified = false;
108
- try {
109
- verified = await manager.current.submitSolution(finalX, finalY, puzzleEvents);
110
- } catch (error) {
111
- callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
112
- }
113
- if (verified) {
114
- setPuzzlePhase("checkbox");
115
- setChallengeData(null);
116
- setShowRetry(false);
117
- setLoading(false);
118
- return;
119
- }
120
- setShowRetry(true);
121
- setPuzzlePhase("dragging");
122
- try {
123
- const newChallenge = await manager.current.start();
124
- if (newChallenge) setChallengeData(newChallenge);
125
- else {
126
- setPuzzlePhase("checkbox");
127
- setChallengeData(null);
128
- setShowRetry(false);
129
- }
130
- } catch {
131
- setPuzzlePhase("checkbox");
132
- setChallengeData(null);
133
- setShowRetry(false);
134
- }
135
- setLoading(false);
136
- }, [callbacks.onError]);
137
- const isInvisible = config.mode === _prosopo_types.ModeEnum.invisible;
138
- const handleDismiss = (0, react.useCallback)(() => {
139
- setPuzzlePhase("checkbox");
140
- setChallengeData(null);
141
- setShowRetry(false);
142
- setLoading(false);
143
- }, []);
144
- const showPuzzleOverlay = (puzzlePhase === "dragging" || puzzlePhase === "submitting") && challengeData;
145
- return /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsxs)(_emotion_react_jsx_runtime.Fragment, { children: [
146
- frictionlessState?.hp && /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)(_prosopo_procaptcha_common.Honeypot, {
147
- ref: hpRef,
148
- encodedQuestion: frictionlessState.hp
149
- }),
150
- showPuzzleOverlay && /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)(require_PuzzleCanvas.PuzzleCanvas, {
151
- originX: challengeData.originX,
152
- originY: challengeData.originY,
153
- background: challengeData.background,
154
- piece: challengeData.piece,
155
- pieceSize: challengeData.pieceSize,
156
- onComplete: handlePuzzleComplete,
157
- showRetry,
158
- submitting: puzzlePhase === "submitting",
159
- theme,
160
- placement: config.placement,
161
- anchor: props.container,
162
- onDismiss: handleDismiss
163
- }),
164
- !isInvisible && /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)(_prosopo_procaptcha_common.Checkbox, {
165
- checked: state.isHuman,
166
- theme,
167
- onChange: async (event) => {
168
- if (loading) return;
169
- setLoading(true);
170
- setShowRetry(false);
171
- let x = 0;
172
- let y = 0;
173
- const mouseOrTouchEvent = event.nativeEvent;
174
- if (!(0, _prosopo_procaptcha_common.isEventTrusted)(mouseOrTouchEvent)) {} else if ("touches" in mouseOrTouchEvent && mouseOrTouchEvent.touches.length > 0 && mouseOrTouchEvent.touches[0]) {
175
- x = mouseOrTouchEvent.touches[0].clientX;
176
- y = mouseOrTouchEvent.touches[0].clientY;
177
- } else if ("clientX" in mouseOrTouchEvent && "clientY" in mouseOrTouchEvent) {
178
- x = mouseOrTouchEvent.clientX;
179
- y = mouseOrTouchEvent.clientY;
180
- }
181
- lastCoordsRef.current = {
182
- x,
183
- y
184
- };
185
- try {
186
- const challenge = await manager.current.start(x, y);
187
- if (challenge) {
188
- setChallengeData(challenge);
189
- setPuzzlePhase("dragging");
190
- }
191
- } catch (error) {
192
- callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
193
- } finally {
194
- setLoading(false);
195
- }
196
- },
197
- labelText: isTranslationReady ? t("WIDGET.I_AM_HUMAN") : "",
198
- error: state.error?.message,
199
- "aria-label": "human checkbox",
200
- loadingText: t("WIDGET.CHECKING", { defaultValue: "Checking that you are human" }),
201
- loading: loading || puzzlePhase === "submitting"
202
- })
203
- ] });
204
- };
205
- //#endregion
206
- exports.default = Procaptcha;
@@ -1,476 +0,0 @@
1
- let _prosopo_locale = require("@prosopo/locale");
2
- let _prosopo_procaptcha_common = require("@prosopo/procaptcha-common");
3
- let react = require("react");
4
- let _emotion_react_jsx_runtime = require("@emotion/react/jsx-runtime");
5
- //#region src/components/PuzzleCanvas.tsx
6
- var CONTAINER_WIDTH = 300;
7
- var CONTAINER_HEIGHT = 200;
8
- var PIECE_CSS_CLASS = "prosopo-puzzle-piece";
9
- var STEP_PX = 10;
10
- var FINE_STEP_PX = 2;
11
- var ANNOUNCE_DEBOUNCE_MS = 400;
12
- var VISUALLY_HIDDEN = {
13
- position: "absolute",
14
- width: "1px",
15
- height: "1px",
16
- padding: 0,
17
- margin: "-1px",
18
- overflow: "hidden",
19
- clip: "rect(0, 0, 0, 0)",
20
- clipPath: "inset(50%)",
21
- whiteSpace: "nowrap",
22
- border: 0
23
- };
24
- var stylesheet = (focusRingColor) => `
25
- @keyframes prosopo-puzzle-shake {
26
- 0%, 100% { transform: translateX(0); }
27
- 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
28
- 20%, 40%, 60%, 80% { transform: translateX(4px); }
29
- }
30
- .${PIECE_CSS_CLASS}:focus-visible {
31
- outline: 3px solid ${focusRingColor};
32
- outline-offset: 2px;
33
- }
34
- `;
35
- var PuzzleCanvas = ({ originX, originY, background, piece, pieceSize, onComplete, showRetry, submitting, theme, placement, anchor, onDismiss }) => {
36
- const [posX, setPosX] = (0, react.useState)(originX);
37
- const [posY, setPosY] = (0, react.useState)(originY);
38
- const isDragging = (0, react.useRef)(false);
39
- const [dragging, setDragging] = (0, react.useState)(false);
40
- const puzzleEvents = (0, react.useRef)([]);
41
- const containerRef = (0, react.useRef)(null);
42
- const offsetRef = (0, react.useRef)({
43
- x: 0,
44
- y: 0
45
- });
46
- const [visible, setVisible] = (0, react.useState)(false);
47
- const [shaking, setShaking] = (0, react.useState)(false);
48
- const { t } = (0, _prosopo_locale.useTranslation)();
49
- const baseId = (0, react.useId)();
50
- const instructionId = `${baseId}-instruction`;
51
- const keyboardHintId = `${baseId}-keyboard-hint`;
52
- const keyboardDragging = (0, react.useRef)(false);
53
- const [announcement, setAnnouncement] = (0, react.useState)("");
54
- const announceTimer = (0, react.useRef)(null);
55
- (0, react.useEffect)(() => {
56
- setPosX(originX);
57
- setPosY(originY);
58
- keyboardDragging.current = false;
59
- }, [originX, originY]);
60
- (0, react.useEffect)(() => {
61
- const frame = requestAnimationFrame(() => setVisible(true));
62
- return () => cancelAnimationFrame(frame);
63
- }, []);
64
- (0, react.useEffect)(() => {
65
- if (showRetry) {
66
- setShaking(true);
67
- const timer = setTimeout(() => setShaking(false), 500);
68
- return () => clearTimeout(timer);
69
- }
70
- return () => {};
71
- }, [showRetry]);
72
- const clamp = (0, react.useCallback)((value, min, max) => {
73
- return Math.max(min, Math.min(max, value));
74
- }, []);
75
- const announce = (0, react.useCallback)((message, delayMs = 0) => {
76
- if (announceTimer.current) {
77
- clearTimeout(announceTimer.current);
78
- announceTimer.current = null;
79
- }
80
- if (delayMs === 0) {
81
- setAnnouncement(message);
82
- return;
83
- }
84
- announceTimer.current = setTimeout(() => setAnnouncement(message), delayMs);
85
- }, []);
86
- (0, react.useEffect)(() => () => {
87
- if (announceTimer.current) clearTimeout(announceTimer.current);
88
- }, []);
89
- const describePosition = (0, react.useCallback)((x, y) => t("WIDGET.PUZZLE.POSITION", {
90
- defaultValue: "{{x}} percent across, {{y}} percent down",
91
- x: Math.round(x / CONTAINER_WIDTH * 100),
92
- y: Math.round(y / CONTAINER_HEIGHT * 100)
93
- }), [t]);
94
- const getContainerOffset = (0, react.useCallback)(() => {
95
- if (containerRef.current) {
96
- const rect = containerRef.current.getBoundingClientRect();
97
- return {
98
- x: rect.left,
99
- y: rect.top
100
- };
101
- }
102
- return {
103
- x: 0,
104
- y: 0
105
- };
106
- }, []);
107
- const handleMoveEvent = (0, react.useCallback)((clientX, clientY) => {
108
- if (!isDragging.current) return;
109
- const containerOffset = getContainerOffset();
110
- const newX = clamp(clientX - containerOffset.x - offsetRef.current.x, 0, CONTAINER_WIDTH);
111
- const newY = clamp(clientY - containerOffset.y - offsetRef.current.y, 0, CONTAINER_HEIGHT);
112
- setPosX(newX);
113
- setPosY(newY);
114
- puzzleEvents.current.push({
115
- x: newX,
116
- y: newY,
117
- t: Date.now()
118
- });
119
- }, [clamp, getContainerOffset]);
120
- const complete = (0, react.useCallback)((finalX, finalY) => {
121
- onComplete(finalX, finalY, [...puzzleEvents.current]);
122
- }, [onComplete]);
123
- const handleEndEvent = (0, react.useCallback)(() => {
124
- if (!isDragging.current) return;
125
- isDragging.current = false;
126
- setDragging(false);
127
- const lastEvent = puzzleEvents.current[puzzleEvents.current.length - 1];
128
- complete(lastEvent ? lastEvent.x : originX, lastEvent ? lastEvent.y : originY);
129
- }, [
130
- complete,
131
- originX,
132
- originY
133
- ]);
134
- const handleMouseMove = (0, react.useCallback)((event) => {
135
- handleMoveEvent(event.clientX, event.clientY);
136
- }, [handleMoveEvent]);
137
- const handleTouchMove = (0, react.useCallback)((event) => {
138
- const touch = event.touches[0];
139
- if (touch) handleMoveEvent(touch.clientX, touch.clientY);
140
- }, [handleMoveEvent]);
141
- const handleMouseUp = (0, react.useCallback)(() => {
142
- handleEndEvent();
143
- }, [handleEndEvent]);
144
- const handleTouchEnd = (0, react.useCallback)(() => {
145
- handleEndEvent();
146
- }, [handleEndEvent]);
147
- (0, react.useEffect)(() => {
148
- document.addEventListener("mousemove", handleMouseMove);
149
- document.addEventListener("mouseup", handleMouseUp);
150
- document.addEventListener("touchmove", handleTouchMove);
151
- document.addEventListener("touchend", handleTouchEnd);
152
- return () => {
153
- document.removeEventListener("mousemove", handleMouseMove);
154
- document.removeEventListener("mouseup", handleMouseUp);
155
- document.removeEventListener("touchmove", handleTouchMove);
156
- document.removeEventListener("touchend", handleTouchEnd);
157
- };
158
- }, [
159
- handleMouseMove,
160
- handleMouseUp,
161
- handleTouchMove,
162
- handleTouchEnd
163
- ]);
164
- const handlePieceMouseDown = (0, react.useCallback)((event) => {
165
- if (submitting) return;
166
- isDragging.current = true;
167
- keyboardDragging.current = false;
168
- setDragging(true);
169
- puzzleEvents.current = [];
170
- const containerOffset = getContainerOffset();
171
- offsetRef.current = {
172
- x: event.clientX - containerOffset.x - posX,
173
- y: event.clientY - containerOffset.y - posY
174
- };
175
- }, [
176
- getContainerOffset,
177
- posX,
178
- posY,
179
- submitting
180
- ]);
181
- const handlePieceTouchStart = (0, react.useCallback)((event) => {
182
- if (submitting) return;
183
- const touch = event.touches[0];
184
- if (touch) {
185
- isDragging.current = true;
186
- keyboardDragging.current = false;
187
- setDragging(true);
188
- puzzleEvents.current = [];
189
- const containerOffset = getContainerOffset();
190
- offsetRef.current = {
191
- x: touch.clientX - containerOffset.x - posX,
192
- y: touch.clientY - containerOffset.y - posY
193
- };
194
- }
195
- }, [
196
- getContainerOffset,
197
- posX,
198
- posY,
199
- submitting
200
- ]);
201
- const moveByKeyboard = (0, react.useCallback)((deltaX, deltaY) => {
202
- if (!keyboardDragging.current) {
203
- keyboardDragging.current = true;
204
- puzzleEvents.current = [];
205
- }
206
- const nextX = clamp(posX + deltaX, 0, CONTAINER_WIDTH);
207
- const nextY = clamp(posY + deltaY, 0, CONTAINER_HEIGHT);
208
- setPosX(nextX);
209
- setPosY(nextY);
210
- puzzleEvents.current.push({
211
- x: nextX,
212
- y: nextY,
213
- t: Date.now()
214
- });
215
- announce(describePosition(nextX, nextY), ANNOUNCE_DEBOUNCE_MS);
216
- }, [
217
- announce,
218
- clamp,
219
- describePosition,
220
- posX,
221
- posY
222
- ]);
223
- const handlePieceKeyDown = (0, react.useCallback)((event) => {
224
- if (submitting) return;
225
- if (!(0, _prosopo_procaptcha_common.isEventTrusted)(event)) return;
226
- const step = event.shiftKey ? FINE_STEP_PX : STEP_PX;
227
- const move = {
228
- ArrowLeft: [-step, 0],
229
- ArrowRight: [step, 0],
230
- ArrowUp: [0, -step],
231
- ArrowDown: [0, step],
232
- Home: [originX - posX, originY - posY]
233
- }[event.key];
234
- if (move) {
235
- event.preventDefault();
236
- moveByKeyboard(move[0], move[1]);
237
- return;
238
- }
239
- if (event.key === "Enter" || event.key === " ") {
240
- event.preventDefault();
241
- keyboardDragging.current = false;
242
- complete(posX, posY);
243
- }
244
- }, [
245
- complete,
246
- moveByKeyboard,
247
- originX,
248
- originY,
249
- posX,
250
- posY,
251
- submitting
252
- ]);
253
- const handlePieceFocus = (0, react.useCallback)(() => {
254
- announce(describePosition(posX, posY));
255
- }, [
256
- announce,
257
- describePosition,
258
- posX,
259
- posY
260
- ]);
261
- const instructionText = showRetry ? t("WIDGET.PUZZLE.RETRY", { defaultValue: "Not quite — try again" }) : t("WIDGET.PUZZLE.DRAG", { defaultValue: "Drag the piece to the target" });
262
- const keyboardHintText = t("WIDGET.PUZZLE.KEYBOARD_HINT", { defaultValue: "Use the arrow keys to move the piece, holding shift for smaller steps. Press Enter to submit it, Home to put it back at the start, or Escape to cancel." });
263
- (0, react.useEffect)(() => {
264
- if (!submitting) return;
265
- announce(t("WIDGET.PUZZLE.CHECKING", { defaultValue: "Checking your answer" }));
266
- }, [
267
- submitting,
268
- announce,
269
- t
270
- ]);
271
- (0, react.useEffect)(() => {
272
- if (!showRetry) return;
273
- announce(t("WIDGET.PUZZLE.RETRY_ANNOUNCEMENT", { defaultValue: "Not quite. A new puzzle has loaded and the piece is back at the start." }));
274
- }, [
275
- showRetry,
276
- announce,
277
- t
278
- ]);
279
- const headerBorderColor = showRetry ? theme.palette.error.main : "transparent";
280
- const headerTextColor = showRetry ? theme.palette.error.main : theme.palette.onSurface;
281
- const puzzleAreaBg = `linear-gradient(135deg, ${theme.palette.surface} 0%, ${theme.palette.primaryContainer.main} 50%, ${theme.palette.surface} 100%)`;
282
- const deltaX = posX - originX;
283
- const deltaY = posY - originY;
284
- const dragDistance = Math.hypot(deltaX, deltaY);
285
- const PARALLAX_FACTOR = .08;
286
- const bgTranslateX = dragging ? -deltaX * PARALLAX_FACTOR : 0;
287
- const bgTranslateY = dragging ? -deltaY * PARALLAX_FACTOR : 0;
288
- const bgScale = dragging ? 1.03 : 1;
289
- const hueShift = dragging ? Math.max(-18, Math.min(18, deltaX * .12)) : 0;
290
- const satBoost = dragging ? 1 + Math.min(.25, dragDistance * .0035) : 1;
291
- const brightness = dragging ? 1 - Math.min(.06, dragDistance * 8e-4) : 1;
292
- const bgFilter = `hue-rotate(${hueShift.toFixed(2)}deg) saturate(${satBoost.toFixed(3)}) brightness(${brightness.toFixed(3)})`;
293
- const bgTransform = `translate(${bgTranslateX.toFixed(2)}px, ${bgTranslateY.toFixed(2)}px) scale(${bgScale.toFixed(3)})`;
294
- return /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsxs)(_prosopo_procaptcha_common.ChallengeSurface, {
295
- show: true,
296
- placement,
297
- anchor,
298
- onDismiss,
299
- scrim: visible ? "dim" : "none",
300
- dialogLabel: t("WIDGET.PUZZLE.DIALOG_LABEL", { defaultValue: "Puzzle challenge" }),
301
- children: [
302
- /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("style", { children: stylesheet(theme.palette.primary.main) }),
303
- /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("output", {
304
- "aria-live": "polite",
305
- "aria-atomic": "true",
306
- style: VISUALLY_HIDDEN,
307
- children: announcement
308
- }),
309
- /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("div", {
310
- id: keyboardHintId,
311
- style: VISUALLY_HIDDEN,
312
- children: keyboardHintText
313
- }),
314
- /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsxs)("div", {
315
- style: {
316
- display: "flex",
317
- flexDirection: "column",
318
- alignItems: "center",
319
- gap: "0",
320
- opacity: visible ? 1 : 0,
321
- transform: visible ? "scale(1)" : "scale(0.9)",
322
- transition: "opacity 0.3s ease, transform 0.3s ease",
323
- animation: shaking ? "prosopo-puzzle-shake 0.5s ease" : "none"
324
- },
325
- children: [/* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("div", {
326
- id: instructionId,
327
- style: {
328
- backgroundColor: theme.palette.surface,
329
- borderRadius: "20px 20px 0 0",
330
- padding: "12px 20px",
331
- width: `${CONTAINER_WIDTH}px`,
332
- boxSizing: "border-box",
333
- textAlign: "center",
334
- fontFamily: theme.font.fontFamily,
335
- fontSize: "14px",
336
- fontWeight: 500,
337
- color: headerTextColor,
338
- borderBottom: `2px solid ${headerBorderColor}`,
339
- transition: "color 0.3s ease, border-color 0.3s ease"
340
- },
341
- children: instructionText
342
- }), /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsxs)("div", {
343
- ref: containerRef,
344
- style: {
345
- position: "relative",
346
- width: `${CONTAINER_WIDTH}px`,
347
- height: `${CONTAINER_HEIGHT}px`,
348
- background: puzzleAreaBg,
349
- borderRadius: "0 0 20px 20px",
350
- overflow: "hidden",
351
- userSelect: "none",
352
- opacity: submitting ? .6 : 1,
353
- pointerEvents: submitting ? "none" : "auto",
354
- transition: "opacity 0.2s ease"
355
- },
356
- children: [/* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("div", {
357
- style: {
358
- position: "absolute",
359
- left: `-${CONTAINER_WIDTH}px`,
360
- top: `-${CONTAINER_HEIGHT}px`,
361
- width: `${CONTAINER_WIDTH * 3}px`,
362
- height: `${CONTAINER_HEIGHT * 3}px`,
363
- pointerEvents: "none",
364
- userSelect: "none",
365
- transform: bgTransform,
366
- transformOrigin: "center center",
367
- filter: bgFilter,
368
- transition: dragging ? "none" : "transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), filter 0.35s ease",
369
- willChange: "transform, filter"
370
- },
371
- children: [
372
- [
373
- 0,
374
- 0,
375
- -1,
376
- -1
377
- ],
378
- [
379
- 1,
380
- 0,
381
- 1,
382
- -1
383
- ],
384
- [
385
- 2,
386
- 0,
387
- -1,
388
- -1
389
- ],
390
- [
391
- 0,
392
- 1,
393
- -1,
394
- 1
395
- ],
396
- [
397
- 1,
398
- 1,
399
- 1,
400
- 1
401
- ],
402
- [
403
- 2,
404
- 1,
405
- -1,
406
- 1
407
- ],
408
- [
409
- 0,
410
- 2,
411
- -1,
412
- -1
413
- ],
414
- [
415
- 1,
416
- 2,
417
- 1,
418
- -1
419
- ],
420
- [
421
- 2,
422
- 2,
423
- -1,
424
- -1
425
- ]
426
- ].map(([col, row, sx, sy]) => {
427
- const key = `${col}-${row}`;
428
- return /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("img", {
429
- src: background,
430
- alt: "",
431
- draggable: false,
432
- style: {
433
- position: "absolute",
434
- left: `${(col ?? 0) * CONTAINER_WIDTH}px`,
435
- top: `${(row ?? 0) * CONTAINER_HEIGHT}px`,
436
- width: `${CONTAINER_WIDTH}px`,
437
- height: `${CONTAINER_HEIGHT}px`,
438
- transform: `scale(${sx}, ${sy})`,
439
- pointerEvents: "none",
440
- userSelect: "none"
441
- }
442
- }, key);
443
- })
444
- }), /* @__PURE__ */ (0, _emotion_react_jsx_runtime.jsx)("div", {
445
- ...process.env.NODE_ENV !== "production" && { "data-cy": "prosopo-puzzle-piece" },
446
- className: PIECE_CSS_CLASS,
447
- role: "application",
448
- "aria-roledescription": t("WIDGET.PUZZLE.PIECE_ROLE", { defaultValue: "draggable puzzle piece" }),
449
- "aria-label": t("WIDGET.PUZZLE.PIECE_LABEL", { defaultValue: "Puzzle piece" }),
450
- "aria-describedby": `${instructionId} ${keyboardHintId}`,
451
- tabIndex: submitting ? -1 : 0,
452
- onFocus: handlePieceFocus,
453
- onKeyDown: handlePieceKeyDown,
454
- onMouseDown: handlePieceMouseDown,
455
- onTouchStart: handlePieceTouchStart,
456
- style: {
457
- position: "absolute",
458
- left: `${posX - pieceSize / 2}px`,
459
- top: `${posY - pieceSize / 2}px`,
460
- width: `${pieceSize}px`,
461
- height: `${pieceSize}px`,
462
- backgroundImage: `url(${piece})`,
463
- backgroundSize: "100% 100%",
464
- touchAction: "none",
465
- cursor: submitting ? "default" : isDragging.current ? "grabbing" : "grab",
466
- filter: isDragging.current ? "drop-shadow(0 4px 10px rgba(0, 0, 0, 0.45))" : "drop-shadow(0 2px 5px rgba(0, 0, 0, 0.35))",
467
- transition: isDragging.current ? "none" : "filter 0.2s ease, left 0.3s ease, top 0.3s ease"
468
- }
469
- })]
470
- })]
471
- })
472
- ]
473
- });
474
- };
475
- //#endregion
476
- exports.PuzzleCanvas = PuzzleCanvas;