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:
agomizi1cmandClaude Opus 5.5 committed 2026-10-21 21:44:21 +09:00
1 parent d5ef2d310f
commit c3fbfdd12e
18 files changed
+4877 -1278

No files matched your search

+179 -52
View File
@@ -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); }
+21 -13
View File
@@ -3,42 +3,49 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#24493b">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow">
<title>ハンドレンジ - Hold'em Night</title>
<link rel="icon" href="/assets/favicon.ico">
<link rel="stylesheet" href="/assets/style.css">
<link rel="stylesheet" href="/range/files/range.css">
</head>
<body>
<main class="wrap range-wrap">
<div class="topbar">
<a class="icon-btn" href="/" aria-label="ロビーへ戻る">←</a>
<h1>ハンドレンジ</h1>
<body class="page-range">
<header class="nav-bar" id="nav">
<div class="nav-inner">
<a class="nav-back" href="/" aria-label="ロビーへ戻る"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>ロビー</a>
<span class="nav-title"></span>
</div>
</header>
<div class="seg" id="tabs">
<main class="wrap range-wrap">
<h1 class="large-title" id="large-title">ハンドレンジ</h1>
<div class="segmented range-tabs" id="tabs">
<button type="button" class="seg-btn active" data-view="range">レンジ表</button>
<button type="button" class="seg-btn" data-view="quiz">クイズ</button>
</div>
<!-- レンジ表 -->
<section id="view-range">
<div class="range-matrix" id="matrix" aria-label="13×13 ハンドレンジ表"></div>
<div class="matrix-card">
<div class="range-matrix" id="matrix" aria-label="13×13 ハンドレンジ表"></div>
</div>
<div class="range-legend" id="legend"></div>
<div class="card mt-12" id="selected">
<div class="card selected-card" id="selected">
<div class="sel-hand">—</div>
<div class="hint">セルをタップすると分類を確認できます</div>
</div>
<button type="button" class="btn btn-orange btn-block mt-12" id="start-from-range">クイズを始める</button>
<button type="button" class="btn btn-tint btn-lg btn-block mt-12" id="start-from-range">クイズを始める</button>
</section>
<!-- クイズ -->
<section id="view-quiz" hidden>
<div class="label">問題数(全169ハンドからランダム)</div>
<div class="seg" id="count">
<div class="segmented" id="count">
<button type="button" class="seg-btn active" data-count="10">10問</button>
<button type="button" class="seg-btn" data-count="20">20問</button>
<button type="button" class="seg-btn" data-count="0">全問</button>
@@ -64,12 +71,13 @@
<div class="quiz-feedback" id="feedback" aria-live="polite" hidden></div>
<div class="row mt-12">
<button type="button" class="btn btn-dark" id="start-quiz">開始</button>
<button type="button" class="btn btn-blue" id="next-question" disabled>次の問題 →</button>
<button type="button" class="btn btn-lg" id="start-quiz">開始</button>
<button type="button" class="btn btn-tint btn-lg" id="next-question" disabled>次の問題</button>
</div>
</section>
</main>
<script src="/assets/fluid.js"></script>
<script src="/range/files/range-data.js"></script>
<script src="/range/files/range.js"></script>
</body>
+10 -2
View File
@@ -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;
}
+440 -121
View File
@@ -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; }
+19 -14
View File
@@ -3,31 +3,35 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#24493b">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow">
<title>CPU戦のレビュー - Hold'em Night</title>
<link rel="icon" href="/assets/favicon.ico">
<link rel="stylesheet" href="/assets/style.css">
<link rel="stylesheet" href="/range/files/range.css">
<link rel="stylesheet" href="/range/files/review.css">
</head>
<body>
<main class="wrap review-wrap">
<div class="topbar">
<a class="icon-btn" id="back" href="/" aria-label="戻る">←</a>
<h1 id="title">CPU戦のレビュー</h1>
<body class="page-review">
<header class="nav-bar" id="nav">
<div class="nav-inner">
<a class="nav-back" id="back" href="/" aria-label="戻る"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg><span id="back-label">ロビー</span></a>
<span class="nav-title"></span>
</div>
</header>
<main class="wrap review-wrap">
<h1 class="large-title" id="title">CPU戦のレビュー</h1>
<!-- 対局の一覧 -->
<section id="view-list">
<p class="hint">レビュー付きCPU戦の対局です(新しい順・最大50件)。</p>
<div id="sessions" class="rv-sessions"></div>
<div class="group-header">レビュー付きCPU戦の対局(新しい順・最大50件)</div>
<div id="sessions" class="list rv-sessions"></div>
</section>
<!-- 1回の対局 -->
<section id="view-session" hidden>
<div class="rv-overview" id="overview"></div>
<div class="seg rv-tabs" id="tabs">
<div class="segmented rv-tabs" id="tabs">
<button type="button" class="seg-btn active" data-tab="scores">点数表</button>
<button type="button" class="seg-btn" data-tab="stats">成績</button>
<button type="button" class="seg-btn" data-tab="range">色・位置</button>
@@ -46,10 +50,10 @@
</div>
<div class="rv-step" id="replay-step" aria-live="polite"></div>
<div class="rv-controls">
<button type="button" class="btn btn-dark" data-step="first" aria-label="最初へ">⏮</button>
<button type="button" class="btn btn-dark" data-step="prev" aria-label="前へ">◀</button>
<button type="button" class="btn btn-blue" data-step="next" aria-label="次へ">▶</button>
<button type="button" class="btn btn-dark" data-step="last" aria-label="最後へ">⏭</button>
<button type="button" class="btn" data-step="first" aria-label="最初へ"><svg class="i i-fill" viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="5.5" width="2.4" height="13" rx="1"/><path d="M19 6.6v10.8a.9.9 0 0 1-1.4.75L9.6 12.9a1.1 1.1 0 0 1 0-1.8l8-5.25a.9.9 0 0 1 1.4.75z"/></svg></button>
<button type="button" class="btn" data-step="prev" aria-label="前へ"><svg class="i i-fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M17 6.6v10.8a.9.9 0 0 1-1.4.75L7.6 12.9a1.1 1.1 0 0 1 0-1.8l8-5.25a.9.9 0 0 1 1.4.75z"/></svg></button>
<button type="button" class="btn btn-tint" data-step="next" aria-label="次へ"><svg class="i i-fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 6.6v10.8a.9.9 0 0 0 1.4.75l8-5.25a1.1 1.1 0 0 0 0-1.8l-8-5.25A.9.9 0 0 0 7 6.6z"/></svg></button>
<button type="button" class="btn" data-step="last" aria-label="最後へ"><svg class="i i-fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6.6v10.8a.9.9 0 0 0 1.4.75l8-5.25a1.1 1.1 0 0 0 0-1.8l-8-5.25A.9.9 0 0 0 5 6.6z"/><rect x="16.6" y="5.5" width="2.4" height="13" rx="1"/></svg></button>
</div>
<div id="replay-analysis"></div>
<div class="rv-players" id="replay-players"></div>
@@ -58,6 +62,7 @@
</section>
</main>
<script src="/assets/fluid.js"></script>
<script src="/assets/common.js"></script>
<script src="/range/files/review.js"></script>
</body>
+18 -5
View File
@@ -19,6 +19,12 @@
let tab = 'scores';
let replay = null; // { hand, step }
const CHEVRON = '<svg class="i chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>';
const SPARKLES =
'<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 3.5l1.6 4.4L16 9.5l-4.4 1.6L10 15.5l-1.6-4.4L4 9.5l4.4-1.6z"/><path d="M18 14l.8 2.2L21 17l-2.2.8L18 20l-.8-2.2L15 17l2.2-.8z"/></svg>';
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) => `
<button type="button" class="rv-session" data-id="${esc(s.id)}">
<span class="rv-session-name">${esc(s.tableName)}${s.active ? '<span class="badge live">PLAYING</span>' : ''}</span>
<span class="rv-session-meta">${dateText(s.createdAt)} ・ ${s.hands}ハンド ・ ${esc(s.heroName)}</span>
<button type="button" class="cell cell-link rv-session" data-id="${esc(s.id)}">
<span class="cell-title">
<span class="rv-session-name">${esc(s.tableName)}${s.active ? '<span class="badge live">PLAYING</span>' : ''}</span>
<span class="cell-sub">${dateText(s.createdAt)} ・ ${s.hands}ハンド ・ ${esc(s.heroName)}</span>
</span>
${CHEVRON}
</button>`
)
.join('');
@@ -125,7 +137,8 @@
<div class="rv-tile"><span>ハンド数</span><b>${s.stats.hands}</b></div>
<div class="rv-tile"><span>平均点</span><b class="score-${scoreClass(s.total)}">${s.total == null ? '—' : s.total}</b></div>
<div class="rv-tile"><span>収支</span><b class="${signClass(net)}">${signed(net)}</b></div>`;
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 @@
</div>
<div class="rv-streets">${STREETS.map((st) => `<div class="rv-street"><span>${STREET_SHORT[st]}</span><b class="score-${scoreClass(h.streetScores[st])}">${h.streetScores[st] == null ? '—' : h.streetScores[st]}</b></div>`).join('')}</div>
${allInHtml(h)}
<button type="button" class="btn btn-dark rv-copy" id="replay-copy">🤖 AIに質問する用にコピー</button>`;
<button type="button" class="btn btn-block rv-copy" id="replay-copy">${SPARKLES}AIに質問する用にコピー</button>`;
$('replay-copy').addEventListener('click', () => copyHandPrompt(h));
$('replay-log').innerHTML = logHtml(h);
renderStep();