@funnycaptcha/slider 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.
package/dist/index.js CHANGED
@@ -20,11 +20,12 @@ async function verifyCompleted(c, proof) {
20
20
 
21
21
  // src/render.ts
22
22
  var STR = {
23
- 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" },
24
- en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end" }
23
+ 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" },
24
+ en: { title: "Drag the slider to the right end", tip: "Drag right", success: "Verified", fail: "Drag to the end", refresh: "Refresh" }
25
25
  };
26
26
  function createSliderInstance(container, config) {
27
27
  const t = STR[config.locale];
28
+ const theme = config.theme ?? "light";
28
29
  let current;
29
30
  let listeners = [];
30
31
  let startTime = Date.now();
@@ -35,17 +36,26 @@ function createSliderInstance(container, config) {
35
36
  done = false;
36
37
  container.innerHTML = `
37
38
  <style>
38
- .fc-slider{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box}
39
- .fc-slider-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
40
- .fc-slider-track{position:relative;height:40px;background:#e8e8e8;border-radius:20px}
41
- .fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:#4a90d9;border-radius:20px;transition:width .05s}
42
- .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}
39
+ .fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
40
+ .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}
41
+ .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}
42
+ .fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
43
+ .fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
44
+ .fc-slider-track{position:relative;height:40px;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:20px}
45
+ .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}
46
+ .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}
43
47
  .fc-slider-handle:active{cursor:grabbing}
44
- .fc-slider-handle::after{content:'\\2192';color:#4a90d9;font-size:18px;font-weight:bold}
45
- .fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:#999;pointer-events:none;transition:opacity .2s}
46
- .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
48
+ .fc-slider-handle::after{content:'\\2192';color:var(--fc-accent);font-size:18px;font-weight:bold;transition:color .15s}
49
+ .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}
50
+ .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
51
+ .fc-slider-row{display:flex;justify-content:center;margin-top:10px}
52
+ .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}
53
+ .fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
54
+ .fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}
55
+ .fc-slider-done .fc-slider-handle::after{color:var(--fc-success)}
56
+ .fc-slider-done .fc-slider-progress{background:var(--fc-success)}
47
57
  </style>
48
- <div class="fc-slider">
58
+ <div class="fc-slider" data-theme="${theme}">
49
59
  <div class="fc-slider-title">${t.title}</div>
50
60
  <div class="fc-slider-track">
51
61
  <div class="fc-slider-progress"></div>
@@ -53,13 +63,19 @@ function createSliderInstance(container, config) {
53
63
  <span class="fc-slider-tip">${t.tip}</span>
54
64
  </div>
55
65
  <div class="fc-slider-msg"></div>
66
+ <div class="fc-slider-row">
67
+ <button class="fc-slider-refresh">${t.refresh}</button>
68
+ </div>
56
69
  </div>
57
70
  `;
71
+ const root = container.querySelector(".fc-slider");
58
72
  const track = container.querySelector(".fc-slider-track");
59
73
  const handle = container.querySelector(".fc-slider-handle");
60
74
  const progress = container.querySelector(".fc-slider-progress");
61
75
  const tip = container.querySelector(".fc-slider-tip");
62
76
  const msg = container.querySelector(".fc-slider-msg");
77
+ const refreshBtn = container.querySelector(".fc-slider-refresh");
78
+ refreshBtn.addEventListener("click", render);
63
79
  let dragging = false;
64
80
  let startX = 0;
65
81
  let startOffset = 0;
@@ -95,6 +111,7 @@ function createSliderInstance(container, config) {
95
111
  if (currentOffset / maxX >= 0.95) {
96
112
  done = true;
97
113
  applyPos(maxX);
114
+ root.classList.add("fc-slider-done");
98
115
  const result = {
99
116
  success: true,
100
117
  proof: await hashProof2(proofInput(current)),
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createSliderInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const sliderPlugin: CaptchaPlugin = {\n id: 'slider',\n category: 'interactive',\n create: createSliderInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '滑动拼图' : 'Slider',\n description: locale === 'zh'\n ? '拖动滑块到终点,简单直接的交互验证'\n : 'Drag the slider to the end. Simple and direct.',\n tags: ['slider', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(sliderPlugin);\n\nexport function mountSlider(container: HTMLElement, config: CaptchaConfig) {\n return createSliderInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type SliderChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块到最右端', tip: '向右拖动', success: '验证成功', fail: '请拖到尽头' },\n en: { title: 'Drag the slider to the right end', tip: 'Drag right', success: 'Verified', fail: 'Drag to the end' },\n};\n\nexport function createSliderInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n let current: SliderChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n container.innerHTML = `\n <style>\n .fc-slider{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box}\n .fc-slider-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}\n .fc-slider-track{position:relative;height:40px;background:#e8e8e8;border-radius:20px}\n .fc-slider-progress{position:absolute;left:0;top:0;height:100%;width:0;background:#4a90d9;border-radius:20px;transition:width .05s}\n .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}\n .fc-slider-handle:active{cursor:grabbing}\n .fc-slider-handle::after{content:'\\\\2192';color:#4a90d9;font-size:18px;font-weight:bold}\n .fc-slider-tip{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;color:#999;pointer-events:none;transition:opacity .2s}\n .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}\n </style>\n <div class=\"fc-slider\">\n <div class=\"fc-slider-title\">${t.title}</div>\n <div class=\"fc-slider-track\">\n <div class=\"fc-slider-progress\"></div>\n <div class=\"fc-slider-handle\" role=\"slider\" tabindex=\"0\"></div>\n <span class=\"fc-slider-tip\">${t.tip}</span>\n </div>\n <div class=\"fc-slider-msg\"></div>\n </div>\n `;\n const track = container.querySelector('.fc-slider-track') as HTMLDivElement;\n const handle = container.querySelector('.fc-slider-handle') as HTMLDivElement;\n const progress = container.querySelector('.fc-slider-progress') as HTMLDivElement;\n const tip = container.querySelector('.fc-slider-tip') as HTMLSpanElement;\n const msg = container.querySelector('.fc-slider-msg') as HTMLDivElement;\n\n let dragging = false;\n let startX = 0;\n let startOffset = 0;\n let currentOffset = 0;\n\n function applyPos(px: number) {\n handle.style.left = `${px}px`;\n progress.style.width = `${px}px`;\n tip.style.opacity = px > 8 ? '0' : '1';\n }\n\n handle.addEventListener('pointerdown', (e) => {\n if (done) return;\n dragging = true;\n startX = e.clientX;\n startOffset = currentOffset;\n try { handle.setPointerCapture(e.pointerId); } catch { /* ignore */ }\n e.preventDefault();\n });\n\n handle.addEventListener('pointermove', (e) => {\n if (!dragging) return;\n const maxX = track.clientWidth - handle.offsetWidth;\n let px = startOffset + (e.clientX - startX);\n px = Math.max(0, Math.min(maxX, px));\n currentOffset = px;\n applyPos(px);\n });\n\n const finish = async () => {\n if (!dragging) return;\n dragging = false;\n const maxX = Math.max(1, track.clientWidth - handle.offsetWidth);\n if (currentOffset / maxX >= 0.95) {\n done = true;\n applyPos(maxX);\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 未到终点,弹回\n currentOffset = 0;\n handle.style.transition = 'left .3s';\n progress.style.transition = 'width .3s';\n applyPos(0);\n msg.textContent = t.fail;\n setTimeout(() => {\n handle.style.transition = '';\n progress.style.transition = '';\n if (!done) msg.textContent = '';\n }, 320);\n }\n };\n\n handle.addEventListener('pointerup', finish);\n handle.addEventListener('pointercancel', finish);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","import { hashProof } from '@funnycaptcha/core';\n\n// 滑动拼图题目:包含一个随机 token,用户拖到终点后用 token 构造 proof\nexport interface SliderChallenge {\n token: string;\n}\n\n// 生成随机 token(用于构造 proof)\nexport function generateChallenge(): SliderChallenge {\n const token = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return { token };\n}\n\n// 构造 proof 输入串:token + ':completed'\nexport function proofInput(c: SliderChallenge): string {\n return `${c.token}:completed`;\n}\n\n// 校验完成 proof:hashProof(token + ':completed') === proof\nexport async function verifyCompleted(c: SliderChallenge, proof: string): Promise<boolean> {\n const expected = await hashProof(proofInput(c));\n return expected === proof;\n}\n","import { generateChallenge, verifyCompleted, type SliderChallenge } from './challenge.js';\n\nexport function issueSliderChallenge(): { challenge: SliderChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { token: challenge.token } };\n}\n\nexport async function verifySliderProof(\n challenge: SliderChallenge,\n proof: string,\n): Promise<boolean> {\n return verifyCompleted(challenge, proof);\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createSliderInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const sliderPlugin: CaptchaPlugin = {\n id: 'slider',\n category: 'interactive',\n create: createSliderInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '滑动拼图' : 'Slider',\n description: locale === 'zh'\n ? '拖动滑块到终点,简单直接的交互验证'\n : 'Drag the slider to the end. Simple and direct.',\n tags: ['slider', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(sliderPlugin);\n\nexport function mountSlider(container: HTMLElement, config: CaptchaConfig) {\n return createSliderInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type SliderChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块到最右端', tip: '向右拖动', success: '验证成功', fail: '请拖到尽头', refresh: '刷新' },\n en: { title: 'Drag the slider to the right end', tip: 'Drag right', success: 'Verified', fail: 'Drag to the end', refresh: 'Refresh' },\n};\n\nexport function createSliderInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n let current: SliderChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n container.innerHTML = `\n <style>\n .fc-slider{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .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}\n .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}\n .fc-slider{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-slider-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}\n .fc-slider-track{position:relative;height:40px;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:20px}\n .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}\n .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}\n .fc-slider-handle:active{cursor:grabbing}\n .fc-slider-handle::after{content:'\\\\2192';color:var(--fc-accent);font-size:18px;font-weight:bold;transition:color .15s}\n .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}\n .fc-slider-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}\n .fc-slider-row{display:flex;justify-content:center;margin-top:10px}\n .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}\n .fc-slider-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n .fc-slider-done .fc-slider-handle{border-color:var(--fc-success)}\n .fc-slider-done .fc-slider-handle::after{color:var(--fc-success)}\n .fc-slider-done .fc-slider-progress{background:var(--fc-success)}\n </style>\n <div class=\"fc-slider\" data-theme=\"${theme}\">\n <div class=\"fc-slider-title\">${t.title}</div>\n <div class=\"fc-slider-track\">\n <div class=\"fc-slider-progress\"></div>\n <div class=\"fc-slider-handle\" role=\"slider\" tabindex=\"0\"></div>\n <span class=\"fc-slider-tip\">${t.tip}</span>\n </div>\n <div class=\"fc-slider-msg\"></div>\n <div class=\"fc-slider-row\">\n <button class=\"fc-slider-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const root = container.querySelector('.fc-slider') as HTMLDivElement;\n const track = container.querySelector('.fc-slider-track') as HTMLDivElement;\n const handle = container.querySelector('.fc-slider-handle') as HTMLDivElement;\n const progress = container.querySelector('.fc-slider-progress') as HTMLDivElement;\n const tip = container.querySelector('.fc-slider-tip') as HTMLSpanElement;\n const msg = container.querySelector('.fc-slider-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-slider-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', render);\n\n let dragging = false;\n let startX = 0;\n let startOffset = 0;\n let currentOffset = 0;\n\n function applyPos(px: number) {\n handle.style.left = `${px}px`;\n progress.style.width = `${px}px`;\n tip.style.opacity = px > 8 ? '0' : '1';\n }\n\n handle.addEventListener('pointerdown', (e) => {\n if (done) return;\n dragging = true;\n startX = e.clientX;\n startOffset = currentOffset;\n try { handle.setPointerCapture(e.pointerId); } catch { /* ignore */ }\n e.preventDefault();\n });\n\n handle.addEventListener('pointermove', (e) => {\n if (!dragging) return;\n const maxX = track.clientWidth - handle.offsetWidth;\n let px = startOffset + (e.clientX - startX);\n px = Math.max(0, Math.min(maxX, px));\n currentOffset = px;\n applyPos(px);\n });\n\n const finish = async () => {\n if (!dragging) return;\n dragging = false;\n const maxX = Math.max(1, track.clientWidth - handle.offsetWidth);\n if (currentOffset / maxX >= 0.95) {\n done = true;\n applyPos(maxX);\n root.classList.add('fc-slider-done');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 未到终点,弹回\n currentOffset = 0;\n handle.style.transition = 'left .3s';\n progress.style.transition = 'width .3s';\n applyPos(0);\n msg.textContent = t.fail;\n setTimeout(() => {\n handle.style.transition = '';\n progress.style.transition = '';\n if (!done) msg.textContent = '';\n }, 320);\n }\n };\n\n handle.addEventListener('pointerup', finish);\n handle.addEventListener('pointercancel', finish);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","import { hashProof } from '@funnycaptcha/core';\n\n// 滑动拼图题目:包含一个随机 token,用户拖到终点后用 token 构造 proof\nexport interface SliderChallenge {\n token: string;\n}\n\n// 生成随机 token(用于构造 proof)\nexport function generateChallenge(): SliderChallenge {\n const token = Math.random().toString(36).slice(2) + Date.now().toString(36);\n return { token };\n}\n\n// 构造 proof 输入串:token + ':completed'\nexport function proofInput(c: SliderChallenge): string {\n return `${c.token}:completed`;\n}\n\n// 校验完成 proof:hashProof(token + ':completed') === proof\nexport async function verifyCompleted(c: SliderChallenge, proof: string): Promise<boolean> {\n const expected = await hashProof(proofInput(c));\n return expected === proof;\n}\n","import { generateChallenge, verifyCompleted, type SliderChallenge } from './challenge.js';\n\nexport function issueSliderChallenge(): { challenge: SliderChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { token: challenge.token } };\n}\n\nexport async function verifySliderProof(\n challenge: SliderChallenge,\n proof: string,\n): Promise<boolean> {\n return verifyCompleted(challenge, proof);\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnycaptcha/slider",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Slider captcha — FunnyChapter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@funnycaptcha/core": "0.2.0"
18
+ "@funnycaptcha/core": "0.3.0"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public",