From ed3d1e437e67fd0f5054289e7538658b2a4baea1 Mon Sep 17 00:00:00 2001 From: KoyoNoguchi Date: Fri, 23 Oct 2026 12:19:10 +0900 Subject: [PATCH] Show the seating editor as a centered popup Add Fluid.Popup, a centered dialog with the same open/close API as Sheet: it scales in like the confirm alert, fades out in place, and closes on backdrop tap or Escape with focus trapped inside. The host's seating editor now uses it instead of a bottom sheet; its list scrolls inside the popup on short screens. Co-Authored-By: Claude Opus 5.5 --- public/assets/fluid.js | 82 +++++++++++++++++++++++++++++++++++++++++ public/assets/style.css | 38 +++++++++++++++++++ public/assets/table.js | 16 ++++---- public/table.html | 9 ++--- 4 files changed, 132 insertions(+), 13 deletions(-) diff --git a/public/assets/fluid.js b/public/assets/fluid.js index 62f15e9..be75972 100644 --- a/public/assets/fluid.js +++ b/public/assets/fluid.js @@ -4,6 +4,7 @@ // 画面上のいまの位置と速度から続くので、動いている最中につかんだり戻したりできる // - VelocityTracker / project / rubberband: 指の速度・勢いから止まる位置の先読み・端での抵抗 // - Sheet: 下から出るシート(つまみを下へ払うと閉じる) +// - Popup: 画面の中央に出るポップアップ(背景を押す・Esc で閉じる) // - Segmented: つまみが滑るセグメント(つまみをドラッグしても選べる) // - presence / countTo / shake / haptic / navBar: 出し入れ・数字の変化・揺れ・振動・ナビゲーションバー // CSP でインラインスタイルを禁止しているので、動きはすべて CSSOM(el.style)で付ける @@ -304,6 +305,86 @@ }; } + // ---------- ポップアップ ---------- + // root は背景(.popup-backdrop)、中に .popup。画面の中央に少し大きい所から縮んで現れ、閉じるときはその場で薄くなる + // (App.confirm のアラートと同じ動き)。背景を押す・Esc で閉じる。 + // opts: { focus(開いたときにフォーカスする要素のセレクタ), onClose, onClosed } + function Popup(root, opts = {}) { + const box = root.querySelector('.popup'); + const appear = new Spring(0, { precision: 0.002 }); + let isOpen = false; + let closing = false; + let prevFocus = null; + let downOnBackdrop = false; + + appear.onUpdate = (v) => { + root.style.setProperty('--appear', v.toFixed(3)); + box.style.opacity = v >= 1 ? '' : v.toFixed(3); + box.style.transform = closing || v >= 1 || reducedMotion() ? '' : `scale(${(1.08 - 0.08 * v).toFixed(4)})`; + }; + + function onKey(e) { + if (e.key === 'Escape') { + e.preventDefault(); + close(); + } else if (e.key === 'Tab') { + trapFocus(box, e); + } + } + + function open() { + if (isOpen) return; + isOpen = true; + closing = false; + if (root.hidden) { + root.hidden = false; + appear.set(0); + } + root.style.pointerEvents = ''; + appear.to(1, { damping: 1, response: 0.32, allowReduced: true }); + prevFocus = document.activeElement; + document.addEventListener('keydown', onKey); + const target = opts.focus ? box.querySelector(opts.focus) : null; + (target || box).focus({ preventScroll: true }); + } + + function close() { + if (!isOpen) return; + isOpen = false; + closing = true; + document.removeEventListener('keydown', onKey); + root.style.pointerEvents = 'none'; // 消えている途中の背景で次の操作を止めない + appear.to(0, { + damping: 1, + response: 0.2, + allowReduced: true, + onRest: () => { + if (isOpen) return; + root.hidden = true; + if (opts.onClosed) opts.onClosed(); + }, + }); + if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus({ preventScroll: true }); + if (opts.onClose) opts.onClose(); + } + + // 背景を押したら閉じる(ポップアップの中から背景へのドラッグは除く) + root.addEventListener('pointerdown', (e) => { + downOnBackdrop = e.target === root; + }); + root.addEventListener('click', (e) => { + if (e.target === root && downOnBackdrop) close(); + }); + + return { + open, + close, + get isOpen() { + return isOpen; + }, + }; + } + // ---------- セグメント ---------- // container の子(label > input[type=radio] か、.active の付いた button)を選ぶつまみを滑らせる。 // 選ばれている所を押すとつまみが少し縮み、そのまま横へドラッグすると指に付いてくる。 @@ -548,6 +629,7 @@ clamp, reducedMotion, Sheet, + Popup, Segmented, presence, countTo, diff --git a/public/assets/style.css b/public/assets/style.css index ea26334..6e8d1cc 100644 --- a/public/assets/style.css +++ b/public/assets/style.css @@ -1026,6 +1026,44 @@ a { letter-spacing: -0.015em; } +/* ---------- ポップアップ(fluid.js の Popup): 画面の中央に出る大きめのダイアログ ---------- */ +.popup-backdrop { + --appear: 1; + position: fixed; + inset: 0; + z-index: 30; + display: grid; + place-items: center; + padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom)); +} +/* 暗さは現れ方に合わせて変わる */ +.popup-backdrop::before { + content: ""; + position: absolute; + inset: 0; + background: var(--scrim); + opacity: var(--appear); +} +.popup { + --bg-2: var(--sheet-cell); + position: relative; + width: min(440px, 100%); + max-height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 22px; + background: var(--sheet-bg); + box-shadow: var(--shadow-l), inset 0 0 0 0.5px var(--material-edge); + outline: none; + will-change: transform, opacity; +} +.popup-header { + flex: none; + padding: 16px 20px 6px; + text-align: center; +} + /* ---------- アラート(App.confirm): 少し大きい所から現れ、その場で消える ---------- */ .alert-backdrop { --appear: 1; diff --git a/public/assets/table.js b/public/assets/table.js index 30311f0..af18ef0 100644 --- a/public/assets/table.js +++ b/public/assets/table.js @@ -31,7 +31,7 @@ const reviewSheet = Fluid.Sheet($('review-modal')); const rankingSheet = Fluid.Sheet($('ranking-modal')); - const seatingSheet = Fluid.Sheet($('seating-modal')); + const seatingPopup = Fluid.Popup($('seating-modal')); const nameSheet = Fluid.Sheet($('name-modal'), { dismissible: false, focus: '#name-input' }); // 払って閉じた・背景を押した・Esc のときも「戻る」と同じ const betSheet = Fluid.Sheet($('bet-modal'), { @@ -120,7 +120,7 @@ $('action-bar').hidden = true; reviewSheet.close(); rankingSheet.close(); - seatingSheet.close(); + seatingPopup.close(); raising = false; betSheet.close(); $('not-found').hidden = false; @@ -268,7 +268,7 @@ else betSheet.close(); renderReview(s); if (rankingSheet.isOpen) renderRanking(s); - if (seatingSheet.isOpen) syncSeating(s); + if (seatingPopup.isOpen) syncSeating(s); renderTimer(); // 自分の番になったら知らせる @@ -774,7 +774,7 @@ // 開いている間に座った人は最後に加え、いなくなった人は外す(並べた順は崩さない)。 // 次のハンドで席番号が振り直されたら読み込み直す function syncSeating(s) { - if (!s.isHost) return seatingSheet.close(); + if (!s.isHost) return seatingPopup.close(); if (seatDrag) return; const occupied = occupiedSeats(s); const renumbered = @@ -962,10 +962,10 @@ $('seating-open').addEventListener('click', () => { if (!state || !state.isHost) return; loadSeating(state); - renderSeating(null); // 中身を入れてから開く(シートの高さを測るため) - seatingSheet.open(); + renderSeating(null); + seatingPopup.open(); }); - $('seating-cancel').addEventListener('click', () => seatingSheet.close()); + $('seating-cancel').addEventListener('click', () => seatingPopup.close()); $('seating-save').addEventListener('click', () => { if (!socket || !state) return; const later = Boolean(state.hand); // ハンドがある間(結果の表示中を含む)は次のハンドから @@ -978,7 +978,7 @@ if (state) syncSeating(state); return; } - seatingSheet.close(); + seatingPopup.close(); toast(later ? '次のハンドから新しい席順になります' : '席順を変更しました'); }); }); diff --git a/public/table.html b/public/table.html index 24bc5f6..c021c74 100644 --- a/public/table.html +++ b/public/table.html @@ -157,11 +157,10 @@ - -