@funnycaptcha/emoji-match 0.2.0 → 0.6.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 +25 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -38,11 +38,12 @@ function verifyAnswer(c, userPick) {
|
|
|
38
38
|
|
|
39
39
|
// src/render.ts
|
|
40
40
|
var STR = {
|
|
41
|
-
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" },
|
|
42
|
-
en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again" }
|
|
41
|
+
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" },
|
|
42
|
+
en: { title: "Pick the face matching the description", success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
|
|
43
43
|
};
|
|
44
44
|
function createEmojiMatchInstance(container, config) {
|
|
45
45
|
const t = STR[config.locale];
|
|
46
|
+
const theme = config.theme ?? "light";
|
|
46
47
|
let current;
|
|
47
48
|
let listeners = [];
|
|
48
49
|
let startTime = Date.now();
|
|
@@ -56,24 +57,36 @@ function createEmojiMatchInstance(container, config) {
|
|
|
56
57
|
).join("");
|
|
57
58
|
container.innerHTML = `
|
|
58
59
|
<style>
|
|
59
|
-
.fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;width:
|
|
60
|
-
.fc-emoji-match-
|
|
61
|
-
.fc-emoji-match-
|
|
60
|
+
.fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
|
|
61
|
+
.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}
|
|
62
|
+
.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}
|
|
63
|
+
.fc-emoji-match{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
|
|
64
|
+
.fc-emoji-match-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
|
|
65
|
+
.fc-emoji-match-desc{font-size:15px;color:var(--fc-accent);margin-bottom:14px;text-align:center;font-weight:600}
|
|
62
66
|
.fc-emoji-match-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
63
|
-
.fc-emoji-match-opt{font-size:30px;height:64px;border:2px solid
|
|
64
|
-
.fc-emoji-match-opt:hover{transform:scale(1.06);border-color
|
|
65
|
-
.fc-emoji-match-opt-wrong{border-color
|
|
66
|
-
.fc-emoji-match-opt-correct{border-color
|
|
67
|
-
.fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color
|
|
67
|
+
.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)}
|
|
68
|
+
.fc-emoji-match-opt:hover{transform:scale(1.06);border-color:var(--fc-accent)}
|
|
69
|
+
.fc-emoji-match-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}
|
|
70
|
+
.fc-emoji-match-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}
|
|
71
|
+
.fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
|
|
72
|
+
.fc-emoji-match-msg-ok{color:var(--fc-success)}
|
|
73
|
+
.fc-emoji-match-row{display:flex;justify-content:center;margin-top:8px}
|
|
74
|
+
.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}
|
|
75
|
+
.fc-emoji-match-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
|
|
68
76
|
</style>
|
|
69
|
-
<div class="fc-emoji-match">
|
|
77
|
+
<div class="fc-emoji-match" data-theme="${theme}">
|
|
70
78
|
<div class="fc-emoji-match-title">${t.title}</div>
|
|
71
79
|
<div class="fc-emoji-match-desc">${current.description}</div>
|
|
72
80
|
<div class="fc-emoji-match-opts">${buttons}</div>
|
|
73
81
|
<div class="fc-emoji-match-msg"></div>
|
|
82
|
+
<div class="fc-emoji-match-row">
|
|
83
|
+
<button class="fc-emoji-match-refresh">${t.refresh}</button>
|
|
84
|
+
</div>
|
|
74
85
|
</div>
|
|
75
86
|
`;
|
|
76
87
|
const msg = container.querySelector(".fc-emoji-match-msg");
|
|
88
|
+
const refreshBtn = container.querySelector(".fc-emoji-match-refresh");
|
|
89
|
+
refreshBtn.addEventListener("click", render);
|
|
77
90
|
container.querySelectorAll(".fc-emoji-match-opt").forEach((el) => {
|
|
78
91
|
el.addEventListener("click", async () => {
|
|
79
92
|
if (finished) return;
|
|
@@ -87,7 +100,7 @@ function createEmojiMatchInstance(container, config) {
|
|
|
87
100
|
proof: await hashProof(current.correct),
|
|
88
101
|
duration: Date.now() - startTime
|
|
89
102
|
};
|
|
90
|
-
msg.
|
|
103
|
+
msg.classList.add("fc-emoji-match-msg-ok");
|
|
91
104
|
msg.textContent = t.success;
|
|
92
105
|
config.onVerify?.(result);
|
|
93
106
|
listeners.forEach((cb) => cb(result));
|
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 { createEmojiMatchInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const emojiMatchPlugin: CaptchaPlugin = {\n id: 'emoji-match',\n category: 'creative',\n create: createEmojiMatchInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '表情匹配' : 'Emoji Match',\n description: locale === 'zh'\n ? '根据文字描述选出对应的表情 emoji'\n : 'Pick the emoji face that matches the description.',\n tags: ['emoji', 'match', 'creative'],\n }),\n};\n\ndefineCaptcha(emojiMatchPlugin);\n\nexport function mountEmojiMatch(container: HTMLElement, config: CaptchaConfig) {\n return createEmojiMatchInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, verifyAnswer, type EmojiMatchChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '选出符合描述的表情', success: '验证成功', fail: '选错了,再试一次' },\n en: { title: 'Pick the face matching the description', success: 'Verified', fail: 'Wrong, try again' },\n};\n\nexport function createEmojiMatchInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n let current: EmojiMatchChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let finished = false;\n\n function render() {\n current = generateChallenge(config.locale);\n finished = false;\n startTime = Date.now();\n const buttons = current.options.map(\n (e) => `<button class=\"fc-emoji-match-opt\" data-emoji=\"${e}\">${e}</button>`,\n ).join('');\n container.innerHTML = `\n <style>\n .fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}\n .fc-emoji-match-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}\n .fc-emoji-match-desc{font-size:15px;color:#4a90d9;margin-bottom:14px;text-align:center;font-weight:600}\n .fc-emoji-match-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}\n .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}\n .fc-emoji-match-opt:hover{transform:scale(1.06);border-color:#4a90d9}\n .fc-emoji-match-opt-wrong{border-color:#e53935;background:#fdecea}\n .fc-emoji-match-opt-correct{border-color:#2e7d32;background:#e8f5e9}\n .fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}\n </style>\n <div class=\"fc-emoji-match\">\n <div class=\"fc-emoji-match-title\">${t.title}</div>\n <div class=\"fc-emoji-match-desc\">${current.description}</div>\n <div class=\"fc-emoji-match-opts\">${buttons}</div>\n <div class=\"fc-emoji-match-msg\"></div>\n </div>\n `;\n const msg = container.querySelector('.fc-emoji-match-msg') as HTMLDivElement;\n\n container.querySelectorAll('.fc-emoji-match-opt').forEach(el => {\n el.addEventListener('click', async () => {\n if (finished) return;\n const btn = el as HTMLButtonElement;\n const pick = btn.dataset.emoji!;\n if (verifyAnswer(current, pick)) {\n finished = true;\n btn.classList.add('fc-emoji-match-opt-correct');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(current.correct),\n duration: Date.now() - startTime,\n };\n msg.style.color = '#2e7d32';\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 选错:高亮错误后换题\n finished = true;\n btn.classList.add('fc-emoji-match-opt-wrong');\n msg.textContent = t.fail;\n setTimeout(render, 800);\n }\n });\n });\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 type { Locale } from '@funnycaptcha/core';\n\n// 表情匹配题目:根据描述从 6 个 emoji 中选出正确的\nexport interface EmojiMatchChallenge {\n // 表情描述\n description: string;\n // 正确 emoji\n correct: string;\n // 6 个备选 emoji(含正确答案)\n options: string[];\n}\n\n// 预设表情数据\ninterface Preset {\n emoji: string;\n zh: string;\n en: string;\n}\n\nconst PRESETS: Preset[] = [\n { emoji: '😀', zh: '开心的表情', en: 'Happy face' },\n { emoji: '😢', zh: '伤心的表情', en: 'Sad face' },\n { emoji: '😠', zh: '生气的表情', en: 'Angry face' },\n { emoji: '😴', zh: '困倦的表情', en: 'Sleepy face' },\n { emoji: '🤔', zh: '思考的表情', en: 'Thinking face' },\n { emoji: '😍', zh: '爱慕的表情', en: 'Loving face' },\n];\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// Fisher-Yates 洗牌\nfunction shuffle<T>(arr: readonly T[]): T[] {\n const a = arr.slice();\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = a[i]!;\n a[i] = a[j]!;\n a[j] = tmp;\n }\n return a;\n}\n\n// 生成题目:随机选一个表情作为正确答案,6 个备选项为全部预设表情的乱序\nexport function generateChallenge(locale: Locale = 'zh'): EmojiMatchChallenge {\n const preset = PRESETS[randInt(0, PRESETS.length - 1)]!;\n const description = locale === 'zh' ? preset.zh : preset.en;\n // 备选项为全部 6 个预设表情打乱顺序\n const options = shuffle(PRESETS.map(p => p.emoji));\n return { description, correct: preset.emoji, options };\n}\n\n// 校验用户选择的 emoji 是否正确\nexport function verifyAnswer(c: EmojiMatchChallenge, userPick: string): boolean {\n return c.correct === userPick;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, type EmojiMatchChallenge } from './challenge.js';\n\n// 服务端签发题目(默认中文描述)\nexport function issueEmojiMatchChallenge(): { challenge: EmojiMatchChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { description: challenge.description, options: challenge.options } };\n}\n\n// 服务端校验 proof\nexport async function verifyEmojiMatchProof(\n challenge: EmojiMatchChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(challenge.correct);\n return expected === 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 { createEmojiMatchInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const emojiMatchPlugin: CaptchaPlugin = {\n id: 'emoji-match',\n category: 'creative',\n create: createEmojiMatchInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '表情匹配' : 'Emoji Match',\n description: locale === 'zh'\n ? '根据文字描述选出对应的表情 emoji'\n : 'Pick the emoji face that matches the description.',\n tags: ['emoji', 'match', 'creative'],\n }),\n};\n\ndefineCaptcha(emojiMatchPlugin);\n\nexport function mountEmojiMatch(container: HTMLElement, config: CaptchaConfig) {\n return createEmojiMatchInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, verifyAnswer, type EmojiMatchChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '选出符合描述的表情', success: '验证成功', fail: '选错了,再试一次', refresh: '刷新' },\n en: { title: 'Pick the face matching the description', success: 'Verified', fail: 'Wrong, try again', refresh: 'Refresh' },\n};\n\nexport function createEmojiMatchInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n let current: EmojiMatchChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let finished = false;\n\n function render() {\n current = generateChallenge(config.locale);\n finished = false;\n startTime = Date.now();\n const buttons = current.options.map(\n (e) => `<button class=\"fc-emoji-match-opt\" data-emoji=\"${e}\">${e}</button>`,\n ).join('');\n container.innerHTML = `\n <style>\n .fc-emoji-match{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .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}\n .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}\n .fc-emoji-match{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-emoji-match-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}\n .fc-emoji-match-desc{font-size:15px;color:var(--fc-accent);margin-bottom:14px;text-align:center;font-weight:600}\n .fc-emoji-match-opts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}\n .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)}\n .fc-emoji-match-opt:hover{transform:scale(1.06);border-color:var(--fc-accent)}\n .fc-emoji-match-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}\n .fc-emoji-match-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}\n .fc-emoji-match-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}\n .fc-emoji-match-msg-ok{color:var(--fc-success)}\n .fc-emoji-match-row{display:flex;justify-content:center;margin-top:8px}\n .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}\n .fc-emoji-match-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n </style>\n <div class=\"fc-emoji-match\" data-theme=\"${theme}\">\n <div class=\"fc-emoji-match-title\">${t.title}</div>\n <div class=\"fc-emoji-match-desc\">${current.description}</div>\n <div class=\"fc-emoji-match-opts\">${buttons}</div>\n <div class=\"fc-emoji-match-msg\"></div>\n <div class=\"fc-emoji-match-row\">\n <button class=\"fc-emoji-match-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const msg = container.querySelector('.fc-emoji-match-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-emoji-match-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', render);\n\n container.querySelectorAll('.fc-emoji-match-opt').forEach(el => {\n el.addEventListener('click', async () => {\n if (finished) return;\n const btn = el as HTMLButtonElement;\n const pick = btn.dataset.emoji!;\n if (verifyAnswer(current, pick)) {\n finished = true;\n btn.classList.add('fc-emoji-match-opt-correct');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(current.correct),\n duration: Date.now() - startTime,\n };\n msg.classList.add('fc-emoji-match-msg-ok');\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 选错:高亮错误后换题\n finished = true;\n btn.classList.add('fc-emoji-match-opt-wrong');\n msg.textContent = t.fail;\n setTimeout(render, 800);\n }\n });\n });\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 type { Locale } from '@funnycaptcha/core';\n\n// 表情匹配题目:根据描述从 6 个 emoji 中选出正确的\nexport interface EmojiMatchChallenge {\n // 表情描述\n description: string;\n // 正确 emoji\n correct: string;\n // 6 个备选 emoji(含正确答案)\n options: string[];\n}\n\n// 预设表情数据\ninterface Preset {\n emoji: string;\n zh: string;\n en: string;\n}\n\nconst PRESETS: Preset[] = [\n { emoji: '😀', zh: '开心的表情', en: 'Happy face' },\n { emoji: '😢', zh: '伤心的表情', en: 'Sad face' },\n { emoji: '😠', zh: '生气的表情', en: 'Angry face' },\n { emoji: '😴', zh: '困倦的表情', en: 'Sleepy face' },\n { emoji: '🤔', zh: '思考的表情', en: 'Thinking face' },\n { emoji: '😍', zh: '爱慕的表情', en: 'Loving face' },\n];\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// Fisher-Yates 洗牌\nfunction shuffle<T>(arr: readonly T[]): T[] {\n const a = arr.slice();\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = a[i]!;\n a[i] = a[j]!;\n a[j] = tmp;\n }\n return a;\n}\n\n// 生成题目:随机选一个表情作为正确答案,6 个备选项为全部预设表情的乱序\nexport function generateChallenge(locale: Locale = 'zh'): EmojiMatchChallenge {\n const preset = PRESETS[randInt(0, PRESETS.length - 1)]!;\n const description = locale === 'zh' ? preset.zh : preset.en;\n // 备选项为全部 6 个预设表情打乱顺序\n const options = shuffle(PRESETS.map(p => p.emoji));\n return { description, correct: preset.emoji, options };\n}\n\n// 校验用户选择的 emoji 是否正确\nexport function verifyAnswer(c: EmojiMatchChallenge, userPick: string): boolean {\n return c.correct === userPick;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, type EmojiMatchChallenge } from './challenge.js';\n\n// 服务端签发题目(默认中文描述)\nexport function issueEmojiMatchChallenge(): { challenge: EmojiMatchChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { description: challenge.description, options: challenge.options } };\n}\n\n// 服务端校验 proof\nexport async function verifyEmojiMatchProof(\n challenge: EmojiMatchChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(challenge.correct);\n return expected === 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/emoji-match",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Emoji-match 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.
|
|
18
|
+
"@funnycaptcha/core": "0.6.0"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public",
|