Rebuild the UI in an Apple style with spring-driven motion
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>
This commit is contained in:
1 parent
d5ef2d310f
commit
c3fbfdd12e
18 files changed
+4877
-1278
No files matched your search
+51
-29
@@ -38,21 +38,25 @@
|
||||
return '$' + Number(n).toLocaleString('en-US');
|
||||
}
|
||||
|
||||
// 上から降りてきて、少ししたら同じ道を上へ戻る通知
|
||||
let toastTimer;
|
||||
function toast(msg) {
|
||||
let el = document.querySelector('.toast');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.className = 'toast';
|
||||
el.setAttribute('role', 'status');
|
||||
el.setAttribute('aria-live', 'polite');
|
||||
document.body.appendChild(el);
|
||||
void el.offsetWidth; // 最初の1回も画面の外から動かす
|
||||
}
|
||||
el.textContent = msg;
|
||||
el.hidden = false;
|
||||
el.classList.add('show');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { el.hidden = true; }, 2500);
|
||||
toastTimer = setTimeout(() => el.classList.remove('show'), 2600);
|
||||
}
|
||||
|
||||
// ブラウザ標準の confirm() の代わりの確認ダイアログ。OK なら true で解決する。
|
||||
// ブラウザ標準の confirm() の代わりの確認ダイアログ(iOS のアラートの形)。OK なら true で解決する。
|
||||
// opts: { title, message(改行可), ok, cancel, danger(OK を赤にする) }
|
||||
// 新しいダイアログを開くと、開いていたものはキャンセル扱いで閉じる
|
||||
let closeConfirm = null;
|
||||
@@ -61,22 +65,24 @@
|
||||
if (closeConfirm) closeConfirm(false);
|
||||
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'modal-backdrop confirm-backdrop';
|
||||
backdrop.className = 'alert-backdrop';
|
||||
backdrop.innerHTML =
|
||||
'<div class="modal confirm-modal" role="alertdialog" aria-modal="true">' +
|
||||
(o.title ? '<h2 class="confirm-title"></h2>' : '') +
|
||||
'<p class="confirm-message"></p>' +
|
||||
'<div class="confirm-buttons">' +
|
||||
'<button type="button" class="btn btn-dark" data-confirm="cancel"></button>' +
|
||||
`<button type="button" class="btn ${o.danger ? 'btn-red' : 'btn-orange'}" data-confirm="ok"></button>` +
|
||||
'<div class="alert" role="alertdialog" aria-modal="true">' +
|
||||
'<div class="alert-body">' +
|
||||
(o.title ? '<h2 class="alert-title"></h2>' : '') +
|
||||
'<p class="alert-message"></p>' +
|
||||
'</div>' +
|
||||
'<div class="alert-buttons">' +
|
||||
'<button type="button" class="alert-btn" data-confirm="cancel"></button>' +
|
||||
`<button type="button" class="alert-btn ${o.danger ? 'destructive' : 'default'}" data-confirm="ok"></button>` +
|
||||
'</div></div>';
|
||||
const dialog = backdrop.firstChild;
|
||||
const msgEl = dialog.querySelector('.confirm-message');
|
||||
const msgEl = dialog.querySelector('.alert-message');
|
||||
msgEl.textContent = o.message || '';
|
||||
msgEl.id = 'confirm-message-' + randomHex(4);
|
||||
dialog.setAttribute('aria-describedby', msgEl.id);
|
||||
if (o.title) {
|
||||
const titleEl = dialog.querySelector('.confirm-title');
|
||||
const titleEl = dialog.querySelector('.alert-title');
|
||||
titleEl.textContent = o.title;
|
||||
titleEl.id = 'confirm-title-' + randomHex(4);
|
||||
dialog.setAttribute('aria-labelledby', titleEl.id);
|
||||
@@ -86,13 +92,24 @@
|
||||
okBtn.textContent = o.ok || 'OK';
|
||||
cancelBtn.textContent = o.cancel || 'キャンセル';
|
||||
|
||||
// 現れるときは少し大きい所から縮み、消えるときはその場で薄くなる
|
||||
let closing = false;
|
||||
const appear = new Fluid.Spring(0, { precision: 0.002 });
|
||||
appear.onUpdate = (v) => {
|
||||
backdrop.style.setProperty('--appear', v.toFixed(3));
|
||||
dialog.style.opacity = v >= 1 ? '' : v.toFixed(3);
|
||||
dialog.style.transform = closing || Fluid.reducedMotion() ? '' : `scale(${(1.12 - 0.12 * v).toFixed(4)})`;
|
||||
};
|
||||
|
||||
const prevFocus = document.activeElement;
|
||||
return new Promise((resolve) => {
|
||||
function close(result) {
|
||||
if (closeConfirm !== close) return;
|
||||
closeConfirm = null;
|
||||
closing = true;
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
backdrop.remove();
|
||||
backdrop.style.pointerEvents = 'none';
|
||||
appear.to(0, { damping: 1, response: 0.2, allowReduced: true, onRest: () => backdrop.remove() });
|
||||
if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus();
|
||||
resolve(result);
|
||||
}
|
||||
@@ -109,15 +126,18 @@
|
||||
okBtn.addEventListener('click', () => close(true));
|
||||
cancelBtn.addEventListener('click', () => close(false));
|
||||
// 背景を押したらキャンセル(ダイアログ内から背景へのドラッグは除く)
|
||||
let downOnBackdrop = false;
|
||||
backdrop.addEventListener('pointerdown', (e) => {
|
||||
backdrop.dataset.downOnBackdrop = e.target === backdrop ? '1' : '';
|
||||
downOnBackdrop = e.target === backdrop;
|
||||
});
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
if (e.target === backdrop && backdrop.dataset.downOnBackdrop) close(false);
|
||||
if (e.target === backdrop && downOnBackdrop) close(false);
|
||||
});
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
closeConfirm = close;
|
||||
document.body.appendChild(backdrop);
|
||||
appear.set(0);
|
||||
appear.to(1, { damping: 1, response: 0.3, allowReduced: true });
|
||||
// 危険な操作はうっかり Enter で確定しないようキャンセルにフォーカスする
|
||||
(o.danger ? cancelBtn : okBtn).focus();
|
||||
});
|
||||
@@ -161,24 +181,26 @@
|
||||
}
|
||||
|
||||
function showCopyDialog(text) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'modal-backdrop confirm-backdrop';
|
||||
backdrop.innerHTML =
|
||||
'<div class="modal copy-modal" role="dialog" aria-modal="true">' +
|
||||
'<h2 class="confirm-title">コピーしてください</h2>' +
|
||||
const root = document.createElement('div');
|
||||
root.className = 'sheet-backdrop';
|
||||
root.hidden = true;
|
||||
root.innerHTML =
|
||||
'<div class="sheet" role="dialog" aria-modal="true" aria-label="コピーしてください" tabindex="-1">' +
|
||||
'<div class="sheet-header" data-sheet-drag><span class="sheet-grabber" aria-hidden="true"></span>' +
|
||||
'<h2 class="sheet-title">コピーしてください</h2></div>' +
|
||||
'<div class="sheet-scroll">' +
|
||||
'<p class="hint">自動でコピーできませんでした。下の文章を全選択してコピーしてください。</p>' +
|
||||
'<textarea class="copy-text" readonly></textarea>' +
|
||||
'<button type="button" class="btn btn-dark copy-close">閉じる</button>' +
|
||||
'</div>' +
|
||||
'<div class="sheet-footer"><button type="button" class="btn btn-lg btn-block copy-close">閉じる</button></div>' +
|
||||
'</div>';
|
||||
const area = backdrop.querySelector('textarea');
|
||||
const area = root.querySelector('textarea');
|
||||
area.value = text;
|
||||
const close = () => backdrop.remove();
|
||||
backdrop.querySelector('.copy-close').addEventListener('click', close);
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
if (e.target === backdrop) close();
|
||||
});
|
||||
document.body.appendChild(backdrop);
|
||||
area.focus();
|
||||
document.body.appendChild(root);
|
||||
const sheet = Fluid.Sheet(root, { onClosed: () => root.remove() });
|
||||
root.querySelector('.copy-close').addEventListener('click', () => sheet.close());
|
||||
sheet.open();
|
||||
area.focus({ preventScroll: true });
|
||||
area.select();
|
||||
area.scrollTop = 0; // select() で末尾までスクロールするので先頭に戻す
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user