From c3fbfdd12eacbde6447b732acbe7c8c59948f882 Mon Sep 17 00:00:00 2001 From: KoyoNoguchi Date: Wed, 21 Oct 2026 21:44:21 +0900 Subject: [PATCH] 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 --- README.md | 3 +- admin/range.css | 231 ++- admin/range.html | 34 +- admin/range.js | 12 +- admin/review.css | 561 +++++-- admin/review.html | 33 +- admin/review.js | 23 +- public/assets/common.js | 80 +- public/assets/create.js | 22 +- public/assets/fluid.js | 542 ++++++ public/assets/key.js | 15 +- public/assets/lobby.js | 219 ++- public/assets/style.css | 3451 +++++++++++++++++++++++++++++++-------- public/assets/table.js | 507 ++++-- public/create.html | 169 +- public/key.html | 22 +- public/lobby.html | 77 +- public/table.html | 154 +- 18 files changed, 4877 insertions(+), 1278 deletions(-) create mode 100644 public/assets/fluid.js diff --git a/README.md b/README.md index 1ae5fba..63bb40b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ http://localhost:4671 を開く。 ## 管理者(ハンドレンジ学習・レビュー付きCPU戦) -`ADMIN_KEY` を入力して入室すると管理者になり、ロビーに「♠ ハンドレンジ学習」(`/range`:レンジ表と色・分類クイズ。レンジ表には CO のオープンを BTN で受けたときの対応も枠で示す。3bet は水色、コールは黄)と「📝 CPU戦のレビュー」(`/review`)が表示され、テーブル作成で「レビュー付きCPU戦」を選べる。ロビーの各テーブルには「削除」ボタンが出て、どのテーブルでも削除できる(着席者・観戦者には「管理者がテーブルを削除しました」と表示される)。管理者は通常のプレイもそのままできる。 +`ADMIN_KEY` を入力して入室すると管理者になり、ロビーに「ハンドレンジ学習」(`/range`:レンジ表と色・分類クイズ。レンジ表には CO のオープンを BTN で受けたときの対応も枠で示す。3bet は水色、コールは黄)と「CPU戦のレビュー」(`/review`)が表示され、テーブル作成で「レビュー付きCPU戦」を選べる。ロビーのテーブルの行を左へスワイプすると「削除」が出て(キーボードでは Tab で削除ボタンに移ると開く)、どのテーブルでも削除できる(着席者・観戦者には「管理者がテーブルを削除しました」と表示される)。管理者は通常のプレイもそのままできる。 ### レビュー付きCPU戦 @@ -117,6 +117,7 @@ http://localhost:4671 を開く。 | `server/config.js` | `.env` の読み込みと各種上限値 | | `public/assets/blinds.js` | ブラインド計算(サーバ・ブラウザ共用) | | `public/*.html`, `public/assets/*.js` | 各画面 | +| `public/assets/style.css`, `public/assets/fluid.js` | 見た目の土台(light / dark の色・半透明の素材・字の大きさ)と、動きの共通部品(ばね・下から出るシート・滑るセグメント・指の速度と勢いの先読み) | | `admin/` | 管理者専用ページ(ハンドレンジ学習・CPU戦のレビュー・登録してあるハンドレンジ・カード画像) | - テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が30分続くと自動削除。 diff --git a/admin/range.css b/admin/range.css index 97bce7a..f76d846 100644 --- a/admin/range.css +++ b/admin/range.css @@ -1,22 +1,8 @@ -/* ハンドレンジ学習(管理者専用)。基本のデザインは /assets/style.css を使う */ +/* ハンドレンジ学習(管理者専用)。基本のデザインとセグメントは /assets/style.css を使う */ -.range-wrap { padding-bottom: 32px; } - -/* タブ */ -.seg { display: flex; gap: 6px; background: var(--panel); border-radius: 12px; padding: 4px; margin: 12px 0 16px; } -.seg-btn { - flex: 1; - font: inherit; - font-size: 15px; - font-weight: 800; - border: 0; - border-radius: 9px; - padding: 10px 6px; - background: transparent; - color: var(--muted); - cursor: pointer; +.range-tabs { + margin: 0 0 16px; } -.seg-btn.active { background: var(--panel-2); color: var(--text); } /* レンジ表の分類ごとの色(レンジ表のデータとしての色なのでサイトの配色とは別) */ .cat-navy { --c: #20365d; --fg: #fff; } @@ -30,57 +16,198 @@ /* レイズを受けたときの対応の枠(3bet は水色、コールは黄)。下地の色と紛れないよう内側に暗い線を添える */ .facing-3bet { --frame: #3fd4ff; } .facing-call { --frame: #ffe23a; } -.range-cell.facing-3bet, .range-cell.facing-call { box-shadow: inset 0 0 0 3px var(--frame), inset 0 0 0 4px rgba(0, 0, 0, 0.45); } +.range-cell.facing-3bet, +.range-cell.facing-call { + box-shadow: inset 0 0 0 3px var(--frame), inset 0 0 0 4px rgba(0, 0, 0, 0.45); +} /* 13×13 の表 */ -.range-matrix { display: grid; grid-template-columns: repeat(13, 1fr); gap: 2px; } +.matrix-card { + padding: 6px; + border-radius: 16px; + background: var(--bg-2); +} +.range-matrix { + display: grid; + grid-template-columns: repeat(13, 1fr); + gap: 2px; +} .range-cell { - aspect-ratio: 1; + position: relative; min-width: 0; + aspect-ratio: 1; padding: 0; border: 0; - border-radius: 3px; + border-radius: 4px; background: var(--c); color: var(--fg); - font: inherit; font-size: clamp(8px, 2.4vw, 12px); - font-weight: 800; + font-weight: 700; letter-spacing: -0.04em; cursor: pointer; - position: relative; + transition: transform 0.45s var(--ease-spring); +} +.range-cell:active { + transform: scale(0.9); + transition: transform 0.1s ease-out; +} +.range-cell.selected { + z-index: 1; + outline: 2px solid var(--label); + outline-offset: 1px; + transform: scale(1.12); } -.range-cell.selected { outline: 2px solid var(--gold); outline-offset: 1px; z-index: 1; } /* 凡例 */ -.range-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 12px; font-size: 12px; color: var(--muted); } -.range-legend span { display: inline-flex; align-items: center; gap: 6px; } -.swatch-dot { width: 14px; height: 14px; border-radius: 4px; background: var(--c); flex: none; } -.swatch-dot.swatch-frame { background: transparent; box-shadow: inset 0 0 0 3px var(--frame); } +.range-legend { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin: 12px 8px 0; + color: var(--label-2); + font-size: 0.75rem; +} +.range-legend span { + display: inline-flex; + align-items: center; + gap: 6px; +} +.swatch-dot { + width: 14px; + height: 14px; + flex: none; + border-radius: 4px; + background: var(--c); + box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.2); +} +.swatch-dot.swatch-frame { + background: transparent; + box-shadow: inset 0 0 0 3px var(--frame); +} /* 選択したハンド */ -.sel-hand { font-size: 28px; font-weight: 900; line-height: 1.1; } -.sel-label { display: flex; align-items: center; gap: 8px; font-weight: 800; margin-top: 6px; } +.selected-card { + margin-top: 16px; +} +.sel-hand { + font-size: 1.75rem; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; +} +.sel-label { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; + font-weight: 600; +} +.selected-card .hint { + margin-top: 6px; +} /* クイズ */ -.quiz-progress { height: 6px; background: var(--input); border-radius: 999px; overflow: hidden; margin: 16px 0 8px; } -.quiz-progress i { display: block; height: 100%; width: 0; background: var(--green); transition: width 0.2s; } -.quiz-stats { display: flex; justify-content: center; gap: 12px; font-size: 13px; color: var(--muted); } -.quiz-hand { text-align: center; margin: 12px 0; padding: 18px 12px; } -.quiz-cards { display: flex; justify-content: center; gap: 10px; min-height: 0; } -.quiz-cards img { width: min(32%, 120px); height: auto; filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4)); } -.quiz-hand-name { font-size: 40px; font-weight: 900; margin-top: 8px; line-height: 1.1; } -.quiz-answers { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } -.quiz-answers .btn { - font-size: 15px; - padding: 12px 8px; - justify-content: flex-start; - background: var(--panel-2); - color: var(--text); - border: 2px solid transparent; +.quiz-progress { + height: 6px; + margin: 18px 0 8px; + overflow: hidden; + border-radius: 999px; + background: var(--fill-2); +} +.quiz-progress i { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: var(--tint); + transition: width 0.5s var(--ease-spring); +} +.quiz-stats { + display: flex; + justify-content: center; + gap: 14px; + color: var(--label-2); + font-size: 0.8125rem; + font-variant-numeric: tabular-nums; +} +.quiz-hand { + margin: 14px 0; + padding: 20px 12px; + text-align: center; +} +.quiz-cards { + display: flex; + justify-content: center; + gap: 10px; +} +.quiz-cards img { + width: min(32%, 120px); + height: auto; + filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.3)); + animation: card-in 0.55s var(--ease-spring) both; +} +.quiz-cards img + img { + animation-delay: 70ms; +} +@keyframes card-in { + from { + opacity: 0; + transform: translateY(-14px) scale(0.92) rotate(-4deg); + } +} +.quiz-hand-name { + margin-top: 10px; + font-size: 2.5rem; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.025em; +} +.quiz-answers { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} +.quiz-answers .btn { + justify-content: flex-start; + padding: 12px 12px; + background: var(--bg-2); + color: var(--label); + font-size: 0.9375rem; + box-shadow: inset 0 0 0 0 transparent; + transition: transform 0.45s var(--ease-spring), box-shadow 0.25s ease, background-color 0.25s ease; +} +.quiz-answers .btn:disabled { + opacity: 1; +} +.quiz-answers .btn.correct { + background: var(--tint-soft); + box-shadow: inset 0 0 0 2px var(--tint); +} +.quiz-answers .btn.wrong { + background: rgba(255, 59, 48, 0.12); + box-shadow: inset 0 0 0 2px var(--red); +} +.quiz-feedback { + margin-top: 12px; + padding: 12px 14px; + border-radius: 14px; + background: var(--bg-2); + font-size: 0.9375rem; + animation: fade-up 0.45s var(--ease-spring) both; +} +.quiz-feedback.good { + box-shadow: inset 0 0 0 2px var(--tint); +} +.quiz-feedback.bad { + box-shadow: inset 0 0 0 2px var(--red); +} + +@media (prefers-reduced-motion: reduce) { + .quiz-cards img, + .quiz-feedback { + animation: fade-in 0.2s ease both; + } + .range-cell.selected { + transform: none; + } } -.quiz-answers .btn:disabled { opacity: 1; } -.quiz-answers .btn.correct { border-color: var(--green); background: rgba(46, 204, 113, 0.18); } -.quiz-answers .btn.wrong { border-color: var(--red); background: rgba(255, 90, 90, 0.15); } -.quiz-feedback { margin-top: 12px; padding: 12px 14px; border-radius: 10px; background: var(--panel); font-size: 14px; } -.quiz-feedback.good { box-shadow: inset 0 0 0 2px var(--green); } -.quiz-feedback.bad { box-shadow: inset 0 0 0 2px var(--red); } diff --git a/admin/range.html b/admin/range.html index 0db752c..8316551 100644 --- a/admin/range.html +++ b/admin/range.html @@ -3,42 +3,49 @@ - + + ハンドレンジ - Hold'em Night - -
-
- ← -

