Add player stats, in-table ranking, host seating and review return

- Stats page (/stats): play style radar (TAG/LAG/rock/fish), VPIP, PFR,
  ATS, 3bet, WTSD, W$SD, WWSF, per-street fold rate, aggression
  frequency, hand win rate and net in BB, per table type, each with a
  bands-and-explanation sheet. Hands are counted per human player when
  they finish and stored as totals under data/stats/, keyed by a hash
  of the player ID (sent in a POST body, never in the URL).
- Ranking sheet at the table: everyone seated, ordered by stack or by
  net (stack minus buy-ins), updating live.
- The host can reorder seats by dragging (with a mini table preview and
  shuffle). Takes effect immediately between hands, otherwise at the
  start of the next hand; the dealer button stays with the same player.
- The full-session review opens in the same tab and its back button
  returns to the table it was opened from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-22 16:04:33 +09:00
1 parent 49203f202c
commit dd39c043c4
18 files changed
+2699 -18

No files matched your search

+877
View File
@@ -3016,6 +3016,876 @@ body.page-table {
}
}
/* ---------- 席順の変更(ホスト) ---------- */
.glass-btn:disabled {
opacity: 0.4;
cursor: default;
}
/* 次のハンドから反映する席順があるときの印 */
.glass-dot {
position: absolute;
top: 5px;
right: 5px;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--orange);
box-shadow: 0 0 0 2px var(--material-thick);
}
/* 小さなテーブル(自分が一番下で時計回り) */
.seating-preview {
position: relative;
width: min(100%, 300px);
aspect-ratio: 1.45;
margin: 2px auto 6px;
}
.seating-felt {
position: absolute;
inset: 16% 14%;
border-radius: 999px;
background: radial-gradient(120% 120% at 50% 30%, var(--felt-hi), var(--felt) 55%, var(--felt-lo));
box-shadow: 0 0 0 5px #2a2a2c, var(--shadow-s);
}
.seating-dot {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
width: 64px;
transform: translate(-50%, -50%);
transition: left 0.5s var(--ease-spring), top 0.5s var(--ease-spring);
}
.seating-dot .rank-avatar {
width: 30px;
height: 30px;
font-size: 0.875rem;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25);
}
.seating-dot .rank-avatar.bot {
border-radius: 9px;
}
.seating-dot .rank-avatar svg {
width: 17px;
height: 17px;
}
.seating-dot.me .rank-avatar {
box-shadow: 0 0 0 2px var(--sheet-bg), 0 0 0 4px var(--tint);
}
.seating-name {
max-width: 100%;
padding: 0 5px;
border-radius: 6px;
background: var(--material-thick);
font-size: 0.6875rem;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.seating-tools {
display: flex;
align-items: center;
gap: 12px;
margin: 4px 0 10px;
}
.seating-tools .hint {
flex: 1;
margin: 0;
}
.seating-tools .btn {
flex: none;
}
.seating-list {
margin: 0;
padding: 0;
list-style: none;
overflow: visible;
}
.seat-row {
gap: 12px;
min-height: 60px;
background: var(--bg-2);
will-change: transform;
}
.seat-row:first-child {
border-radius: 14px 14px 0 0;
}
.seat-row:last-child {
border-radius: 0 0 14px 14px;
}
.seat-row:only-child {
border-radius: 14px;
}
.seat-row.me {
background: var(--tint-soft);
}
.cell.seat-row + .cell::before {
left: 64px;
}
/* つかんでいる行は持ち上げて手前に */
.seat-row.dragging {
z-index: 2;
border-radius: 14px;
box-shadow: var(--shadow-l);
}
.seat-row.dragging::before,
.seat-row.dragging + .cell::before {
opacity: 0;
}
.seat-row .cell-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.seat-pin {
flex: none;
padding: 2px 9px;
border-radius: 999px;
background: var(--fill-3);
color: var(--label-2);
font-size: 0.75rem;
font-weight: 600;
}
.seat-handle {
-webkit-appearance: none;
appearance: none;
flex: none;
width: 44px;
height: 44px;
margin: -8px -10px -8px 0;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 10px;
background: transparent;
color: var(--label-3);
cursor: grab;
touch-action: none;
}
.seat-row.dragging .seat-handle {
color: var(--label);
cursor: grabbing;
}
.seating-pending {
padding: 10px 16px 0;
}
/* ---------- テーブル内のランキング ---------- */
.ranking-list {
margin: 12px 0 0;
padding: 0;
list-style: none;
}
.rank-row {
gap: 12px;
background: var(--bg-2);
will-change: transform;
}
.rank-row.me {
background: var(--tint-soft);
}
.cell.rank-row:has(.rank-avatar) + .cell::before {
left: 98px;
}
.rank-no {
flex: none;
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 50%;
font-family: var(--font-num);
font-size: 0.9375rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--label-2);
}
.rank-no.top-1 {
background: linear-gradient(180deg, #ffd96a, #e7a913);
color: #4a3200;
}
.rank-no.top-2 {
background: linear-gradient(180deg, #e9ecf0, #aeb5bf);
color: #2f343b;
}
.rank-no.top-3 {
background: linear-gradient(180deg, #f0b98a, #c47a3e);
color: #3d1f06;
}
.rank-avatar {
--hue: 210;
flex: none;
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 50%;
background: linear-gradient(180deg, hsl(var(--hue) 48% 64%), hsl(var(--hue) 46% 42%));
color: #fff;
font-size: 1rem;
font-weight: 600;
line-height: 1;
}
.rank-avatar.bot {
border-radius: 10px;
}
.rank-avatar svg {
width: 20px;
height: 20px;
fill: #fff;
}
.rank-row .cell-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.rank-value {
flex: none;
font-family: var(--font-num);
font-size: 1.0625rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.ranking-note {
padding: 8px 16px 0;
}
/* ---------- 統計 ---------- */
:root {
/* レーダーの軸の色と、自分のスタイルの印 */
--st-aggr: #e8378a;
--st-passive: #1a9fd0;
--st-tight: #9a45dd;
--st-loose: #e08700;
--st-pick: #f0896a;
--st-on-pick: #2b1206;
}
@media (prefers-color-scheme: dark) {
:root {
--st-aggr: #ff4f9a;
--st-passive: #3fc8f4;
--st-tight: #c278ff;
--st-loose: #ffab3d;
}
}
/* 段階の色: スタイル(参加・積極性)は緑 → ピンク、成績(勝率など)は赤 → 緑 */
.tone-style-0 { --tone: #5fd38f; --tone-2: #85e0aa; }
.tone-style-1 { --tone: #8fcfa3; --tone-2: #aac9b0; }
.tone-style-2 { --tone: #b8b0b9; --tone-2: #c9a3b9; }
.tone-style-3 { --tone: #d48db5; --tone-2: #e07fb1; }
.tone-style-4 { --tone: #e5649f; --tone-2: #f04a95; }
.tone-quality-0 { --tone: #ef5f73; --tone-2: #e7998f; }
.tone-quality-1 { --tone: #e5a564; --tone-2: #e2c17c; }
.tone-quality-2 { --tone: #a9cf74; --tone-2: #96d98c; }
.tone-quality-3 { --tone: #4fcd88; --tone-2: #3cc38a; }
.nav-action {
-webkit-appearance: none;
appearance: none;
grid-column: 3;
justify-self: end;
width: 44px;
height: 44px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 10px;
background: transparent;
color: var(--tint);
cursor: pointer;
transition: opacity 0.25s;
}
.nav-action:active {
opacity: 0.4;
transition: none;
}
.nav-action.spinning .i {
animation: spin 0.8s linear infinite;
}
.st-card {
position: relative;
padding: 14px 16px 16px;
border-radius: 18px;
background: var(--bg-2);
box-shadow: inset 0 0 0 1px var(--fill-3);
}
.st-grid {
display: grid;
gap: 12px;
}
.st-grid-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.st-grid-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
#all-body > * {
margin-top: 12px;
}
.st-tabs {
margin-bottom: 4px;
}
.st-empty {
color: var(--label-2);
font-size: 0.9375rem;
text-align: center;
}
.st-foot {
margin: 20px 4px 0;
}
/* 見出し(アイコン・名前・?) */
.st-head {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
color: var(--label-2);
font-size: 0.875rem;
font-weight: 500;
}
.st-icon {
font-size: 0.9375rem;
line-height: 1;
filter: grayscale(1);
opacity: 0.85;
}
.st-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.st-help {
-webkit-appearance: none;
appearance: none;
flex: none;
width: 30px;
height: 30px;
margin: -6px -8px -6px 0;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--label-3);
cursor: pointer;
}
.st-help .i {
width: 19px;
height: 19px;
stroke-width: 1.7;
}
.st-help:active {
background: var(--fill-3);
}
.st-value {
position: relative;
z-index: 1;
margin-top: 6px;
font-family: var(--font-num);
font-size: 2.75rem;
font-weight: 500;
line-height: 1.05;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.st-value small {
margin-left: 2px;
font-size: 1.0625rem;
font-weight: 500;
letter-spacing: 0;
}
.st-metric.compact .st-value {
font-size: 1.75rem;
}
.st-metric.compact .st-value small {
font-size: 0.9375rem;
}
.st-band {
margin-top: 8px;
font-size: 0.9375rem;
font-weight: 500;
}
.st-metric.compact .st-band {
margin-top: 6px;
font-size: 0.8125rem;
color: var(--label-2);
}
/* 段階のバー */
.st-bar {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 4px;
margin-top: 12px;
}
.st-seg {
display: block;
height: 6px;
border-radius: 3px;
background: var(--fill-2);
overflow: hidden;
}
.st-seg-fill {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--tone), var(--tone-2));
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.7s var(--ease-spring);
}
/* 水位で見せるタイル(WTSD・W$SD・WWSF) */
.st-liquid-card {
min-height: 118px;
overflow: hidden;
padding: 12px 12px 14px;
}
/* 3つ並ぶので見出しを詰める(WTSD などが省略されないように) */
.st-liquid-card .st-head {
gap: 4px;
font-size: 0.8125rem;
}
.st-liquid-card .st-icon {
font-size: 0.8125rem;
}
.st-liquid-card .st-name {
overflow: visible;
text-overflow: clip;
}
.st-liquid-card .st-help {
width: 24px;
height: 24px;
margin: -4px -6px -4px 0;
}
.st-liquid-card .st-help .i {
width: 17px;
height: 17px;
}
.st-liquid-card .st-value {
font-size: 2rem;
}
.st-liquid-card .st-value small {
font-size: 0.875rem;
}
.st-liquid {
--level: 0%;
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: var(--level);
background: var(--fill-2);
transition: height 1s var(--ease-spring);
}
.st-liquid::before {
content: "";
position: absolute;
left: 0;
bottom: 100%;
width: 200%;
height: 8px;
background: inherit;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 8' preserveAspectRatio='none'%3E%3Cpath d='M0 4C15 0 15 0 30 4S45 8 60 4 75 0 90 4 105 8 120 4V8H0z'/%3E%3C/svg%3E") repeat-x;
-webkit-mask-size: 50% 100%;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 8' preserveAspectRatio='none'%3E%3Cpath d='M0 4C15 0 15 0 30 4S45 8 60 4 75 0 90 4 105 8 120 4V8H0z'/%3E%3C/svg%3E") repeat-x;
mask-size: 50% 100%;
animation: st-wave 6s linear infinite;
}
@keyframes st-wave {
to {
transform: translateX(-50%);
}
}
/* 小さなメーター(フォールド率) */
.st-minis {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px;
margin-top: 10px;
text-align: center;
}
.st-mini-gauge {
position: relative;
width: 100%;
max-width: 76px;
margin: 0 auto;
aspect-ratio: 1;
}
.st-gauge {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.st-gauge path {
fill: none;
stroke-width: 6;
stroke-linecap: round;
}
.st-gauge-track {
stroke: var(--fill);
}
.st-gauge-val {
stroke: var(--label);
}
.st-mini-gauge > span {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: var(--font-num);
font-size: 1.0625rem;
font-variant-numeric: tabular-nums;
}
.st-mini-gauge small {
font-size: 0.6875rem;
}
.st-mini-label {
display: block;
margin-top: -2px;
font-size: 0.8125rem;
color: var(--label-2);
}
/* 輪と一覧(アグレッション頻度・ハンド・収支) */
.st-split {
display: grid;
grid-template-columns: minmax(0, 132px) minmax(0, 1fr);
gap: 16px;
align-items: center;
margin-top: 8px;
}
.st-ring-box {
position: relative;
aspect-ratio: 1;
}
.st-ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.st-ring path {
fill: none;
stroke-width: 8;
stroke-linecap: round;
}
.st-ring-track {
stroke: var(--fill-2);
}
.st-ring-val {
stroke: var(--tone);
}
.st-ring-text {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.st-ring-text .st-value {
margin: 0;
font-size: 2rem;
}
.st-ring-text .st-value small {
font-size: 0.875rem;
}
.st-ring-text > span {
font-size: 0.8125rem;
color: var(--label-2);
}
.st-rows {
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 14px;
border-left: 0.5px solid var(--separator);
}
.st-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
padding: 9px 0;
font-size: 0.9375rem;
color: var(--label-2);
}
.st-row + .st-row {
border-top: 0.5px solid var(--separator);
}
.st-row b {
color: var(--label);
font-family: var(--font-num);
font-size: 1.0625rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.st-net {
text-align: center;
}
.st-net b {
display: block;
font-family: var(--font-num);
font-size: 2rem;
font-weight: 500;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.st-net span {
font-size: 0.8125rem;
color: var(--label-2);
}
/* プレイスタイルのレーダー */
.st-style {
padding: 6px;
cursor: pointer;
transition: transform 0.45s var(--ease-spring);
}
.st-style:active {
transform: scale(0.985);
transition: transform 0.1s ease-out;
}
.st-radar {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.st-radar-ring {
fill: none;
stroke: var(--label-3);
stroke-width: 1;
stroke-dasharray: 4 4;
}
.st-radar-outer {
fill: none;
stroke: var(--label-3);
stroke-width: 1.5;
}
.st-radar-axis {
stroke: var(--label-3);
stroke-width: 1;
stroke-dasharray: 2 4;
}
.st-radar-shape {
stroke: none;
}
.st-radar-link {
stroke: var(--st-pick);
stroke-width: 2;
}
.st-chip rect {
fill: transparent;
stroke: var(--separator);
stroke-width: 1;
}
.st-chip text {
fill: var(--label-2);
font-size: 14px;
font-weight: 500;
}
.st-chip.on rect {
fill: var(--st-pick);
stroke: none;
}
.st-chip.on text {
fill: var(--st-on-pick);
font-weight: 600;
}
.st-axis {
font-size: 17px;
font-weight: 600;
}
.st-axis.aggr {
fill: var(--st-aggr);
}
.st-axis.passive {
fill: var(--st-passive);
}
.st-axis.tight {
fill: var(--st-tight);
}
.st-axis.loose {
fill: var(--st-loose);
}
.st-axis-sub {
fill: var(--label-2);
font-size: 11.5px;
}
.st-empty-note {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--label-2);
font-size: 0.9375rem;
pointer-events: none;
}
/* 説明シート */
.st-info-icon {
margin-top: 4px;
font-size: 3.25rem;
line-height: 1.1;
text-align: center;
}
.st-info-title {
margin: 8px 0 14px;
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.015em;
text-align: center;
}
.st-info-value {
margin-bottom: 16px;
font-family: var(--font-num);
font-size: 2.25rem;
font-weight: 500;
line-height: 1.1;
text-align: center;
font-variant-numeric: tabular-nums;
}
.st-info-value small {
font-size: 1rem;
}
.st-scale {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 4px;
text-align: center;
}
.st-scale-col {
display: grid;
gap: 6px;
min-width: 0;
}
.st-scale-col .st-seg {
height: 10px;
border-radius: 5px;
}
.st-scale-label,
.st-scale-range {
font-size: 0.75rem;
line-height: 1.2;
color: var(--label-2);
white-space: nowrap;
}
.st-scale-col.on .st-scale-label,
.st-scale-col.on .st-scale-range {
color: var(--label);
font-weight: 600;
}
.st-info-desc {
margin-top: 20px;
color: var(--label-2);
font-size: 1rem;
line-height: 1.65;
text-align: center;
}
.st-info-sample {
margin: 10px 0 4px;
color: var(--label-3);
font-size: 0.8125rem;
text-align: center;
}
.btn.st-share {
background: var(--label);
color: var(--bg-2);
}
/* プレイスタイルのシート(横に送る) */
.st-carousel {
display: flex;
gap: 12px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
padding: 4px 9% 2px;
scrollbar-width: none;
}
.st-carousel::-webkit-scrollbar {
display: none;
}
.st-type {
flex: 0 0 82%;
scroll-snap-align: center;
padding: 26px 20px 24px;
border-radius: 22px;
background: var(--bg-2);
box-shadow: inset 0 0 0 1px var(--fill-2);
text-align: center;
}
.st-type-emoji {
font-size: 4rem;
line-height: 1.1;
}
.st-type-name {
margin-top: 10px;
font-size: 1.625rem;
font-weight: 600;
letter-spacing: -0.015em;
}
.st-type-full {
color: var(--label-2);
}
.st-type-desc {
margin-top: 14px;
color: var(--label-2);
font-size: 0.9375rem;
line-height: 1.65;
}
.st-type-mine {
display: inline-block;
margin-top: 14px;
padding: 4px 12px;
border-radius: 999px;
background: var(--st-pick);
color: var(--st-on-pick);
font-size: 0.8125rem;
font-weight: 600;
}
.st-dots {
display: flex;
justify-content: center;
gap: 8px;
padding: 14px 0 2px;
}
.st-dots span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--label-3);
transition: background-color 0.2s;
}
.st-dots span.on {
background: var(--label);
}
/* ---------- 視差効果を減らす: 動きは短いクロスフェードに ---------- */
@media (prefers-reduced-motion: reduce) {
.pcard.deal,
@@ -3054,6 +3924,13 @@ body.page-table {
.nav-title {
transform: none;
}
.st-liquid::before {
animation: none;
}
.st-seg-fill,
.st-liquid {
transition: none;
}
}
@keyframes fade-in {
from {