@funnycaptcha/embed 0.2.0 → 0.3.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.
@@ -92,11 +92,12 @@ var FunnyCaptcha = (() => {
92
92
  return c.answer === userAnswer;
93
93
  }
94
94
  var STR = {
95
- zh: { title: "\u8BF7\u8BA1\u7B97", placeholder: "\u8F93\u5165\u7B54\u6848", submit: "\u9A8C\u8BC1", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
96
- en: { title: "Solve", placeholder: "Enter answer", submit: "Verify", fail: "Wrong, try again" }
95
+ zh: { title: "\u8BF7\u8BA1\u7B97", placeholder: "\u8F93\u5165\u7B54\u6848", submit: "\u9A8C\u8BC1", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898", refresh: "\u5237\u65B0" },
96
+ en: { title: "Solve", placeholder: "Enter answer", submit: "Verify", fail: "Wrong, try again", refresh: "Refresh" }
97
97
  };
98
98
  function createMathInstance(container, config) {
99
99
  const t = STR[config.locale];
100
+ const theme = config.theme ?? "light";
100
101
  let current;
101
102
  let listeners = [];
102
103
  let startTime = Date.now();
@@ -104,16 +105,36 @@ var FunnyCaptcha = (() => {
104
105
  current = generateChallenge();
105
106
  startTime = Date.now();
106
107
  container.innerHTML = `
107
- <div class="fc-math">
108
+ <style>
109
+ .fc-math{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
110
+ .fc-math[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
111
+ .fc-math[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
112
+ .fc-math{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
113
+ .fc-math-q{display:block;font-size:14px;font-weight:600;color:var(--fc-text);margin-bottom:12px}
114
+ .fc-math-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
115
+ .fc-math-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
116
+ .fc-math-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
117
+ .fc-math-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
118
+ .fc-math-btn:hover{opacity:.9}
119
+ .fc-math-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
120
+ .fc-math-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
121
+ .fc-math-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
122
+ </style>
123
+ <div class="fc-math" data-theme="${theme}">
108
124
  <label class="fc-math-q">${t.title}: ${current.question}</label>
109
- <input class="fc-math-input" type="text" inputmode="numeric" placeholder="${t.placeholder}" />
110
- <button class="fc-math-btn">${t.submit}</button>
125
+ <div class="fc-math-row">
126
+ <input class="fc-math-input" type="text" inputmode="numeric" placeholder="${t.placeholder}" />
127
+ <button class="fc-math-btn">${t.submit}</button>
128
+ <button class="fc-math-refresh">${t.refresh}</button>
129
+ </div>
111
130
  <div class="fc-math-msg"></div>
112
131
  </div>
113
132
  `;
114
133
  const btn = container.querySelector(".fc-math-btn");
115
134
  const input = container.querySelector(".fc-math-input");
116
135
  const msg = container.querySelector(".fc-math-msg");
136
+ const refreshBtn = container.querySelector(".fc-math-refresh");
137
+ refreshBtn.addEventListener("click", render2);
117
138
  btn.addEventListener("click", async () => {
118
139
  const val = Number(input.value);
119
140
  const success = verifyAnswer(current, val);
@@ -172,12 +193,16 @@ var FunnyCaptcha = (() => {
172
193
  zh: { placeholder: "\u8F93\u5165\u56FE\u4E2D\u5B57\u7B26", submit: "\u9A8C\u8BC1", fail: "\u770B\u4E0D\u6E05\uFF1F\u6362\u4E00\u5F20", refresh: "\u5237\u65B0" },
173
194
  en: { placeholder: "Type the text", submit: "Verify", fail: "Wrong? Try another", refresh: "Refresh" }
174
195
  };
175
- function drawDistorted(canvas, code) {
196
+ function drawDistorted(canvas, code, surface, text, border) {
176
197
  const ctx = canvas.getContext("2d");
177
198
  if (!ctx) return;
178
199
  ctx.clearRect(0, 0, canvas.width, canvas.height);
179
- ctx.fillStyle = "#f0f0f0";
200
+ ctx.fillStyle = surface;
180
201
  ctx.fillRect(0, 0, canvas.width, canvas.height);
202
+ for (let i = 0; i < 80; i++) {
203
+ ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.12})`;
204
+ ctx.fillRect(Math.random() * canvas.width, Math.random() * canvas.height, 1.5, 1.5);
205
+ }
181
206
  const chars = code.split("");
182
207
  const step = canvas.width / (chars.length + 1);
183
208
  chars.forEach((ch, i) => {
@@ -187,22 +212,34 @@ var FunnyCaptcha = (() => {
187
212
  ctx.translate(x, y);
188
213
  ctx.rotate((Math.random() - 0.5) * 0.5);
189
214
  ctx.font = `${24 + Math.floor(Math.random() * 8)}px sans-serif`;
190
- ctx.fillStyle = `hsl(${Math.random() * 360}, 70%, 40%)`;
215
+ ctx.fillStyle = text;
191
216
  ctx.textAlign = "center";
192
217
  ctx.textBaseline = "middle";
193
218
  ctx.fillText(ch, 0, 0);
194
219
  ctx.restore();
195
220
  });
196
- for (let i = 0; i < 4; i++) {
197
- ctx.strokeStyle = `hsla(${Math.random() * 360}, 70%, 50%, 0.4)`;
221
+ for (let i = 0; i < 6; i++) {
222
+ ctx.strokeStyle = border;
223
+ ctx.lineWidth = 1 + Math.random() * 1.5;
198
224
  ctx.beginPath();
199
- ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
200
- ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
225
+ const x1 = Math.random() * canvas.width;
226
+ const y1 = Math.random() * canvas.height;
227
+ const x2 = Math.random() * canvas.width;
228
+ const y2 = Math.random() * canvas.height;
229
+ const cpx = Math.random() * canvas.width;
230
+ const cpy = Math.random() * canvas.height;
231
+ ctx.moveTo(x1, y1);
232
+ ctx.quadraticCurveTo(cpx, cpy, x2, y2);
201
233
  ctx.stroke();
202
234
  }
203
235
  }
236
+ function readVar(root, name, fallback) {
237
+ const v = getComputedStyle(root).getPropertyValue(name).trim();
238
+ return v || fallback;
239
+ }
204
240
  function createTextDistortInstance(container, config) {
205
241
  const t = STR2[config.locale];
242
+ const theme = config.theme ?? "light";
206
243
  let current;
207
244
  let listeners = [];
208
245
  let startTime = Date.now();
@@ -210,16 +247,37 @@ var FunnyCaptcha = (() => {
210
247
  current = generateChallenge2();
211
248
  startTime = Date.now();
212
249
  container.innerHTML = `
213
- <div class="fc-text">
214
- <canvas class="fc-text-canvas" width="180" height="60"></canvas>
215
- <button class="fc-text-refresh">${t.refresh}</button>
216
- <input class="fc-text-input" placeholder="${t.placeholder}" />
217
- <button class="fc-text-btn">${t.submit}</button>
250
+ <style>
251
+ .fc-text{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
252
+ .fc-text[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
253
+ .fc-text[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
254
+ .fc-text{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
255
+ .fc-text-canvas{display:block;width:100%;max-width:240px;height:60px;border-radius:8px;border:1px solid var(--fc-border);margin-bottom:10px}
256
+ .fc-text-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
257
+ .fc-text-input{flex:1;min-width:0;padding:8px 12px;font-size:14px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text);border-radius:8px;box-sizing:border-box;outline:none}
258
+ .fc-text-input:focus{border-color:var(--fc-accent);box-shadow:0 0 0 3px var(--fc-accent-soft)}
259
+ .fc-text-btn{padding:8px 16px;font-size:14px;border:1px solid var(--fc-accent);background:var(--fc-accent);color:#fff;border-radius:8px;cursor:pointer;transition:opacity .15s}
260
+ .fc-text-btn:hover{opacity:.9}
261
+ .fc-text-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
262
+ .fc-text-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
263
+ .fc-text-msg{font-size:13px;min-height:18px;margin-top:10px;color:var(--fc-danger)}
264
+ </style>
265
+ <div class="fc-text" data-theme="${theme}">
266
+ <canvas class="fc-text-canvas" width="240" height="60"></canvas>
267
+ <div class="fc-text-row">
268
+ <input class="fc-text-input" placeholder="${t.placeholder}" />
269
+ <button class="fc-text-btn">${t.submit}</button>
270
+ <button class="fc-text-refresh">${t.refresh}</button>
271
+ </div>
218
272
  <div class="fc-text-msg"></div>
219
273
  </div>
220
274
  `;
275
+ const root = container.querySelector(".fc-text");
221
276
  const canvas = container.querySelector(".fc-text-canvas");
222
- drawDistorted(canvas, current.code);
277
+ const surface = readVar(root, "--fc-surface", "#f6f7f9");
278
+ const text = readVar(root, "--fc-text", "#0f172a");
279
+ const border = readVar(root, "--fc-border", "#e2e8f0");
280
+ drawDistorted(canvas, current.code, surface, text, border);
223
281
  container.querySelector(".fc-text-refresh").addEventListener("click", render2);
224
282
  const btn = container.querySelector(".fc-text-btn");
225
283
  const input = container.querySelector(".fc-text-input");
@@ -274,11 +332,12 @@ var FunnyCaptcha = (() => {
274
332
  return `${c.token}:completed`;
275
333
  }
276
334
  var STR3 = {
277
- zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\u6700\u53F3\u7AEF", tip: "\u5411\u53F3\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934" },
278
- en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end" }
335
+ zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5230\u6700\u53F3\u7AEF", tip: "\u5411\u53F3\u62D6\u52A8", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u8BF7\u62D6\u5230\u5C3D\u5934", refresh: "\u5237\u65B0" },
336
+ en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end", refresh: "Refresh" }
279
337
  };
280
338
  function createSliderInstance(container, config) {
281
339
  const t = STR3[config.locale];
340
+ const theme = config.theme ?? "light";
282
341
  let current;
283
342
  let listeners = [];
284
343
  let startTime = Date.now();
@@ -289,17 +348,26 @@ var FunnyCaptcha = (() => {
289
348
  done = false;
290
349
  container.innerHTML = `
291
350
  <style>
292
- .fc-slider{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box}
293
- .fc-slider-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
294
- .fc-slider-track{position:relative;height:40px;background:#e8e8e8;border-radius:20px}
295
- .fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:#4a90d9;border-radius:20px;transition:width .05s}
296
- .fc-slider-handle{position:absolute;left:0;top:50%;transform:translateY(-50%);width:50px;height:50px;background:#fff;border:2px solid #4a90d9;border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.15);user-select:none;touch-action:none}
351
+ .fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
352
+ .fc-slider[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
353
+ .fc-slider[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
354
+ .fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
355
+ .fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
356
+ .fc-slider-track{position:relative;height:40px;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:20px}
357
+ .fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:var(--fc-accent);border-radius:20px;transition:width .05s,background .15s}
358
+ .fc-slider-handle{position:absolute;left:0;top:50%;transform:translateY(-50%);width:50px;height:50px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.15);user-select:none;touch-action:none;transition:border-color .15s,color .15s}
297
359
  .fc-slider-handle:active{cursor:grabbing}
298
- .fc-slider-handle::after{content:'\\2192';color:#4a90d9;font-size:18px;font-weight:bold}
299
- .fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:#999;pointer-events:none;transition:opacity .2s}
300
- .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
360
+ .fc-slider-handle::after{content:'\\2192';color:var(--fc-accent);font-size:18px;font-weight:bold;transition:color .15s}
361
+ .fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:var(--fc-text-soft);pointer-events:none;transition:opacity .2s}
362
+ .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
363
+ .fc-slider-row{display:flex;justify-content:center;margin-top:10px}
364
+ .fc-slider-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
365
+ .fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
366
+ .fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}
367
+ .fc-slider-done .fc-slider-handle::after{color:var(--fc-success)}
368
+ .fc-slider-done .fc-slider-progress{background:var(--fc-success)}
301
369
  </style>
302
- <div class="fc-slider">
370
+ <div class="fc-slider" data-theme="${theme}">
303
371
  <div class="fc-slider-title">${t.title}</div>
304
372
  <div class="fc-slider-track">
305
373
  <div class="fc-slider-progress"></div>
@@ -307,13 +375,19 @@ var FunnyCaptcha = (() => {
307
375
  <span class="fc-slider-tip">${t.tip}</span>
308
376
  </div>
309
377
  <div class="fc-slider-msg"></div>
378
+ <div class="fc-slider-row">
379
+ <button class="fc-slider-refresh">${t.refresh}</button>
380
+ </div>
310
381
  </div>
311
382
  `;
383
+ const root = container.querySelector(".fc-slider");
312
384
  const track = container.querySelector(".fc-slider-track");
313
385
  const handle = container.querySelector(".fc-slider-handle");
314
386
  const progress = container.querySelector(".fc-slider-progress");
315
387
  const tip = container.querySelector(".fc-slider-tip");
316
388
  const msg = container.querySelector(".fc-slider-msg");
389
+ const refreshBtn = container.querySelector(".fc-slider-refresh");
390
+ refreshBtn.addEventListener("click", render2);
317
391
  let dragging = false;
318
392
  let startX = 0;
319
393
  let startOffset = 0;
@@ -349,6 +423,7 @@ var FunnyCaptcha = (() => {
349
423
  if (currentOffset / maxX >= 0.95) {
350
424
  done = true;
351
425
  applyPos(maxX);
426
+ root.classList.add("fc-slider-done");
352
427
  const result = {
353
428
  success: true,
354
429
  proof: await hashProof(proofInput(current)),
@@ -414,17 +489,41 @@ var FunnyCaptcha = (() => {
414
489
  return c.order.join(",");
415
490
  }
416
491
  var STR4 = {
417
- zh: { title: "\u6309 1 \u2192 2 \u2192 3 \u2192 4 \u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5" },
418
- en: { title: "Click in order 1 \u2192 2 \u2192 3 \u2192 4", success: "Verified", fail: "Wrong order, try again" }
492
+ zh: { title: "\u6309 1 \u2192 2 \u2192 3 \u2192 4 \u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
493
+ en: { title: "Click in order 1 \u2192 2 \u2192 3 \u2192 4", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
419
494
  };
420
- var SLOTS = [
421
- { x: 20, y: 20 },
422
- { x: 160, y: 20 },
423
- { x: 20, y: 100 },
424
- { x: 160, y: 100 }
425
- ];
495
+ var AREA_W = 280;
496
+ var AREA_H = 180;
497
+ var BOX_W = 100;
498
+ var BOX_H = 60;
499
+ var MIN_DIST = 110;
500
+ function generateSlots(count) {
501
+ const maxX = AREA_W - BOX_W;
502
+ const maxY = AREA_H - BOX_H;
503
+ const slots = [];
504
+ let attempts = 0;
505
+ while (slots.length < count && attempts < 1e3) {
506
+ attempts++;
507
+ const candidate = { x: Math.random() * maxX, y: Math.random() * maxY };
508
+ let ok = true;
509
+ for (const s of slots) {
510
+ const dx = candidate.x - s.x;
511
+ const dy = candidate.y - s.y;
512
+ if (Math.sqrt(dx * dx + dy * dy) < MIN_DIST) {
513
+ ok = false;
514
+ break;
515
+ }
516
+ }
517
+ if (ok) slots.push(candidate);
518
+ }
519
+ while (slots.length < count) {
520
+ slots.push({ x: Math.random() * maxX, y: Math.random() * maxY });
521
+ }
522
+ return slots;
523
+ }
426
524
  function createClickOrderInstance(container, config) {
427
525
  const t = STR4[config.locale];
526
+ const theme = config.theme ?? "light";
428
527
  let current;
429
528
  let listeners = [];
430
529
  let startTime = Date.now();
@@ -435,30 +534,42 @@ var FunnyCaptcha = (() => {
435
534
  clicked = [];
436
535
  finished = false;
437
536
  startTime = Date.now();
537
+ const slots = generateSlots(current.targets.length);
438
538
  const boxes = current.targets.map((num, i) => {
439
- const slot = SLOTS[i];
440
- return `<div class="fc-click-box" data-num="${num}" style="left:${slot.x}px;top:${slot.y}px;">${num}</div>`;
539
+ const slot = slots[i];
540
+ return `<div class="fc-click-box" data-num="${num}" style="left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;">${num}</div>`;
441
541
  }).join("");
442
542
  container.innerHTML = `
443
543
  <style>
444
- .fc-click{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
445
- .fc-click-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
446
- .fc-click-area{position:relative;width:280px;height:180px;background:#f5f5f5;border-radius:8px;border:1px solid #e0e0e0;overflow:hidden}
447
- .fc-click-box{position:absolute;width:100px;height:60px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:#4a90d9;background:#fff;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s}
544
+ .fc-click{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
545
+ .fc-click[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
546
+ .fc-click[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
547
+ .fc-click{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
548
+ .fc-click-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
549
+ .fc-click-area{position:relative;width:${AREA_W}px;height:${AREA_H}px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
550
+ .fc-click-box{position:absolute;width:${BOX_W}px;height:${BOX_H}px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:var(--fc-accent);background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s,border-color .15s}
448
551
  .fc-click-box:hover{transform:scale(1.05)}
449
- .fc-click-box-active{background:#4a90d9;color:#fff}
450
- .fc-click-box-done{background:#2e7d32;border-color:#2e7d32;color:#fff;cursor:default}
552
+ .fc-click-box-active{background:var(--fc-accent);color:var(--fc-bg)}
553
+ .fc-click-box-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
451
554
  .fc-click-box-done:hover{transform:none}
452
- .fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
555
+ .fc-click-row{display:flex;justify-content:center;margin-top:10px}
556
+ .fc-click-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
557
+ .fc-click-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
558
+ .fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
453
559
  </style>
454
- <div class="fc-click">
560
+ <div class="fc-click" data-theme="${theme}">
455
561
  <div class="fc-click-title">${t.title}</div>
456
562
  <div class="fc-click-area">${boxes}</div>
457
563
  <div class="fc-click-msg"></div>
564
+ <div class="fc-click-row">
565
+ <button class="fc-click-refresh">${t.refresh}</button>
566
+ </div>
458
567
  </div>
459
568
  `;
460
569
  const area = container.querySelector(".fc-click-area");
461
570
  const msg = container.querySelector(".fc-click-msg");
571
+ const refreshBtn = container.querySelector(".fc-click-refresh");
572
+ refreshBtn.addEventListener("click", render2);
462
573
  area.querySelectorAll(".fc-click-box").forEach((el) => {
463
574
  el.addEventListener("click", async () => {
464
575
  if (finished) return;
@@ -527,11 +638,12 @@ var FunnyCaptcha = (() => {
527
638
  return `${c.angle}:aligned`;
528
639
  }
529
640
  var STR5 = {
530
- zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9" },
531
- en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up" }
641
+ zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9", refresh: "\u5237\u65B0" },
642
+ en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up", refresh: "Refresh" }
532
643
  };
533
644
  function createRotateInstance(container, config) {
534
645
  const t = STR5[config.locale];
646
+ const theme = config.theme ?? "light";
535
647
  let current;
536
648
  let listeners = [];
537
649
  let startTime = Date.now();
@@ -542,31 +654,42 @@ var FunnyCaptcha = (() => {
542
654
  done = false;
543
655
  container.innerHTML = `
544
656
  <style>
545
- .fc-rotate{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
546
- .fc-rotate-title{font-size:14px;color:#333;text-align:center}
547
- .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed #ccc;border-radius:50%}
657
+ .fc-rotate{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
658
+ .fc-rotate[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
659
+ .fc-rotate[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
660
+ .fc-rotate{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
661
+ .fc-rotate-title{font-size:14px;color:var(--fc-text);text-align:center}
662
+ .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed var(--fc-border);border-radius:50%}
548
663
  .fc-rotate-figure{width:100px;height:100px;transform-origin:center;transition:transform .05s}
549
- .fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid #e74c3c}
550
- .fc-rotate-hint{font-size:12px;color:#999}
551
- .fc-rotate-range{width:240px;accent-color:#4a90d9}
552
- .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:#2e7d32}
664
+ .fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid var(--fc-danger)}
665
+ .fc-rotate-hint{font-size:12px;color:var(--fc-text-soft)}
666
+ .fc-rotate-range{width:240px;accent-color:var(--fc-accent)}
667
+ .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:var(--fc-success)}
668
+ .fc-rotate-row{display:flex;justify-content:center}
669
+ .fc-rotate-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
670
+ .fc-rotate-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
553
671
  </style>
554
- <div class="fc-rotate">
672
+ <div class="fc-rotate" data-theme="${theme}">
555
673
  <div class="fc-rotate-title">${t.title}</div>
556
674
  <div class="fc-rotate-stage">
557
675
  <div class="fc-rotate-figure" style="transform: rotate(${current.angle}deg)">
558
- <svg viewBox="0 0 100 100" width="100" height="100"><polygon points="50,12 90,85 10,85" fill="#4a90d9"/></svg>
676
+ <svg viewBox="0 0 100 100" width="100" height="100"><path d="M 50 8 L 82 42 L 62 42 L 62 92 L 38 92 L 38 42 Z" fill="var(--fc-accent)"/></svg>
559
677
  </div>
560
678
  <div class="fc-rotate-marker"></div>
561
679
  </div>
562
680
  <div class="fc-rotate-hint">${t.hint}</div>
563
681
  <input class="fc-rotate-range" type="range" min="0" max="360" step="1" value="0" />
564
682
  <div class="fc-rotate-msg"></div>
683
+ <div class="fc-rotate-row">
684
+ <button class="fc-rotate-refresh">${t.refresh}</button>
685
+ </div>
565
686
  </div>
566
687
  `;
567
688
  const figure = container.querySelector(".fc-rotate-figure");
568
689
  const range = container.querySelector(".fc-rotate-range");
569
690
  const msg = container.querySelector(".fc-rotate-msg");
691
+ const refreshBtn = container.querySelector(".fc-rotate-refresh");
692
+ refreshBtn.addEventListener("click", render2);
570
693
  const update = async () => {
571
694
  if (done) return;
572
695
  const v = Number(range.value);
@@ -649,11 +772,12 @@ var FunnyCaptcha = (() => {
649
772
  return sorted.every((v, i) => v === c.diffs[i]);
650
773
  }
651
774
  var STR6 = {
652
- zh: { title: "\u627E\u51FA\u4E24\u5904\u4E0D\u540C", hint: "\u70B9\u51FB\u53F3\u4FA7\u7F51\u683C\u4E2D\u4E0D\u540C\u7684\u683C\u5B50", found: "\u5DF2\u627E\u5230", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u627E\u5F97\u4E0D\u5BF9\uFF0C\u518D\u6765\u4E00\u6B21" },
653
- en: { title: "Spot 2 differences", hint: "Click the differing cells on the right grid", found: "Found", success: "Verified", fail: "Not quite, try again" }
775
+ zh: { title: "\u627E\u51FA\u4E24\u5904\u4E0D\u540C", hint: "\u70B9\u51FB\u53F3\u4FA7\u7F51\u683C\u4E2D\u4E0D\u540C\u7684\u683C\u5B50", found: "\u5DF2\u627E\u5230", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u627E\u5F97\u4E0D\u5BF9\uFF0C\u518D\u6765\u4E00\u6B21", refresh: "\u5237\u65B0" },
776
+ en: { title: "Spot 2 differences", hint: "Click the differing cells on the right grid", found: "Found", success: "Verified", fail: "Not quite, try again", refresh: "Refresh" }
654
777
  };
655
778
  function createSpotDiffInstance(container, config) {
656
779
  const t = STR6[config.locale];
780
+ const theme = config.theme ?? "light";
657
781
  let current;
658
782
  let listeners = [];
659
783
  let startTime = Date.now();
@@ -670,20 +794,26 @@ var FunnyCaptcha = (() => {
670
794
  startTime = Date.now();
671
795
  container.innerHTML = `
672
796
  <style>
673
- .fc-spot-diff{font-family:-apple-system,system-ui,sans-serif;width:360px;padding:16px;box-sizing:border-box}
674
- .fc-spot-diff-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
675
- .fc-spot-diff-hint{font-size:12px;color:#888;margin-bottom:12px;text-align:center}
797
+ .fc-spot-diff{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
798
+ .fc-spot-diff[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
799
+ .fc-spot-diff[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
800
+ .fc-spot-diff{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
801
+ .fc-spot-diff-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
802
+ .fc-spot-diff-hint{font-size:12px;color:var(--fc-text-soft);margin-bottom:12px;text-align:center}
676
803
  .fc-spot-diff-grids{display:flex;gap:16px;justify-content:center}
677
- .fc-spot-diff-grid{display:grid;grid-template-columns:repeat(3,48px);grid-template-rows:repeat(3,48px);gap:4px;background:#f5f5f5;padding:8px;border-radius:8px;border:1px solid #e0e0e0}
678
- .fc-spot-diff-cell{display:flex;align-items:center;justify-content:center;font-size:26px;background:#fff;border-radius:6px;user-select:none}
804
+ .fc-spot-diff-grid{display:grid;grid-template-columns:repeat(3,48px);grid-template-rows:repeat(3,48px);gap:4px;background:var(--fc-surface);padding:8px;border-radius:8px;border:1px solid var(--fc-border)}
805
+ .fc-spot-diff-cell{display:flex;align-items:center;justify-content:center;font-size:26px;background:var(--fc-bg);border-radius:6px;user-select:none}
679
806
  .fc-spot-diff-clickable{cursor:pointer;transition:transform .12s,box-shadow .12s}
680
807
  .fc-spot-diff-clickable:hover{transform:scale(1.06)}
681
- .fc-spot-diff-marked{box-shadow:0 0 0 3px #4a90d9 inset;background:#eaf3ff}
682
- .fc-spot-diff-wrong{box-shadow:0 0 0 3px #e53935 inset;background:#fdecea}
683
- .fc-spot-diff-status{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
684
- .fc-spot-diff-status-fail{color:#e53935}
808
+ .fc-spot-diff-marked{box-shadow:0 0 0 3px var(--fc-accent) inset;background:var(--fc-accent-soft)}
809
+ .fc-spot-diff-wrong{box-shadow:0 0 0 3px var(--fc-danger) inset;background:var(--fc-danger)}
810
+ .fc-spot-diff-status{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-success)}
811
+ .fc-spot-diff-status-fail{color:var(--fc-danger)}
812
+ .fc-spot-diff-row{display:flex;justify-content:center;margin-top:8px}
813
+ .fc-spot-diff-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
814
+ .fc-spot-diff-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
685
815
  </style>
686
- <div class="fc-spot-diff">
816
+ <div class="fc-spot-diff" data-theme="${theme}">
687
817
  <div class="fc-spot-diff-title">${t.title}</div>
688
818
  <div class="fc-spot-diff-hint">${t.hint}</div>
689
819
  <div class="fc-spot-diff-grids">
@@ -691,10 +821,15 @@ var FunnyCaptcha = (() => {
691
821
  <div class="fc-spot-diff-grid fc-spot-diff-grid-b">${renderGrid(current.gridB, true)}</div>
692
822
  </div>
693
823
  <div class="fc-spot-diff-status">${t.found} 0/${current.diffs.length}</div>
824
+ <div class="fc-spot-diff-row">
825
+ <button class="fc-spot-diff-refresh">${t.refresh}</button>
826
+ </div>
694
827
  </div>
695
828
  `;
696
829
  const gridB = container.querySelector(".fc-spot-diff-grid-b");
697
830
  const status = container.querySelector(".fc-spot-diff-status");
831
+ const refreshBtn = container.querySelector(".fc-spot-diff-refresh");
832
+ refreshBtn.addEventListener("click", render2);
698
833
  gridB.querySelectorAll(".fc-spot-diff-clickable").forEach((el) => {
699
834
  el.addEventListener("click", async () => {
700
835
  if (finished) return;
@@ -789,11 +924,12 @@ var FunnyCaptcha = (() => {
789
924
  return c.correct === userPick;
790
925
  }
791
926
  var STR7 = {
792
- zh: { title: "\u9009\u51FA\u7B26\u5408\u63CF\u8FF0\u7684\u8868\u60C5", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21" },
793
- en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again" }
927
+ zh: { title: "\u9009\u51FA\u7B26\u5408\u63CF\u8FF0\u7684\u8868\u60C5", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21", refresh: "\u5237\u65B0" },
928
+ en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
794
929
  };
795
930
  function createEmojiMatchInstance(container, config) {
796
931
  const t = STR7[config.locale];
932
+ const theme = config.theme ?? "light";
797
933
  let current;
798
934
  let listeners = [];
799
935
  let startTime = Date.now();
@@ -807,24 +943,36 @@ var FunnyCaptcha = (() => {
807
943
  ).join("");
808
944
  container.innerHTML = `
809
945
  <style>
810
- .fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
811
- .fc-emoji-match-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
812
- .fc-emoji-match-desc{font-size:15px;color:#4a90d9;margin-bottom:14px;text-align:center;font-weight:600}
946
+ .fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
947
+ .fc-emoji-match[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
948
+ .fc-emoji-match[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
949
+ .fc-emoji-match{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
950
+ .fc-emoji-match-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
951
+ .fc-emoji-match-desc{font-size:15px;color:var(--fc-accent);margin-bottom:14px;text-align:center;font-weight:600}
813
952
  .fc-emoji-match-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
814
- .fc-emoji-match-opt{font-size:30px;height:64px;border:2px solid #e0e0e0;background:#fff;border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;display:flex;align-items:center;justify-content:center}
815
- .fc-emoji-match-opt:hover{transform:scale(1.06);border-color:#4a90d9}
816
- .fc-emoji-match-opt-wrong{border-color:#e53935;background:#fdecea}
817
- .fc-emoji-match-opt-correct{border-color:#2e7d32;background:#e8f5e9}
818
- .fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
953
+ .fc-emoji-match-opt{font-size:30px;height:64px;border:2px solid var(--fc-border);background:var(--fc-bg);border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;display:flex;align-items:center;justify-content:center;color:var(--fc-text)}
954
+ .fc-emoji-match-opt:hover{transform:scale(1.06);border-color:var(--fc-accent)}
955
+ .fc-emoji-match-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}
956
+ .fc-emoji-match-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}
957
+ .fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
958
+ .fc-emoji-match-msg-ok{color:var(--fc-success)}
959
+ .fc-emoji-match-row{display:flex;justify-content:center;margin-top:8px}
960
+ .fc-emoji-match-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
961
+ .fc-emoji-match-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
819
962
  </style>
820
- <div class="fc-emoji-match">
963
+ <div class="fc-emoji-match" data-theme="${theme}">
821
964
  <div class="fc-emoji-match-title">${t.title}</div>
822
965
  <div class="fc-emoji-match-desc">${current.description}</div>
823
966
  <div class="fc-emoji-match-opts">${buttons}</div>
824
967
  <div class="fc-emoji-match-msg"></div>
968
+ <div class="fc-emoji-match-row">
969
+ <button class="fc-emoji-match-refresh">${t.refresh}</button>
970
+ </div>
825
971
  </div>
826
972
  `;
827
973
  const msg = container.querySelector(".fc-emoji-match-msg");
974
+ const refreshBtn = container.querySelector(".fc-emoji-match-refresh");
975
+ refreshBtn.addEventListener("click", render2);
828
976
  container.querySelectorAll(".fc-emoji-match-opt").forEach((el) => {
829
977
  el.addEventListener("click", async () => {
830
978
  if (finished) return;
@@ -838,7 +986,7 @@ var FunnyCaptcha = (() => {
838
986
  proof: await hashProof(current.correct),
839
987
  duration: Date.now() - startTime
840
988
  };
841
- msg.style.color = "#2e7d32";
989
+ msg.classList.add("fc-emoji-match-msg-ok");
842
990
  msg.textContent = t.success;
843
991
  config.onVerify?.(result);
844
992
  listeners.forEach((cb) => cb(result));
@@ -906,11 +1054,12 @@ var FunnyCaptcha = (() => {
906
1054
  return c.correct === userPick;
907
1055
  }
908
1056
  var STR8 = {
909
- zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
910
- en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again" }
1057
+ zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898", refresh: "\u5237\u65B0" },
1058
+ en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
911
1059
  };
912
1060
  function createMemeQuizInstance(container, config) {
913
1061
  const t = STR8[config.locale];
1062
+ const theme = config.theme ?? "light";
914
1063
  let current;
915
1064
  let listeners = [];
916
1065
  let startTime = Date.now();
@@ -924,26 +1073,38 @@ var FunnyCaptcha = (() => {
924
1073
  ).join("");
925
1074
  container.innerHTML = `
926
1075
  <style>
927
- .fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
928
- .fc-meme-quiz-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
929
- .fc-meme-quiz-hint{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
930
- .fc-meme-quiz-q{font-size:24px;color:#4a90d9;margin-bottom:14px;text-align:center;letter-spacing:2px}
1076
+ .fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
1077
+ .fc-meme-quiz[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1078
+ .fc-meme-quiz[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
1079
+ .fc-meme-quiz{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
1080
+ .fc-meme-quiz-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
1081
+ .fc-meme-quiz-hint{font-size:12px;color:var(--fc-text-soft);margin-bottom:10px;text-align:center}
1082
+ .fc-meme-quiz-q{font-size:24px;color:var(--fc-accent);margin-bottom:14px;text-align:center;letter-spacing:2px}
931
1083
  .fc-meme-quiz-opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}
932
- .fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid #e0e0e0;background:#fff;border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:#333}
933
- .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:#4a90d9}
934
- .fc-meme-quiz-opt-wrong{border-color:#e53935;background:#fdecea}
935
- .fc-meme-quiz-opt-correct{border-color:#2e7d32;background:#e8f5e9}
936
- .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
1084
+ .fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid var(--fc-border);background:var(--fc-bg);border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:var(--fc-text)}
1085
+ .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:var(--fc-accent)}
1086
+ .fc-meme-quiz-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}
1087
+ .fc-meme-quiz-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}
1088
+ .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
1089
+ .fc-meme-quiz-msg-ok{color:var(--fc-success)}
1090
+ .fc-meme-quiz-row{display:flex;justify-content:center;margin-top:8px}
1091
+ .fc-meme-quiz-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
1092
+ .fc-meme-quiz-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
937
1093
  </style>
938
- <div class="fc-meme-quiz">
1094
+ <div class="fc-meme-quiz" data-theme="${theme}">
939
1095
  <div class="fc-meme-quiz-title">${t.title}</div>
940
1096
  <div class="fc-meme-quiz-hint">${t.hint}</div>
941
1097
  <div class="fc-meme-quiz-q">${current.question}</div>
942
1098
  <div class="fc-meme-quiz-opts">${buttons}</div>
943
1099
  <div class="fc-meme-quiz-msg"></div>
1100
+ <div class="fc-meme-quiz-row">
1101
+ <button class="fc-meme-quiz-refresh">${t.refresh}</button>
1102
+ </div>
944
1103
  </div>
945
1104
  `;
946
1105
  const msg = container.querySelector(".fc-meme-quiz-msg");
1106
+ const refreshBtn = container.querySelector(".fc-meme-quiz-refresh");
1107
+ refreshBtn.addEventListener("click", render2);
947
1108
  container.querySelectorAll(".fc-meme-quiz-opt").forEach((el) => {
948
1109
  el.addEventListener("click", async () => {
949
1110
  if (finished) return;
@@ -957,7 +1118,7 @@ var FunnyCaptcha = (() => {
957
1118
  proof: await hashProof(current.correct),
958
1119
  duration: Date.now() - startTime
959
1120
  };
960
- msg.style.color = "#2e7d32";
1121
+ msg.classList.add("fc-meme-quiz-msg-ok");
961
1122
  msg.textContent = t.success;
962
1123
  config.onVerify?.(result);
963
1124
  listeners.forEach((cb) => cb(result));
@@ -1014,7 +1175,8 @@ var FunnyCaptcha = (() => {
1014
1175
  time: "\u5269\u4F59",
1015
1176
  sec: "\u79D2",
1016
1177
  success: "\u9A8C\u8BC1\u6210\u529F\uFF01\u4F60\u679C\u7136\u662F\u4EBA\u7C7B",
1017
- fail: "\u65F6\u95F4\u5230\uFF0C\u6CA1\u8FBE\u5230\u76EE\u6807\u5206\u6570"
1178
+ fail: "\u65F6\u95F4\u5230\uFF0C\u6CA1\u8FBE\u5230\u76EE\u6807\u5206\u6570",
1179
+ restart: "\u91CD\u65B0\u5F00\u59CB"
1018
1180
  },
1019
1181
  en: {
1020
1182
  title: "Whack-a-Mole",
@@ -1023,12 +1185,16 @@ var FunnyCaptcha = (() => {
1023
1185
  time: "Time",
1024
1186
  sec: "s",
1025
1187
  success: "Verified! You are human.",
1026
- fail: "Time's up, target not reached"
1188
+ fail: "Time's up, target not reached",
1189
+ restart: "Restart"
1027
1190
  }
1028
1191
  };
1029
1192
  var HOLE_COUNT = 9;
1193
+ var MOLE_MIN_DELAY = 600;
1194
+ var MOLE_MAX_DELAY = 1200;
1030
1195
  function createMiniGameInstance(container, config) {
1031
1196
  const t = STR9[config.locale];
1197
+ const theme = config.theme ?? "light";
1032
1198
  let current;
1033
1199
  let listeners = [];
1034
1200
  let startTime = Date.now();
@@ -1045,7 +1211,7 @@ var FunnyCaptcha = (() => {
1045
1211
  countdownTimer = null;
1046
1212
  }
1047
1213
  if (moleTimer) {
1048
- clearInterval(moleTimer);
1214
+ clearTimeout(moleTimer);
1049
1215
  moleTimer = null;
1050
1216
  }
1051
1217
  if (hideTimer) {
@@ -1093,6 +1259,14 @@ var FunnyCaptcha = (() => {
1093
1259
  if (activeHole === next) activeHole = -1;
1094
1260
  }, 700);
1095
1261
  }
1262
+ function scheduleMole() {
1263
+ if (ended) return;
1264
+ const delay = MOLE_MIN_DELAY + Math.random() * (MOLE_MAX_DELAY - MOLE_MIN_DELAY);
1265
+ moleTimer = setTimeout(() => {
1266
+ showMole();
1267
+ scheduleMole();
1268
+ }, delay);
1269
+ }
1096
1270
  function startGame() {
1097
1271
  clearTimers();
1098
1272
  score = 0;
@@ -1114,8 +1288,8 @@ var FunnyCaptcha = (() => {
1114
1288
  void endGame(verifyScore(current, score));
1115
1289
  }
1116
1290
  }, 100);
1117
- moleTimer = setInterval(showMole, 850);
1118
1291
  showMole();
1292
+ scheduleMole();
1119
1293
  }
1120
1294
  function render2() {
1121
1295
  current = generateChallenge9();
@@ -1129,19 +1303,25 @@ var FunnyCaptcha = (() => {
1129
1303
  ).join("");
1130
1304
  container.innerHTML = `
1131
1305
  <style>
1132
- .fc-mini-game{font-family:-apple-system,system-ui,sans-serif;width:340px;padding:16px;box-sizing:border-box}
1133
- .fc-mini-game-title{font-size:15px;color:#333;margin-bottom:4px;text-align:center;font-weight:600}
1134
- .fc-mini-game-rule{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
1135
- .fc-mini-game-hud{display:flex;justify-content:space-between;font-size:13px;color:#555;margin-bottom:10px}
1136
- .fc-mini-game-hud b{color:#4a90d9}
1137
- .fc-mini-game-board{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:8px;width:300px;height:300px;margin:0 auto;background:#e8f5e9;border:1px solid #c8e6c9;border-radius:10px;padding:8px;box-sizing:border-box}
1306
+ .fc-mini-game{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
1307
+ .fc-mini-game[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1308
+ .fc-mini-game[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
1309
+ .fc-mini-game{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
1310
+ .fc-mini-game-title{font-size:15px;color:var(--fc-text);margin-bottom:4px;text-align:center;font-weight:600}
1311
+ .fc-mini-game-rule{font-size:12px;color:var(--fc-text-soft);margin-bottom:10px;text-align:center}
1312
+ .fc-mini-game-hud{display:flex;justify-content:space-between;font-size:13px;color:var(--fc-text-soft);margin-bottom:10px}
1313
+ .fc-mini-game-hud b{color:var(--fc-accent)}
1314
+ .fc-mini-game-board{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(3,1fr);gap:8px;width:300px;height:300px;margin:0 auto;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:10px;padding:8px;box-sizing:border-box}
1138
1315
  .fc-mini-game-hole{position:relative;background:radial-gradient(circle at 50% 60%,#6d4c41,#4e342e);border-radius:50%;display:flex;align-items:flex-end;justify-content:center;overflow:hidden;cursor:pointer;user-select:none}
1139
1316
  .fc-mini-game-hole:hover{background:radial-gradient(circle at 50% 60%,#7d5c51,#5e443e)}
1140
1317
  .fc-mini-game-mole{font-size:34px;line-height:1;transform:translateY(120%);transition:transform .12s ease-out;pointer-events:none}
1141
1318
  .fc-mini-game-mole-active .fc-mini-game-mole{transform:translateY(0)}
1142
- .fc-mini-game-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
1319
+ .fc-mini-game-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-success)}
1320
+ .fc-mini-game-row{display:flex;justify-content:center;margin-top:8px}
1321
+ .fc-mini-game-restart{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
1322
+ .fc-mini-game-restart:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
1143
1323
  </style>
1144
- <div class="fc-mini-game">
1324
+ <div class="fc-mini-game" data-theme="${theme}">
1145
1325
  <div class="fc-mini-game-title">${t.title}</div>
1146
1326
  <div class="fc-mini-game-rule">${t.rule}</div>
1147
1327
  <div class="fc-mini-game-hud">
@@ -1150,8 +1330,13 @@ var FunnyCaptcha = (() => {
1150
1330
  </div>
1151
1331
  <div class="fc-mini-game-board">${holes}</div>
1152
1332
  <div class="fc-mini-game-msg"></div>
1333
+ <div class="fc-mini-game-row">
1334
+ <button class="fc-mini-game-restart">${t.restart}</button>
1335
+ </div>
1153
1336
  </div>
1154
1337
  `;
1338
+ const restartBtn = container.querySelector(".fc-mini-game-restart");
1339
+ restartBtn.addEventListener("click", startGame);
1155
1340
  container.querySelectorAll(".fc-mini-game-hole").forEach((el) => {
1156
1341
  el.addEventListener("click", () => {
1157
1342
  if (ended) return;
@@ -1233,6 +1418,7 @@ var FunnyCaptcha = (() => {
1233
1418
  };
1234
1419
  function createAntiBotInstance(container, config) {
1235
1420
  const t = STR10[config.locale];
1421
+ const theme = config.theme ?? "light";
1236
1422
  let current;
1237
1423
  let listeners = [];
1238
1424
  let startTime = Date.now();
@@ -1270,17 +1456,20 @@ var FunnyCaptcha = (() => {
1270
1456
  }
1271
1457
  container.innerHTML = `
1272
1458
  <style>
1273
- .fc-anti-bot{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:20px;box-sizing:border-box;border:1px solid #e0e0e0;border-radius:10px;background:#fafafa}
1274
- .fc-anti-bot-title{font-size:18px;color:#333;text-align:center;font-weight:600;margin-bottom:6px}
1275
- .fc-anti-bot-subtitle{font-size:12px;color:#888;text-align:center;margin-bottom:16px}
1276
- .fc-anti-bot-btn{display:block;width:100%;padding:12px;font-size:15px;border:1px solid #ccc;background:#fff;border-radius:6px;cursor:pointer;color:#333;transition:background .15s}
1277
- .fc-anti-bot-btn:hover:not(:disabled){background:#f0f7ff}
1278
- .fc-anti-bot-btn:disabled{cursor:default;color:#2e7d32;border-color:#2e7d32;background:#e8f5e9}
1279
- .fc-anti-bot-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#2e7d32}
1280
- .fc-anti-bot-hint{font-size:11px;color:#bbb;margin-top:14px;text-align:center;line-height:1.5;font-style:italic}
1281
- .fc-anti-bot-footer{font-size:10px;color:#ccc;margin-top:8px;text-align:center}
1459
+ .fc-anti-bot{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:20px;box-sizing:border-box;border:1px solid var(--fc-border);border-radius:10px;background:var(--fc-surface)}
1460
+ .fc-anti-bot[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1461
+ .fc-anti-bot[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
1462
+ .fc-anti-bot{color:var(--fc-text)}
1463
+ .fc-anti-bot-title{font-size:18px;color:var(--fc-text);text-align:center;font-weight:600;margin-bottom:6px}
1464
+ .fc-anti-bot-subtitle{font-size:12px;color:var(--fc-text-soft);text-align:center;margin-bottom:16px}
1465
+ .fc-anti-bot-btn{display:block;width:100%;padding:12px;font-size:15px;border:1px solid var(--fc-border);background:var(--fc-bg);border-radius:6px;cursor:pointer;color:var(--fc-text);transition:background .15s,border-color .15s}
1466
+ .fc-anti-bot-btn:hover:not(:disabled){background:var(--fc-accent-soft);border-color:var(--fc-accent)}
1467
+ .fc-anti-bot-btn:disabled{cursor:default;color:var(--fc-success);border-color:var(--fc-success);background:var(--fc-accent-soft)}
1468
+ .fc-anti-bot-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-success)}
1469
+ .fc-anti-bot-hint{font-size:11px;color:var(--fc-text-soft);margin-top:14px;text-align:center;line-height:1.5;font-style:italic}
1470
+ .fc-anti-bot-footer{font-size:10px;color:var(--fc-text-soft);margin-top:8px;text-align:center;opacity:.7}
1282
1471
  </style>
1283
- <div class="fc-anti-bot">
1472
+ <div class="fc-anti-bot" data-theme="${theme}">
1284
1473
  <div class="fc-anti-bot-title">${t.title}</div>
1285
1474
  <div class="fc-anti-bot-subtitle">${t.subtitle}</div>
1286
1475
  <button class="fc-anti-bot-btn">${t.btn}</button>
@@ -1330,6 +1519,415 @@ var FunnyCaptcha = (() => {
1330
1519
  };
1331
1520
  defineCaptcha(antiBotPlugin);
1332
1521
 
1522
+ // ../captchas/click-text/dist/index.js
1523
+ var COMMON_CHARS = [
1524
+ "\u7684",
1525
+ "\u4E86",
1526
+ "\u662F",
1527
+ "\u5728",
1528
+ "\u6709",
1529
+ "\u4EBA",
1530
+ "\u8FD9",
1531
+ "\u4E2D",
1532
+ "\u5927",
1533
+ "\u4E3A",
1534
+ "\u4E0A",
1535
+ "\u4E2A",
1536
+ "\u56FD",
1537
+ "\u6211",
1538
+ "\u4EE5",
1539
+ "\u8981",
1540
+ "\u4ED6",
1541
+ "\u65F6",
1542
+ "\u6765",
1543
+ "\u7528",
1544
+ "\u4EEC",
1545
+ "\u5730",
1546
+ "\u5230",
1547
+ "\u53EF"
1548
+ ];
1549
+ var AREA_W2 = 320;
1550
+ var AREA_H2 = 200;
1551
+ var MARGIN = 30;
1552
+ var MIN_DIST2 = 100;
1553
+ function randInt6(min, max) {
1554
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1555
+ }
1556
+ function shuffle5(arr) {
1557
+ const a = arr.slice();
1558
+ for (let i = a.length - 1; i > 0; i--) {
1559
+ const j = Math.floor(Math.random() * (i + 1));
1560
+ const tmp = a[i];
1561
+ a[i] = a[j];
1562
+ a[j] = tmp;
1563
+ }
1564
+ return a;
1565
+ }
1566
+ function placePositions(count) {
1567
+ const positions = [];
1568
+ let attempts = 0;
1569
+ while (positions.length < count && attempts < 2e3) {
1570
+ attempts++;
1571
+ const x = MARGIN + Math.random() * (AREA_W2 - MARGIN * 2);
1572
+ const y = MARGIN + Math.random() * (AREA_H2 - MARGIN * 2);
1573
+ const ok = positions.every((p) => Math.hypot(p.x - x, p.y - y) >= MIN_DIST2);
1574
+ if (ok) positions.push({ x, y });
1575
+ }
1576
+ return positions;
1577
+ }
1578
+ function generateChallenge11() {
1579
+ const count = randInt6(3, 4);
1580
+ const chars = shuffle5(COMMON_CHARS).slice(0, count);
1581
+ const positions = placePositions(count);
1582
+ return { chars, positions };
1583
+ }
1584
+ function proofInput6(c) {
1585
+ return `${c.chars.join("")}:completed`;
1586
+ }
1587
+ var STR11 = {
1588
+ zh: { title: "\u8BF7\u6309\u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
1589
+ en: { title: "Click in order", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
1590
+ };
1591
+ function createClickTextInstance(container, config) {
1592
+ const t = STR11[config.locale];
1593
+ let current;
1594
+ let listeners = [];
1595
+ let startTime = Date.now();
1596
+ let clicked = [];
1597
+ let finished = false;
1598
+ function render2() {
1599
+ current = generateChallenge11();
1600
+ clicked = [];
1601
+ finished = false;
1602
+ startTime = Date.now();
1603
+ const prompt = current.chars.join("\u3001");
1604
+ const chars = current.chars.map((ch, i) => {
1605
+ const p = current.positions[i];
1606
+ return `<div class="fc-click-text-char" data-ch="${ch}" style="left:${p.x}px;top:${p.y}px;">${ch}</div>`;
1607
+ }).join("");
1608
+ container.innerHTML = `
1609
+ <style>
1610
+ .fc-click-text{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
1611
+ .fc-click-text[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1612
+ .fc-click-text[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
1613
+ .fc-click-text-title{font-size:14px;color:var(--fc-text);margin-bottom:4px;text-align:center}
1614
+ .fc-click-text-prompt{font-size:15px;color:var(--fc-accent);margin-bottom:12px;text-align:center;font-weight:600}
1615
+ .fc-click-text-area{position:relative;width:320px;height:200px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
1616
+ .fc-click-text-char{position:absolute;width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:600;color:var(--fc-text);background:var(--fc-bg);border:2px solid var(--fc-border);border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s,border-color .15s}
1617
+ .fc-click-text-char:hover{transform:scale(1.08);border-color:var(--fc-accent)}
1618
+ .fc-click-text-char-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
1619
+ .fc-click-text-char-done:hover{transform:none}
1620
+ .fc-click-text-char-wrong{background:var(--fc-danger);border-color:var(--fc-danger);color:#fff}
1621
+ .fc-click-text-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
1622
+ .fc-click-text-msg-fail{color:var(--fc-danger)}
1623
+ .fc-click-text-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
1624
+ .fc-click-text-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
1625
+ </style>
1626
+ <div class="fc-click-text" data-theme="${config.theme}">
1627
+ <div class="fc-click-text-title">${t.title}</div>
1628
+ <div class="fc-click-text-prompt">${prompt}</div>
1629
+ <div class="fc-click-text-area">${chars}</div>
1630
+ <div class="fc-click-text-msg"></div>
1631
+ <button class="fc-click-text-refresh">${t.refresh}</button>
1632
+ </div>
1633
+ `;
1634
+ const msg = container.querySelector(".fc-click-text-msg");
1635
+ const refreshBtn = container.querySelector(".fc-click-text-refresh");
1636
+ refreshBtn.addEventListener("click", () => render2());
1637
+ container.querySelectorAll(".fc-click-text-char").forEach((el) => {
1638
+ el.addEventListener("click", async () => {
1639
+ if (finished) return;
1640
+ const box = el;
1641
+ if (box.classList.contains("fc-click-text-char-done")) return;
1642
+ const ch = box.dataset.ch;
1643
+ const expected = current.chars[clicked.length];
1644
+ if (ch === expected) {
1645
+ clicked.push(ch);
1646
+ box.classList.add("fc-click-text-char-done");
1647
+ if (clicked.length === current.chars.length) {
1648
+ finished = true;
1649
+ const result = {
1650
+ success: true,
1651
+ proof: await hashProof(proofInput6(current)),
1652
+ duration: Date.now() - startTime
1653
+ };
1654
+ msg.textContent = t.success;
1655
+ config.onVerify?.(result);
1656
+ listeners.forEach((cb) => cb(result));
1657
+ }
1658
+ } else {
1659
+ finished = true;
1660
+ box.classList.add("fc-click-text-char-wrong");
1661
+ msg.classList.add("fc-click-text-msg-fail");
1662
+ msg.textContent = t.fail;
1663
+ setTimeout(render2, 700);
1664
+ }
1665
+ });
1666
+ });
1667
+ }
1668
+ return {
1669
+ mount: () => render2(),
1670
+ reset: () => render2(),
1671
+ destroy: () => {
1672
+ container.innerHTML = "";
1673
+ listeners = [];
1674
+ },
1675
+ onResult: (cb) => listeners.push(cb)
1676
+ };
1677
+ }
1678
+ var clickTextPlugin = {
1679
+ id: "click-text",
1680
+ category: "interactive",
1681
+ create: createClickTextInstance,
1682
+ describe: (locale) => ({
1683
+ name: locale === "zh" ? "\u6587\u5B57\u70B9\u9009" : "Click Text",
1684
+ description: locale === "zh" ? "\u6309\u6307\u5B9A\u987A\u5E8F\u70B9\u51FB\u753B\u5E03\u4E0A\u7684\u6C49\u5B57" : "Click characters on the canvas in the specified order.",
1685
+ tags: ["click", "text", "interactive"]
1686
+ })
1687
+ };
1688
+ defineCaptcha(clickTextPlugin);
1689
+
1690
+ // ../captchas/color-pick/dist/index.js
1691
+ var COLORS = [
1692
+ { name: "red", zh: "\u7EA2\u8272", en: "red", hex: "#ef4444" },
1693
+ { name: "blue", zh: "\u84DD\u8272", en: "blue", hex: "#3b82f6" },
1694
+ { name: "green", zh: "\u7EFF\u8272", en: "green", hex: "#22c55e" },
1695
+ { name: "yellow", zh: "\u9EC4\u8272", en: "yellow", hex: "#eab308" },
1696
+ { name: "purple", zh: "\u7D2B\u8272", en: "purple", hex: "#a855f7" },
1697
+ { name: "orange", zh: "\u6A59\u8272", en: "orange", hex: "#f97316" },
1698
+ { name: "pink", zh: "\u7C89\u8272", en: "pink", hex: "#ec4899" },
1699
+ { name: "cyan", zh: "\u9752\u8272", en: "cyan", hex: "#06b6d4" }
1700
+ ];
1701
+ function randInt7(min, max) {
1702
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1703
+ }
1704
+ function shuffle6(arr) {
1705
+ const a = arr.slice();
1706
+ for (let i = a.length - 1; i > 0; i--) {
1707
+ const j = Math.floor(Math.random() * (i + 1));
1708
+ const tmp = a[i];
1709
+ a[i] = a[j];
1710
+ a[j] = tmp;
1711
+ }
1712
+ return a;
1713
+ }
1714
+ function colorLabel(name, locale) {
1715
+ const c = COLORS.find((c2) => c2.name === name);
1716
+ return c ? locale === "zh" ? c.zh : c.en : name;
1717
+ }
1718
+ function generateChallenge12() {
1719
+ const target = COLORS[randInt7(0, COLORS.length - 1)];
1720
+ const distractorCount = randInt7(5, 8);
1721
+ const others = COLORS.filter((c) => c.name !== target.name);
1722
+ const distractors = shuffle6(others).slice(0, Math.min(distractorCount, others.length));
1723
+ const options = shuffle6([
1724
+ { name: target.name, hex: target.hex },
1725
+ ...distractors.map((d) => ({ name: d.name, hex: d.hex }))
1726
+ ]);
1727
+ return { targetColor: target.name, targetHex: target.hex, options };
1728
+ }
1729
+ function proofInput7(c) {
1730
+ return `${c.targetColor}:completed`;
1731
+ }
1732
+ function verifyAnswer5(c, userPickHex) {
1733
+ return c.targetHex === userPickHex;
1734
+ }
1735
+ var STR12 = {
1736
+ zh: { success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u9009\u9519\u4E86\uFF0C\u518D\u8BD5\u4E00\u6B21", refresh: "\u5237\u65B0" },
1737
+ en: { success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
1738
+ };
1739
+ function createColorPickInstance(container, config) {
1740
+ const t = STR12[config.locale];
1741
+ let current;
1742
+ let listeners = [];
1743
+ let startTime = Date.now();
1744
+ let finished = false;
1745
+ function render2() {
1746
+ current = generateChallenge12();
1747
+ finished = false;
1748
+ startTime = Date.now();
1749
+ const label = colorLabel(current.targetColor, config.locale);
1750
+ const prompt = config.locale === "zh" ? `\u8BF7\u70B9\u51FB${label}\u7684\u65B9\u5757` : `Click the ${label} square`;
1751
+ const squares = current.options.map(
1752
+ (o) => `<button class="fc-color-pick-opt" data-hex="${o.hex}" style="background:${o.hex}" aria-label="${o.name}"></button>`
1753
+ ).join("");
1754
+ container.innerHTML = `
1755
+ <style>
1756
+ .fc-color-pick{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
1757
+ .fc-color-pick[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1758
+ .fc-color-pick[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
1759
+ .fc-color-pick-prompt{font-size:15px;color:var(--fc-text);margin-bottom:12px;text-align:center;font-weight:600}
1760
+ .fc-color-pick-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
1761
+ .fc-color-pick-opt{height:56px;border:2px solid var(--fc-border);border-radius:10px;cursor:pointer;transition:transform .12s,border-color .12s;outline:none;padding:0}
1762
+ .fc-color-pick-opt:hover{transform:scale(1.06);border-color:var(--fc-accent)}
1763
+ .fc-color-pick-opt-correct{border-color:var(--fc-success);transform:scale(1.06)}
1764
+ .fc-color-pick-opt-wrong{border-color:var(--fc-danger)}
1765
+ .fc-color-pick-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
1766
+ .fc-color-pick-msg-success{color:var(--fc-success)}
1767
+ .fc-color-pick-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
1768
+ .fc-color-pick-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
1769
+ </style>
1770
+ <div class="fc-color-pick" data-theme="${config.theme}">
1771
+ <div class="fc-color-pick-prompt">${prompt}</div>
1772
+ <div class="fc-color-pick-opts">${squares}</div>
1773
+ <div class="fc-color-pick-msg"></div>
1774
+ <button class="fc-color-pick-refresh">${t.refresh}</button>
1775
+ </div>
1776
+ `;
1777
+ const msg = container.querySelector(".fc-color-pick-msg");
1778
+ const refreshBtn = container.querySelector(".fc-color-pick-refresh");
1779
+ refreshBtn.addEventListener("click", () => render2());
1780
+ container.querySelectorAll(".fc-color-pick-opt").forEach((el) => {
1781
+ el.addEventListener("click", async () => {
1782
+ if (finished) return;
1783
+ const btn = el;
1784
+ const pick2 = btn.dataset.hex;
1785
+ if (verifyAnswer5(current, pick2)) {
1786
+ finished = true;
1787
+ btn.classList.add("fc-color-pick-opt-correct");
1788
+ msg.classList.add("fc-color-pick-msg-success");
1789
+ const result = {
1790
+ success: true,
1791
+ proof: await hashProof(proofInput7(current)),
1792
+ duration: Date.now() - startTime
1793
+ };
1794
+ msg.textContent = t.success;
1795
+ config.onVerify?.(result);
1796
+ listeners.forEach((cb) => cb(result));
1797
+ } else {
1798
+ finished = true;
1799
+ btn.classList.add("fc-color-pick-opt-wrong");
1800
+ msg.textContent = t.fail;
1801
+ setTimeout(render2, 800);
1802
+ }
1803
+ });
1804
+ });
1805
+ }
1806
+ return {
1807
+ mount: () => render2(),
1808
+ reset: () => render2(),
1809
+ destroy: () => {
1810
+ container.innerHTML = "";
1811
+ listeners = [];
1812
+ },
1813
+ onResult: (cb) => listeners.push(cb)
1814
+ };
1815
+ }
1816
+ var colorPickPlugin = {
1817
+ id: "color-pick",
1818
+ category: "recognize",
1819
+ create: createColorPickInstance,
1820
+ describe: (locale) => ({
1821
+ name: locale === "zh" ? "\u989C\u8272\u9009\u62E9" : "Color Pick",
1822
+ description: locale === "zh" ? "\u4ECE\u591A\u4E2A\u5F69\u8272\u65B9\u5757\u4E2D\u70B9\u51FB\u6307\u5B9A\u989C\u8272" : "Pick the square of the specified color.",
1823
+ tags: ["color", "pick", "recognize"]
1824
+ })
1825
+ };
1826
+ defineCaptcha(colorPickPlugin);
1827
+
1828
+ // ../captchas/puzzle/dist/index.js
1829
+ var TOLERANCE = 5;
1830
+ function randInt8(min, max) {
1831
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1832
+ }
1833
+ function generateChallenge13() {
1834
+ return { gapPosition: randInt8(10, 90) };
1835
+ }
1836
+ function proofInput8(c) {
1837
+ return `${c.gapPosition}:completed`;
1838
+ }
1839
+ function verifyPosition(c, userPos) {
1840
+ return Math.abs(userPos - c.gapPosition) <= TOLERANCE;
1841
+ }
1842
+ var STR13 = {
1843
+ zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", refresh: "\u5237\u65B0" },
1844
+ en: { title: "Drag the slider to fit the puzzle", success: "Verified", refresh: "Refresh" }
1845
+ };
1846
+ var STAGE_W = 300;
1847
+ var PIECE_W = 50;
1848
+ var MAX_OFFSET = STAGE_W - PIECE_W;
1849
+ function createPuzzleInstance(container, config) {
1850
+ const t = STR13[config.locale];
1851
+ let current;
1852
+ let listeners = [];
1853
+ let startTime = Date.now();
1854
+ let done = false;
1855
+ function render2() {
1856
+ current = generateChallenge13();
1857
+ startTime = Date.now();
1858
+ done = false;
1859
+ const gapLeft = current.gapPosition / 100 * MAX_OFFSET;
1860
+ container.innerHTML = `
1861
+ <style>
1862
+ .fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
1863
+ .fc-puzzle[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
1864
+ .fc-puzzle[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
1865
+ .fc-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
1866
+ .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:160px;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}
1867
+ .fc-puzzle-gap{position:absolute;top:55px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15)}
1868
+ .fc-puzzle-piece{position:absolute;top:55px;left:0;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25)}
1869
+ .fc-puzzle-range{width:${STAGE_W}px;margin-top:14px;accent-color:var(--fc-accent)}
1870
+ .fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
1871
+ .fc-puzzle-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
1872
+ .fc-puzzle-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
1873
+ </style>
1874
+ <div class="fc-puzzle" data-theme="${config.theme}">
1875
+ <div class="fc-puzzle-title">${t.title}</div>
1876
+ <div class="fc-puzzle-stage">
1877
+ <div class="fc-puzzle-gap" style="left:${gapLeft}px"></div>
1878
+ <div class="fc-puzzle-piece" style="left:0"></div>
1879
+ </div>
1880
+ <input class="fc-puzzle-range" type="range" min="0" max="100" step="1" value="0" />
1881
+ <div class="fc-puzzle-msg"></div>
1882
+ <button class="fc-puzzle-refresh">${t.refresh}</button>
1883
+ </div>
1884
+ `;
1885
+ const piece = container.querySelector(".fc-puzzle-piece");
1886
+ const range = container.querySelector(".fc-puzzle-range");
1887
+ const msg = container.querySelector(".fc-puzzle-msg");
1888
+ const refreshBtn = container.querySelector(".fc-puzzle-refresh");
1889
+ refreshBtn.addEventListener("click", () => render2());
1890
+ range.addEventListener("input", async () => {
1891
+ if (done) return;
1892
+ const v = Number(range.value);
1893
+ piece.style.left = `${v / 100 * MAX_OFFSET}px`;
1894
+ if (verifyPosition(current, v)) {
1895
+ done = true;
1896
+ range.disabled = true;
1897
+ piece.style.left = `${gapLeft}px`;
1898
+ const result = {
1899
+ success: true,
1900
+ proof: await hashProof(proofInput8(current)),
1901
+ duration: Date.now() - startTime
1902
+ };
1903
+ msg.textContent = t.success;
1904
+ config.onVerify?.(result);
1905
+ listeners.forEach((cb) => cb(result));
1906
+ }
1907
+ });
1908
+ }
1909
+ return {
1910
+ mount: () => render2(),
1911
+ reset: () => render2(),
1912
+ destroy: () => {
1913
+ container.innerHTML = "";
1914
+ listeners = [];
1915
+ },
1916
+ onResult: (cb) => listeners.push(cb)
1917
+ };
1918
+ }
1919
+ var puzzlePlugin = {
1920
+ id: "puzzle",
1921
+ category: "interactive",
1922
+ create: createPuzzleInstance,
1923
+ describe: (locale) => ({
1924
+ name: locale === "zh" ? "\u62FC\u56FE\u7F3A\u53E3" : "Puzzle Gap",
1925
+ description: locale === "zh" ? "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3\u4F4D\u7F6E" : "Drag the slider to fit the puzzle piece into the gap.",
1926
+ tags: ["puzzle", "drag", "interactive"]
1927
+ })
1928
+ };
1929
+ defineCaptcha(puzzlePlugin);
1930
+
1333
1931
  // src/index.ts
1334
1932
  var instances = /* @__PURE__ */ new Map();
1335
1933
  function render(root = document) {