ハンドレンジ

+ + -
+
+

ハンドレンジ

+ +
-
+
+
+
-
+
—
セルをタップすると分類を確認できます
- +
+ diff --git a/admin/range.js b/admin/range.js index 147563e..638a564 100644 --- a/admin/range.js +++ b/admin/range.js @@ -5,6 +5,10 @@ const $ = (id) => document.getElementById(id); const { RANKS, CATEGORIES, FACING_ACTIONS, handAt, categoryOf, labelOf, facingOf, facingAction } = window.HandRange; + Fluid.navBar($('nav'), $('large-title')); + const tabsSeg = Fluid.Segmented($('tabs')); + Fluid.Segmented($('count')); + function swatch(id) { const s = document.createElement('span'); s.className = `swatch-dot cat-${id}`; @@ -22,7 +26,8 @@ function showView(name) { $('view-range').hidden = name !== 'range'; $('view-quiz').hidden = name !== 'quiz'; - for (const b of $('tabs').children) b.classList.toggle('active', b.dataset.view === name); + for (const b of $('tabs').querySelectorAll('[data-view]')) b.classList.toggle('active', b.dataset.view === name); + tabsSeg.update(); } $('tabs').addEventListener('click', (e) => { const b = e.target.closest('[data-view]'); @@ -99,7 +104,7 @@ const b = e.target.closest('[data-count]'); if (!b) return; quizCount = Number(b.dataset.count); - for (const x of $('count').children) x.classList.toggle('active', x === b); + for (const x of $('count').querySelectorAll('[data-count]')) x.classList.toggle('active', x === b); }); function shuffle(a) { @@ -186,6 +191,9 @@ fb.className = `quiz-feedback ${ok ? 'good' : 'bad'}`; fb.textContent = `${ok ? '正解!' : '不正解'} ${hand} は「${labelOf(right)}」です。`; fb.hidden = false; + // 間違えたら選んだボタンが揺れる(振動と同じ瞬間に) + if (!ok) Fluid.shake(button); + Fluid.haptic(ok ? 10 : [18, 50, 18]); updateStats(); $('next-question').disabled = false; } diff --git a/admin/review.css b/admin/review.css index 85a9296..93005e6 100644 --- a/admin/review.css +++ b/admin/review.css @@ -1,146 +1,465 @@ -/* CPU戦のレビュー(管理者専用)。基本のデザインは /assets/style.css、タブは range.css、 +/* CPU戦のレビュー(管理者専用)。基本のデザイン・セグメント・リストは /assets/style.css、 ハンドレンジの色・点数の印(.rv-color / .rv-mark など)は style.css の「レビュー付きCPU戦」を使う */ -.review-wrap { padding-bottom: 40px; } -.plus { color: var(--green); } -.minus { color: var(--red); } - /* 点数の色(◎ ○ △ ✕ と同じ段階) */ .score-great { color: var(--green); } -.score-good { color: #8fd18f; } +.score-good { color: #64b96a; } .score-warn { color: var(--orange); } .score-bad { color: var(--red); } -.score-none { color: var(--muted); } +.score-none { color: var(--label-2); } /* 対局の一覧 */ -.rv-sessions { display: grid; gap: 8px; margin-top: 12px; } -.rv-session { - display: grid; - gap: 4px; - text-align: left; - font: inherit; - color: var(--text); - background: var(--panel); - border: 0; - border-radius: var(--radius); - padding: 14px 16px; - cursor: pointer; +.rv-sessions { + margin-bottom: 28px; +} +.rv-session { + -webkit-appearance: none; + appearance: none; + width: 100%; + border: 0; + text-align: left; +} +.rv-session-name { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; } -.rv-session:hover, .rv-session:focus-visible { background: var(--panel-2); } -.rv-session-name { font-weight: 800; font-size: 16px; display: flex; align-items: center; gap: 8px; } -.rv-session-meta { color: var(--muted); font-size: 12px; } -/* 概要とタイル */ -.rv-overview { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; } -.rv-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; } -.rv-tile { background: var(--panel); border-radius: 12px; padding: 10px 12px; display: grid; gap: 2px; min-width: 0; } -.rv-tile span { font-size: 12px; color: var(--muted); font-weight: 700; } -.rv-tile b { font-size: 22px; font-variant-numeric: tabular-nums; } -.rv-tile small { font-size: 11px; color: var(--muted); line-height: 1.4; } -.rv-tabs .seg-btn { font-size: 13px; padding: 9px 2px; } +/* 概要とタイル(ウィジェットのような小さな面) */ +.rv-overview { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-bottom: 16px; +} +.rv-tiles { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 8px; +} +.rv-tile { + display: grid; + gap: 2px; + min-width: 0; + padding: 12px 14px; + border-radius: 16px; + background: var(--bg-2); +} +.rv-tile span { + color: var(--label-2); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.01em; +} +.rv-tile b { + font-family: var(--font-num); + font-size: 1.5rem; + line-height: 1.2; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; +} +.rv-tile small { + color: var(--label-2); + font-size: 0.6875rem; + line-height: 1.45; +} +.rv-tabs { + margin-bottom: 14px; +} +.rv-tabs .seg-btn { + padding: 4px 2px; + font-size: 0.8125rem; +} +#tab-body > .hint, +.review-wrap p.hint { + margin: 10px 4px; +} /* 表 */ -.rv-table-wrap { overflow-x: auto; margin-top: 8px; border-radius: 12px; background: var(--panel); } -.rv-table { width: 100%; border-collapse: collapse; font-size: 13px; } -.rv-table th, .rv-table td { padding: 8px 6px; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--line); } -.rv-table th { color: var(--muted); font-size: 11px; font-weight: 700; } -.rv-table .num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; } -.rv-table tbody tr[data-hand] { cursor: pointer; } -.rv-table tbody tr[data-hand]:hover, .rv-table tbody tr[data-hand]:focus-visible { background: var(--panel-2); outline: none; } -.rv-table tfoot td { font-weight: 800; border-bottom: 0; } -.rv-hand-cell b { margin-right: 2px; } -.rv-table .rv-color { font-size: 11px; padding: 1px 6px; } +.rv-table-wrap { + margin-top: 8px; + overflow-x: auto; + border-radius: 14px; + background: var(--bg-2); +} +.rv-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; +} +.rv-table th, +.rv-table td { + padding: 9px 8px; + border-bottom: 0.5px solid var(--separator); + text-align: left; + white-space: nowrap; +} +.rv-table th { + color: var(--label-2); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.02em; +} +.rv-table .num { + text-align: right; + font-weight: 600; + font-variant-numeric: tabular-nums; +} +.rv-table tbody tr[data-hand] { + cursor: pointer; + transition: background-color 0.35s ease; +} +.rv-table tbody tr[data-hand]:active { + background: var(--fill-3); + transition: none; +} +.rv-table tbody tr[data-hand]:focus-visible { + outline: 2px solid var(--blue); + outline-offset: -2px; +} +@media (hover: hover) { + .rv-table tbody tr[data-hand]:hover { + background: var(--fill-3); + } +} +.rv-table tfoot td { + border-bottom: 0; + font-weight: 700; +} +.rv-hand-cell b { + margin-right: 2px; +} +.rv-table .rv-color { + padding: 1px 6px; + font-size: 0.6875rem; +} -.rv-leaks { margin: 0; padding-left: 20px; display: grid; gap: 4px; font-size: 14px; } -.rv-leaks b { color: var(--gold); } +.rv-leaks { + display: grid; + gap: 6px; + margin: 0; + padding: 12px 14px 12px 32px; + border-radius: 14px; + background: var(--bg-2); + font-size: 0.9375rem; +} +.rv-leaks b { + color: var(--orange); +} /* 見直したいハンド */ -.rv-worst { display: grid; gap: 8px; } -.rv-worst-item { - display: grid; - grid-template-columns: 28px 1fr; - gap: 2px 8px; - align-items: center; - text-align: left; - font: inherit; - color: var(--text); - background: var(--panel); - border: 0; - border-radius: 12px; - padding: 10px 12px; - cursor: pointer; +.rv-worst { + overflow: hidden; + border-radius: 14px; + background: var(--bg-2); +} +.rv-worst-item { + -webkit-appearance: none; + appearance: none; + position: relative; + width: 100%; + display: grid; + grid-template-columns: 32px 1fr; + gap: 2px 10px; + align-items: center; + padding: 11px 14px; + border: 0; + background: transparent; + color: var(--label); + text-align: left; + cursor: pointer; + transition: background-color 0.35s ease; +} +.rv-worst-item + .rv-worst-item::before { + content: ""; + position: absolute; + top: 0; + right: 0; + left: 56px; + border-top: 0.5px solid var(--separator); +} +.rv-worst-item:active { + background: var(--fill-3); + transition: none; +} +.rv-rank { + grid-row: span 2; + color: var(--orange); + font-family: var(--font-num); + font-size: 1.375rem; + font-weight: 700; + text-align: center; +} +.rv-worst-meta { + color: var(--label-2); + font-size: 0.75rem; + font-variant-numeric: tabular-nums; +} +.rv-worst-item .rv-color { + width: 14px; + height: 14px; + padding: 0; + vertical-align: -2px; } -.rv-worst-item:hover, .rv-worst-item:focus-visible { background: var(--panel-2); } -.rv-rank { grid-row: span 2; font-size: 20px; font-weight: 900; color: var(--gold); text-align: center; } -.rv-worst-meta { font-size: 12px; color: var(--muted); } -.rv-worst-item .rv-color { width: 14px; height: 14px; padding: 0; vertical-align: -2px; } /* 収支のグラフ(実際 = 実線、運を除く = 破線。色だけに頼らない) */ -.rv-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; margin-bottom: 6px; } -.sw { display: inline-block; width: 16px; height: 0; border-top: 2px solid; vertical-align: middle; margin-right: 6px; } -.sw.actual { border-color: #b8891c; } -.sw.ev { border-color: #4a90e2; border-top-style: dashed; } -.rv-chart { position: relative; background: var(--panel); border-radius: 12px; padding: 8px; } -.rv-chart svg { display: block; width: 100%; height: auto; touch-action: pan-y; } -.rv-chart .gl { stroke: var(--line); stroke-width: 1; } -.rv-chart .zero { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 2 3; } -.rv-chart .tick { fill: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; } -.rv-chart .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; } -.rv-chart .line.actual { stroke: #b8891c; } -.rv-chart .line.ev { stroke: #4a90e2; stroke-dasharray: 6 4; } -.rv-chart .cross { stroke: var(--muted); stroke-width: 1; } -.rv-chart .dot { stroke: var(--panel); stroke-width: 2; } -.rv-chart .dot.actual { fill: #b8891c; } -.rv-chart .dot.ev { fill: #4a90e2; } -.rv-chart .hit { fill: transparent; cursor: crosshair; } +.rv-legend { + display: flex; + flex-wrap: wrap; + gap: 6px 16px; + margin: 0 4px 8px; + font-size: 0.8125rem; +} +.sw { + display: inline-block; + width: 16px; + height: 0; + margin-right: 6px; + border-top: 2px solid; + vertical-align: middle; +} +.sw.actual { + border-color: var(--orange); +} +.sw.ev { + border-color: var(--blue); + border-top-style: dashed; +} +.rv-chart { + position: relative; + padding: 10px; + border-radius: 16px; + background: var(--bg-2); +} +.rv-chart svg { + display: block; + width: 100%; + height: auto; + touch-action: pan-y; +} +.rv-chart .gl { + stroke: var(--separator); + stroke-width: 0.5; +} +.rv-chart .zero { + stroke: var(--label-3); + stroke-width: 1; + stroke-dasharray: 2 3; +} +.rv-chart .tick { + fill: var(--label-2); + font-size: 11px; + font-variant-numeric: tabular-nums; +} +.rv-chart .line { + fill: none; + stroke-width: 2; + stroke-linejoin: round; + stroke-linecap: round; +} +.rv-chart .line.actual { + stroke: var(--orange); +} +.rv-chart .line.ev { + stroke: var(--blue); + stroke-dasharray: 6 4; +} +.rv-chart .cross { + stroke: var(--label-3); + stroke-width: 1; +} +.rv-chart .dot { + stroke: var(--bg-2); + stroke-width: 2; +} +.rv-chart .dot.actual { + fill: var(--orange); +} +.rv-chart .dot.ev { + fill: var(--blue); +} +.rv-chart .hit { + fill: transparent; + cursor: crosshair; +} .rv-tip { position: absolute; - top: 8px; - background: var(--bg-deep); - border: 1px solid var(--line); - border-radius: 8px; - padding: 6px 10px; - font-size: 12px; - pointer-events: none; + top: 10px; + padding: 7px 11px; + border-radius: 12px; + background: var(--material-thick); + -webkit-backdrop-filter: blur(20px) saturate(180%); + backdrop-filter: blur(20px) saturate(180%); + box-shadow: var(--shadow-m), inset 0 0 0 0.5px var(--material-edge); + font-size: 0.75rem; + font-variant-numeric: tabular-nums; white-space: nowrap; + pointer-events: none; } /* リプレイ */ -.rv-note { margin-top: 10px; font-size: 13px; background: var(--panel); border-radius: 10px; padding: 8px 10px; } -.rv-copy { width: 100%; margin-top: 10px; } -.rv-board-box { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 14px; background: var(--felt-dark); border-radius: 12px; padding: 10px; } -.rv-board { display: flex; gap: 3px; } -.rv-pot { font-weight: 900; color: #fff; font-variant-numeric: tabular-nums; } -.rv-step { margin-top: 10px; background: var(--panel); border-radius: 12px; padding: 10px 12px; font-size: 14px; min-height: 64px; } -.rv-step-title { font-size: 12px; font-weight: 800; color: var(--gold); margin-bottom: 2px; } -.rv-controls { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 8px; } -.rv-controls .btn { padding: 10px 0; font-size: 16px; } -.rv-analysis { margin-top: 10px; background: var(--panel); border: 1px solid var(--gold); border-radius: 12px; padding: 6px 12px 10px; } -.rv-analysis .rv-item:first-child { border-top: 0; } -.rv-ev { margin-top: 6px; } -.rv-ev tr.actual td { background: rgba(246, 199, 68, 0.08); } -.rv-ev .best { color: var(--green); } -.rv-players { display: grid; gap: 6px; margin-top: 12px; } -.rv-player { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center; background: var(--panel); border-radius: 10px; padding: 6px 10px; border: 2px solid transparent; } -.rv-player.hero { border-color: rgba(246, 199, 68, 0.35); } -.rv-player.acting { border-color: var(--gold); } -.rv-player.folded { opacity: 0.55; } -.rv-player-info { min-width: 0; font-size: 14px; } -.rv-player-act { font-size: 12px; font-weight: 800; color: var(--muted); text-align: right; } -.rv-pos { font-size: 11px; font-weight: 800; color: var(--gold); margin-left: 4px; } -.rv-log { display: flex; flex-wrap: wrap; gap: 4px; } -.rv-log-street { width: 100%; font-size: 11px; color: var(--muted); font-weight: 800; margin-top: 6px; } -.rv-log-item { - font: inherit; - font-size: 12px; - color: var(--text); - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; - padding: 4px 8px; - cursor: pointer; +.rv-note { + margin-top: 10px; + padding: 10px 12px; + border-radius: 14px; + background: var(--bg-2); + font-size: 0.8125rem; +} +.rv-copy { + margin-top: 10px; +} +.rv-board-box { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: 14px; + padding: 12px 14px; + border-radius: 18px; + background: radial-gradient(120% 140% at 30% 20%, var(--felt-hi), var(--felt) 50%, var(--felt-lo)); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25), inset 0 8px 20px rgba(0, 0, 0, 0.25); +} +.rv-board { + display: flex; + gap: 4px; +} +.rv-pot { + color: #fff; + font-family: var(--font-num); + font-weight: 700; + font-variant-numeric: tabular-nums; +} +.rv-step { + min-height: 68px; + margin-top: 10px; + padding: 10px 14px; + border-radius: 14px; + background: var(--bg-2); + font-size: 0.9375rem; +} +.rv-step-title { + margin-bottom: 2px; + color: var(--label-2); + font-size: 0.75rem; + font-weight: 600; +} +.rv-controls { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-top: 8px; +} +.rv-controls .btn { + padding: 10px 0; +} +.rv-controls .btn .i { + width: 20px; + height: 20px; +} +.rv-analysis { + margin-top: 10px; + padding: 4px 14px 12px; + border-radius: 14px; + background: var(--bg-2); + box-shadow: inset 0 0 0 1.5px var(--orange); +} +.rv-ev { + margin-top: 6px; +} +.rv-ev tr.actual td { + background: rgba(255, 149, 0, 0.1); +} +.rv-ev .best { + color: var(--green); +} +.rv-players { + display: grid; + gap: 6px; + margin-top: 12px; +} +.rv-player { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px; + align-items: center; + padding: 8px 12px; + border-radius: 14px; + background: var(--bg-2); + box-shadow: inset 0 0 0 0 transparent; + transition: box-shadow 0.3s ease, opacity 0.3s ease; +} +.rv-player.hero { + box-shadow: inset 0 0 0 1.5px var(--tint-soft); +} +.rv-player.acting { + box-shadow: inset 0 0 0 2px var(--orange); +} +.rv-player.folded { + opacity: 0.5; +} +.rv-player-info { + min-width: 0; + font-size: 0.875rem; +} +.rv-player-act { + color: var(--label-2); + font-size: 0.75rem; + font-weight: 700; + text-align: right; +} +.rv-pos { + margin-left: 4px; + color: var(--tint); + font-size: 0.6875rem; + font-weight: 700; +} +.rv-log { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.rv-log-street { + width: 100%; + margin-top: 6px; + color: var(--label-2); + font-size: 0.6875rem; + font-weight: 600; +} +.rv-log-item { + -webkit-appearance: none; + appearance: none; + padding: 5px 10px; + border: 0; + border-radius: 999px; + background: var(--bg-2); + color: var(--label); + font-size: 0.75rem; + cursor: pointer; + box-shadow: inset 0 0 0 0.5px var(--separator); + transition: background-color 0.25s ease, color 0.25s ease, transform 0.45s var(--ease-spring); +} +.rv-log-item:active { + transform: scale(0.94); + transition: transform 0.1s ease-out; +} +.rv-log-item .hint { + margin: 0 4px; + font-size: 0.6875rem; +} +.rv-log-item.mine { + box-shadow: inset 0 0 0 1.5px var(--tint); +} +.rv-log-item.current { + background: var(--orange); + color: var(--on-orange); +} +.rv-log-item.current .hint { + color: rgba(0, 0, 0, 0.6); +} +.plus { + color: var(--green); +} +.minus { + color: var(--red); } -.rv-log-item .hint { margin: 0 4px; font-size: 11px; } -.rv-log-item.mine { border-color: rgba(246, 199, 68, 0.6); } -.rv-log-item.current { background: var(--gold); color: #1a1a1a; } -.rv-log-item.current .hint { color: #333; } diff --git a/admin/review.html b/admin/review.html index d2ccbf9..fa029fa 100644 --- a/admin/review.html +++ b/admin/review.html @@ -3,31 +3,35 @@ - + + CPU戦のレビュー - Hold'em Night - - -
-
- ← -

CPU戦のレビュー

+ + + +
+

CPU戦のレビュー

-

レビュー付きCPU戦の対局です(新しい順・最大50件)。

-
+
レビュー付きCPU戦の対局(新しい順・最大50件)
+
+ diff --git a/admin/review.js b/admin/review.js index 1eddcf2..3bd1a8b 100644 --- a/admin/review.js +++ b/admin/review.js @@ -19,6 +19,12 @@ let tab = 'scores'; let replay = null; // { hand, step } + const CHEVRON = ''; + const SPARKLES = + ''; + Fluid.navBar($('nav'), $('title')); + const tabsSeg = Fluid.Segmented($('tabs')); + const markOf = (score) => (score == null ? '' : score >= 90 ? '◎' : score >= 70 ? '○' : score >= 40 ? '△' : '✕'); const scoreClass = (score) => (score == null ? 'none' : MARK_CLASS[markOf(score)]); const signed = (n) => (n > 0 ? '+' : n < 0 ? '−' : '±') + chips(Math.abs(n)); @@ -55,6 +61,7 @@ if (!id) { $('title').textContent = 'CPU戦のレビュー'; $('back').href = '/'; + $('back-label').textContent = 'ロビー'; show('list'); return loadList(); } @@ -71,10 +78,12 @@ const h = session.hands.find((x) => x.number === hand); if (h) { $('back').href = `?s=${encodeURIComponent(id)}`; + $('back-label').textContent = '対局'; return openReplay(h); } } $('back').href = '/review'; + $('back-label').textContent = '一覧'; show('session'); renderSession(); } @@ -105,9 +114,12 @@ $('sessions').innerHTML = list .map( (s) => ` - ` ) .join(''); @@ -125,7 +137,8 @@
ハンド数${s.stats.hands}
平均点${s.total == null ? '—' : s.total}
収支${signed(net)}
`; - for (const b of $('tabs').children) b.classList.toggle('active', b.dataset.tab === tab); + for (const b of $('tabs').querySelectorAll('[data-tab]')) b.classList.toggle('active', b.dataset.tab === tab); + tabsSeg.update(); const render = { scores: renderScores, stats: renderStats, range: renderRange, worst: renderWorst, graph: renderGraph }[tab]; $('tab-body').innerHTML = ''; render(); @@ -370,7 +383,7 @@
${STREETS.map((st) => `
${STREET_SHORT[st]}${h.streetScores[st] == null ? '—' : h.streetScores[st]}
`).join('')}
${allInHtml(h)} - `; + `; $('replay-copy').addEventListener('click', () => copyHandPrompt(h)); $('replay-log').innerHTML = logHtml(h); renderStep(); diff --git a/public/assets/common.js b/public/assets/common.js index d0ce261..a9a9776 100644 --- a/public/assets/common.js +++ b/public/assets/common.js @@ -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 = - '