Replace the look of every page (key, lobby, create, table, range, review) with light/dark semantic colors, translucent materials, iOS-style grouped lists and system typography. Add fluid.js, a small spring engine with interruptible, velocity-aware animations, and use it for draggable bottom sheets, sliding segmented controls, a custom bet slider with rubber-banding, swipe-to-delete in the lobby, and dealt/flipped cards. Seats and cards are now reused between renders so animations are not cut off. Honors reduced motion, reduced transparency and increased contrast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
2.9 KiB
JavaScript
98 lines
2.9 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const form = document.getElementById('form');
|
|
const box = document.getElementById('digits');
|
|
const inputs = [...box.querySelectorAll('input')];
|
|
const errorEl = document.getElementById('error');
|
|
const submitBtn = document.getElementById('submit');
|
|
let busy = false;
|
|
|
|
// /key?admin … 入室済みの人が管理者キーを入れるための画面
|
|
if (new URLSearchParams(location.search).has('admin')) {
|
|
document.getElementById('key-title').textContent = '管理者キーを入力';
|
|
document.getElementById('key-hint').textContent = '管理者用の6桁の数字';
|
|
}
|
|
|
|
const value = () => inputs.map((i) => i.value).join('');
|
|
|
|
// 数字が入った枠は小さく弾む(空に戻ったら印を外す)
|
|
function markFilled() {
|
|
for (const i of inputs) i.classList.toggle('filled', Boolean(i.value));
|
|
}
|
|
|
|
function fill(digits, from) {
|
|
let i = from;
|
|
for (const d of digits) {
|
|
if (i >= inputs.length) break;
|
|
inputs[i++].value = d;
|
|
}
|
|
markFilled();
|
|
inputs[Math.min(i, inputs.length - 1)].focus();
|
|
if (value().length === 6) submit();
|
|
}
|
|
|
|
inputs.forEach((input, idx) => {
|
|
input.addEventListener('input', () => {
|
|
const digits = input.value.replace(/\D/g, '');
|
|
input.value = '';
|
|
if (digits) fill(digits, idx);
|
|
else markFilled();
|
|
errorEl.textContent = '';
|
|
});
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Backspace' && !input.value && idx > 0) {
|
|
inputs[idx - 1].value = '';
|
|
markFilled();
|
|
inputs[idx - 1].focus();
|
|
e.preventDefault();
|
|
} else if (e.key === 'ArrowLeft' && idx > 0) {
|
|
inputs[idx - 1].focus();
|
|
} else if (e.key === 'ArrowRight' && idx < inputs.length - 1) {
|
|
inputs[idx + 1].focus();
|
|
}
|
|
});
|
|
input.addEventListener('paste', (e) => {
|
|
const digits = (e.clipboardData.getData('text') || '').replace(/\D/g, '');
|
|
if (!digits) return;
|
|
e.preventDefault();
|
|
fill(digits, idx);
|
|
});
|
|
input.addEventListener('focus', () => input.select());
|
|
});
|
|
|
|
async function submit() {
|
|
if (busy) return;
|
|
const key = value();
|
|
if (key.length !== 6) {
|
|
errorEl.textContent = '6桁すべて入力してください。';
|
|
return;
|
|
}
|
|
busy = true;
|
|
submitBtn.disabled = true;
|
|
try {
|
|
await App.api('POST', '/api/auth', { key });
|
|
const next = new URLSearchParams(location.search).get('next');
|
|
location.replace(App.safeNext(next));
|
|
} catch (err) {
|
|
errorEl.textContent = err.message;
|
|
// 揺れと振動は同じ瞬間に(どちらも「違う」の合図)
|
|
Fluid.shake(box);
|
|
Fluid.haptic([18, 50, 18]);
|
|
inputs.forEach((i) => { i.value = ''; });
|
|
markFilled();
|
|
inputs[0].focus();
|
|
} finally {
|
|
busy = false;
|
|
submitBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
form.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
submit();
|
|
});
|
|
|
|
inputs[0].focus();
|
|
})();
|